简介:一套仿京东数码网页的纯前端源码,基于HTML、CSS与JavaScript构建,完整呈现电商页面常见的导航、横幅、商品陈列与动态交互,适合前端初学者模仿练习,也可作为电商风格页面的参考模板。压缩包共20个文件、约503KB,内含2个HTML页面、1个CSS样式文件,以及17张jpg/png图片素材,主要用作商品图、背景图和图标等。已有134人学习下载,资源内图片素材分类放置,目录结构清晰。通过打开两个HTML文件即可直接查看动态效果,对照源码可深入理解CSS布局、Flex/Grid排列以及JavaScript事件处理、轮播等常见交互实现,是练习纯前端页面开发的实用素材。
1. 仿京东页面,真正要先定的是交互清单
一个电商页面看起来复杂,其实拆开就两类事:结构怎么排、交互怎么动。而仿京东数码这种页面,最花时间的往往不是 html 和 CSS 的骨架,而是鼠标移入、滚动、轮播、切换这一串动态效果怎么用原生三件套还原出来。很多开发者第一反应是找组件库,但纯 CSS + HTML + JavaScript 反而能把页面做得很轻,也能把每个动效的触发条件、参数和边界都握在自己手里。这篇内容适合两类人:一类是刚能把页面画出来、想补齐交互细节的前端;另一类是需要用原生技术写活动页、专题页的工程师。先列交互清单,再逐段落地,后面的代码都是在为清单里的动作兜底。
2. 布局与卡片:栅格、优惠券锯齿、两行超出省略
几乎所有电商页面都长在同一套骨架上:顶部搜索栏、商品导航、轮播焦点图、商品流。模拟京东数码频道时,不需要把整站搬下来,重点是先把商品卡片的布局规则定清楚,因为商品卡片是动态效果的主要载体,轮播、筛选、hover 反馈都在它上面发生。
2.1 栅格骨架与区块划分
常见的做法是先定一个版心宽度,然后把商品列表做成响应式栅格。京东的版心在 PC 端是 1190px,我一般会用max-width: 1190px; margin: 0 auto;来做容器,既不写死宽度,也不会在大屏下拉伸得失去控制。列表区的栅格用 CSS Grid 最省事,因为gap属性直接管理卡片间距,不需要给每个子项写margin。
<div class="container"> <ul class="product-grid" id="product-list"> <!-- 商品卡片由 JavaScript 渲染 --> </ul> </div>.container { max-width: 1190px; margin: 0 auto; padding: 0 12px; } .product-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; list-style: none; padding: 0; } @media (max-width: 768px) { .product-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; } }repeat(4, 1fr)表示每一行放 4 列,每一列等分剩余宽度。1fr是 Grid 里的比例单位,卡片宽度会随容器伸缩,不会出现一列被内容撑破、其他列收缩的情况。媒体查询里改成repeat(2, 1fr),是为了在窄屏下避免卡片过窄被迫内部换行,属于移动端最先要考虑的一个降级策略。
2.2 商品卡片的三个 CSS 关键点:内边距、价格、两行省略
商品卡片看起来简单,但经常返工的是三处:图片与文字的内边距、价格与原价的层级关系、标题超出一行或两行后的截断方式。先看一个最小可用的卡片结构:
<li class="product-card"> <a href="#"> <div class="thumb"> <img src="img/headphone.jpg" alt="无线降噪耳机"> <span class="tag">热卖</span> </div> <div class="info"> <h3>无线降噪耳机 主动降噪 蓝牙5.4 长续航</h3> <div class="price-row"> <b class="price">¥399</b> <!-- css 删除线的典型场景:原价 --> <s class="old-price">¥599</s> </div> </div> </a> </li>.product-card { background: #fff; border-radius: 8px; overflow: hidden; transition: transform .25s ease, box-shadow .25s ease; } .product-card .thumb { position: relative; overflow: hidden; aspect-ratio: 1 / 1; background: #f5f5f5; } .thumb img { width: 100%; height: 100%; object-fit: cover; } .product-card .info { padding: 12px 14px 16px; } .product-card h3 { font-size: 14px; line-height: 1.5; color: #333; /* 两行超出省略,由下面三行组成 */ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .price-row { display: flex; align-items: baseline; gap: 8px; margin-top: 8px; } .price { color: #e1251b; font-size: 18px; font-weight: 700; } .old-price { color: #999; font-size: 13px; text-decoration: line-through; }aspect-ratio: 1 / 1能保证图片区域始终是正方形,配合object-fit: cover,图片裁切后不会变形。这里要注意,浏览器兼容性已经不是问题,但老的移动 WebView 里有时还要img { display: block; }来消掉行内元素底部产生的间隙,这个细节很容易被忽略。
两行超出省略是三行模式的 css 文件里很实用的东西:display: -webkit-box声明为弹性盒子,-webkit-line-clamp控制显示行数,-webkit-box-orient指定主轴方向,最后overflow: hidden。这一组不能拆,少一行都可能在某些浏览器里失效,尤其是box-orient,很多缩写工具会把它丢掉。
2.3 促销标签与优惠券锯齿的圆切实现
促销标签和优惠券是数码频道里最常见的视觉元素。标签好做,一个绝对定位的span加背景色就行;难点是优惠券左右两侧的半圆缺口,也就是热词里常说的 css 优惠券圆切。最简单的方案是用伪元素加圆形背景,让圆点盖在卡片边缘,形成视觉上的“打孔”效果。
.coupon { position: relative; display: inline-block; padding: 6px 24px; border: 1px dashed #e1251b; color: #e1251b; font-size: 13px; } .coupon::before, .coupon::after { content: ""; position: absolute; top: 50%; width: 12px; height: 12px; border-radius: 50%; background: #f5f5f5; transform: translateY(-50%); } .coupon::before { left: -6px; } .coupon::after { right: -6px; }伪元素的圆形背景必须和页面背景一致,而不是白色,否则圆切位置会露出白点。这里用#f5f5f5是因为所在区块背景就是浅灰。如果页面背景是渐变或图片,这个方法就不好用了,更严谨的做法是用clip-path直接裁切出锯齿,但clip-path画的锯齿边缘不平滑,而且对边框虚线支持不自然。
| 实现方式 | 代码量 | 锯齿圆滑度 | 适用场景 |
|---|---|---|---|
| 伪元素圆形遮盖 | 8-10 行 | 高 | 纯色背景 |
| clip-path 锯齿 | 5-8 行 | 边缘生硬 | 无边框、纯色背景 |
| SVG 背景图 | 需要单独文件 | 高 | 需要多色、渐变背景 |
促销标签我建议用position: absolute挂在图片左上角,标签宽度不固定,用left: 8px; top: 8px; padding: 2px 8px;控制位置和尺寸,不要用固定宽度,避免文案变长后溢出。
3. 纯 CSS 动效:鼠标移入、字体渐变与打点加载
栅格搭好、卡片有了静态样子后,下一步是把“动”的部分交给 CSS 处理。电商页的动态效果有相当大比例是纯 CSS 能完成的,而且比 JavaScript 驱动更顺滑,因为过渡和动画跑在合成器线程上,不需要频繁改 DOM 属性。这一章重点拆三个场景:鼠标移入反馈、价格字体渐变、打点 loading。
3.1 用 hover 和 transition 还原鼠标移入事件
京东数码卡片最常见的反馈是“上浮 + 阴影加深 + 图片轻微放大”。鼠标移入事件在这里不需要 JavaScript 监听,CSS 的:hover配合transition就可以完整覆盖。注意一个原则:不要在 hover 状态下直接改width、height、top这些会触发布局重排的属性,优先改transform和box-shadow。
.product-card { transition: transform .25s ease, box-shadow .25s ease; } .product-card:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0, 0, 0, .08); } .thumb img { transition: transform .4s cubic-bezier(.25, .8, .25, 1); } .product-card:hover .thumb img { transform: scale(1.06); }transition的拆分写法把transform和box-shadow分开声明,这样两个属性的变化时长和曲线可以独立控制。图片放大用的是.product-card:hover .thumb img后代选择器,因为放大的 flash 效果只作用于图片,但触发的热区是整个卡片。cubic-bezier曲线比默认的ease更柔和,前慢后快,松手时不突兀。
这里有一个容易犯的错:transition写在了:hover里面,导致鼠标移出时动画瞬间消失。正确做法是把transition写在默认状态上,让移入和移出都平滑过渡。
3.2 价格区的字体渐变和红黑层级
价格是数码频道视觉重点,京东常用红色渐变来强调数字,但渐变方向、饱和度都要克制,否则整屏数字都在发光会显得很吵。css 字体渐变的原理是先用background渲染渐变,再用background-clip: text把背景裁剪到文字形状,最后把文字颜色设为透明。
.price { background: linear-gradient(180deg, #e1251b 0%, #ff6a00 100%); -webkit-background-clip: text; background-clip: text; color: transparent; font-size: 20px; font-weight: 700; }要留意-webkit-background-clip: text的前缀,虽然现代浏览器普遍支持无前缀写法,但为了保险,两个都要写。透明文字如果遇到不支持background-clip: text的浏览器会变成不可见,所以这种效果只适合用在有降级方案的位置上。实战中我通常把数字拆成“梯梯”结构:整数部分用大字号红色,小数部分用小字号灰色,让价格看起来更专业。
3.3 用 steps 做打点 loading 动画
商品列表在请求数据等待、图片懒加载时,经常会用到打点动画:“加载中”后面跟着三个点依次出现。css 打点不复杂,核心是用steps()做一个步进动画,把宽度从 0 撑到 3em,通过overflow: hidden截断文字,模拟出点逐个出现的效果。
<p class="loading">加载中<span></span></p>.loading span { display: inline-block; width: 0; overflow: hidden; vertical-align: bottom; animation: dotting 1.2s steps(3, end) infinite; } @keyframes dotting { 0% { width: 0; } 100% { width: 3em; } }steps(3, end)表示整个动画分成 3 次跳跃完成,end参数让每一跳在动画时间段末尾生效,这样点不是连续滑出来的,而是按“加载中”“加载中.”“加载中..”“加载中...”的节奏递进。width从 0 到 3em,每跳增加 1em,正好一个点宽。这个方案不需要操作 content,也不依赖任何框架,对中文排版友好。
4. JavaScript 动态化:渲染、轮播与事件委托
纯 CSS 解决的是“静态页面上的动态感”,但商品列表的渲染、焦点图的切换、Tab 筛选这些真正的数据变化,还是要交给 JavaScript。这一章用的都是基础语法,不引入框架和构建工具,打开 html 文件就能跑通。
4.1 用数组加模板字符串渲染商品列表
商品卡片不要一条条写在 HTML 里,而是把数据放到数组里,再用模板字符串批量生成。这样后续接入接口、做筛选和排序都直接操作数据,不用改页面结构。javascript filter 函数在这里正好配合筛选场景使用。
const products = [ { id: 1, name: '无线降噪耳机 主动降噪 蓝牙5.4', price: 399, oldPrice: 599, tag: '热卖', type: 'audio' }, { id: 2, name: '智能手表 心率监测 血氧检测 运动模式', price: 999, oldPrice: 1299, tag: '新品', type: 'watch' }, { id: 3, name: '机械键盘 87键 PBT键帽 客制化', price: 199, oldPrice: 299, tag: '秒杀', type: 'keyboard' } ]; const list = document.getElementById('product-list'); function render(list) { const html = list.map(item => ` <li class="product-card">const track = document.querySelector('.carousel-track'); const slides = document.querySelectorAll('.carousel-slide'); let currentIndex = 0; let intervalId = null; function goTo(index) { const total = slides.length; if (index >= total) index = 0; if (index < 0) index = total - 1; currentIndex = index; track.style.transform = `translateX(-${currentIndex * 100}%)`; } function play() { if (intervalId) clearInterval(intervalId); intervalId = setInterval(() => { goTo(currentIndex + 1); }, 4000); } document.querySelector('.next').addEventListener('click', () => { goTo(currentIndex + 1); play(); }); document.addEventListener('visibilitychange', () => { if (document.hidden) { clearInterval(intervalId); } else { play(); } }); play();核心参数有两个:translateX(-${currentIndex * 100}%)是按视口宽度的百分比平移,轨道宽度默认就是视口宽度,所以每张图正好占满一屏;play里先clearInterval再重新设置,是防止用户手动切换后,旧的定时器和新定时器叠加导致切换速度快一倍。
visibilitychange的监听很重要:页面切到后台时浏览器会降频定时器,回到前台后会连续跳好几张图,所以在标签页不可见时清掉定时器,可见时重新启动,这是轮播最容易被忽略的一个坑。
4.3 合并配置对象、筛选函数与事件委托
代码写多了,配置会散落在各个函数里。页面初始化时通常会把默认配置和用户传入配置合并,用Object.assign或展开运算符都能做到。javascript 合并两个对象最稳妥的方式是后者,因为写起来清晰,也不会误改原对象。
const defaultOptions = { autoplay: true, interval: 4000, loop: true }; const userOptions = { interval: 3000 }; const options = { ...defaultOptions, ...userOptions };展开运算符的合并顺序是后者覆盖前者,interval最终变成 3000,autoplay和loop保持默认值。与Object.assign({}, defaultOptions, userOptions)的效果一致,但可读性更好。如果不小心把userOptions放在前面,默认值会把用户配置覆盖掉,这是新手经常遇到的反直觉问题。
商品筛选是另一个高频场景。javascript filter 函数不会修改原数组,而是返回一个新数组,适合和渲染函数组合使用:
function filterByType(type) { const filtered = products.filter(item => item.type === type); render(filtered); } document.querySelectorAll('.filter-tab').forEach(tab => { tab.addEventListener('click', event => { filterByType(tab.dataset.type); }); });动态渲染后的节点需要绑定事件时,最简单的方式是在每个按钮上循环绑定,但商品列表一旦超过几十个,逐个绑定既慢又不好维护。事件委托的思路是:把监听器挂到父容器上,利用事件冒泡找到真正被点击的子节点。
list.addEventListener('click', event => { const card = event.target.closest('.product-card'); if (!card) return; const id = Number(card.dataset.id); const product = products.find(item => item.id === id); if (product) { console.log('打开商品', product.name); } });event.target.closest('.product-card')会从实际点击的元素向上找,直到匹配到.product-card为止。if (!card) return用来过滤掉点击空白区域的情况,比判断tagName更抗结构变化。用dataset.id拿到的字符串转成数字,再通过find从数据源中找到完整商品信息,这样就避免把整条商品数据塞进>const nav = document.querySelector('.nav-bar'); const backTop = document.querySelector('.back-top'); let ticking = false; function updateOnScroll() { const scrolled = window.scrollY; nav.classList.toggle('fixed', scrolled > 120); backTop.classList.toggle('show', scrolled > 400); ticking = false; } window.addEventListener('scroll', () => { if (!ticking) { requestAnimationFrame(updateOnScroll); ticking = true; } }, { passive: true }); backTop.addEventListener('click', () => { window.scrollTo({ top: 0, behavior: 'smooth' }); });
requestAnimationFrame在这里做节流:滚动事件可能一帧内触发多次,但updateOnScroll每帧最多执行一次,保证读scrollY和改 class 的频率与屏幕刷新率对齐。ticking变量是个锁,函数执行完才打开,避免一帧内多次请求重复动画帧。classList.toggle的第二个参数是布尔值,为真时添加类,为假时移除类,所以不需要在滚动函数里手动写if/else。
一键返回顶部算法性能更好一点的做法是不依赖window.scrollTo的平滑滚动,而是自己用requestAnimationFrame做插值,因为behavior: 'smooth'在部分浏览器里动画不受控制,滚动过程中页面上其他动效也会被影响。简单页面用scrollTo足够,长列表页再用插值方案替换。
5.2 三行 CSS 验证常见布局问题
手写页面到最后,最常被问到的不是大功能,而是“input 为什么不在容器中间”“图片底部多了一条缝”“标题文字怎么都换行”。这些问题都能用三行模式的 css 文件快速解决,也是 css 中把 input 居中最直接的答案。
input { display: block; margin: 0 auto; } img { display: block; max-width: 100%; } p { word-break: break-all; }第一组用于区域内的块级 input,display: block让 input 独占一行,margin: 0 auto让它在父容器里水平居中。第三方 UI 库的 input 因为本身宽度由父容器决定,直接在外层包一个text-align: center效果反而更稳,两种方式要区分场景。第二组img { display: block; }消除行内元素放在基线底部产生的 4px 间隙,这是图片下方莫名多一条白缝的最常见原因。第三组是针对中英文混排的强制换行,数码商品标题里常有英文型号,长度过长时会被默认换行规则撑破布局。
验证动态效果是否拖慢页面时,最简单的办法是打开 DevTools 的 Rendering 面板,勾选Paint flashing,滚动页面时观察有没有大面积绿色闪烁。闪烁区域越大,说明重绘范围越大,需要检查是不是某个动效改写了布局属性。再用 Performance 面板录制一次滚动,看长任务是否超过 50ms。做用户体感验证时,把设备模拟切到低端机型档位,固定 CPU 在 4 倍降速再滚一遍,大多数卡顿就能暴露出来。
本文还有配套的精品资源,点击获取