资讯动态

小米商城前端实战:HTML、CSS、JavaScript从零搭建电商首页

发布时间:2026/10/9 9:09:42 来源:尧图企业网站定制
简介一份以小米商城为案例的前端实战项目面向需要系统学习网页开发三大基础技术的初学者以及希望获得电商网站实战经验的中级开发者。项目完整覆盖首页、商品列表、购物车、用户登录等七个核心页面从页面布局搭建、样式细节打磨到动态交互实现逐层拆解并强调还原真实商城界面与操作便捷性贴近实际开发流程。压缩包共六百二十五个文件包含样式表、脚本、网页源码以及大量图片、字体、视频素材可整体对照学习或直接复用包体约六十二点五七兆。目前已有七十一人学习下载适合作为从入门到进阶的前端练手素材。通过该教程读者不仅能掌握创建电商网站的基本技能还能积累一套完整的界面与交互实现方案并可将经验迁移至其他同类购物网站开发项目。1. 小米商城前端实战一套把 HTML、CSS、JavaScript 全串起来的现成教材很多自学前端的人卡在同一个位置语法都认识代码也能读懂但让他独立做出一个完整的页面半天憋不出一个像样的布局。小米商城首页恰好是这个阶段最好的练手对象——它有通栏导航、搜索框、轮播图、秒杀倒计时、商品瀑布流几乎把电商首页该有的交互全占了。这套小米商城前端实战教程讲的就是怎么用纯 HTML、CSS、JavaScript 从零把首页搭出来不依赖任何框架。适合刚学完基础语法想找个完整项目练手的人也适合准备前端面试、需要一个真实项目撑场面的人甚至可以直接拿里面的布局思路和交互代码改一改套到自己的外包项目里。2. HTML 骨架先画分区再写标签首页布局一瞬间就清晰了2.1 页面分区的三层结构header、main、footer 之外还需要什么拿到设计稿先别急着写代码我一般会先在纸上把页面拆成横向区块。小米商城首页从上到下是顶部导航、搜索区、轮播图、秒杀区、商品推荐、底部链接对应到 HTML 就是header、main、footer三大块。body header classsite-header div classtopbar!-- 顶部通栏 --/div div classsearch-bar!-- 搜索框 --/div nav classmain-nav!-- 商品分类导航 --/nav /header main classsite-main section classcarousel!-- 轮播图 --/section section classflash-sale!-- 秒杀倒计时 --/section section classproduct-grid!-- 商品卡片 --/section /main footer classsite-footer !-- 服务保障、友情链接、备案信息 -- /footer /body这里有一个关键点header、main、footer是语义化标签浏览器默认没有特殊样式但屏幕阅读器和搜索引擎爬虫会按语义理解页面结构。main一个页面只能用一次section表示独立的内容区块每个section里最好配一个标题标签h1~h3哪怕样式上把标题隐藏掉结构上也要有。区块划分完再往每一块里填内容就不会乱。先分区后写标签这个习惯能直接避免那种「写着写着不知道这个div该关在哪」的混乱状态。2.2 商品卡片列表的语义化标签ul/li 与 a 的嵌套商品列表是整个首页重复最多的地方。小米商城每个商品卡片包含图片、名称、描述、价格四个要素适合用列表结构来组织而不是一个个孤立的div。ul classproduct-list li classproduct-item a hrefproduct-detail.html?id1001 img src./images/phone.jpg alt小米手机 15 loadinglazy h3小米手机 15/h3 p徕卡光学镜头影像旗舰/p span classprice¥3999/span /a /li li classproduct-item a hrefproduct-detail.html?id1002 img src./images/earphone.jpg alt小米降噪耳机 loadinglazy h3小米降噪耳机/h3 p55dB 深度降噪/p span classprice¥499/span /a /li /ulul和li的语义是「列表」搜索引擎清楚这里是一组同类条目a标签把整张卡片包起来让用户点击卡片任意位置都能跳转。img的alt属性必须写图片加载失败时显示文字也是无障碍访问的基本要求。loadinglazy是浏览器原生懒加载页面滚动到图片位置才去请求首屏加载速度会明显改善。关于price这个类名可以讨论一下是放span还是p。我的习惯是价格这种行内信息用span因为它在视觉上与名称、描述共处一行如果价格单独占一行用p也没问题关键是全站保持一致。2.3 搜索框与表单一个 input 就能决定用户体验搜索框在电商首页是高频交互小米商城的搜索栏结构值得拆开看。它的 HTML 并不复杂但有几个属性直接影响可用性。form classsearch-form action./search.html methodget rolesearch input typetext namekeyword idsearch-input placeholder手机 | 笔记本 | 平板 autocompleteoff maxlength30 button typesubmit aria-label搜索搜索/button /formform的action指向搜索结果页methodget把关键词拼到 URL 查询参数里比如search.html?keyword手机——这是电商站最常见的搜索提交方式。namekeyword决定了参数名后端按这个字段取值。input的autocompleteoff是刻意关掉浏览器历史补全因为电商搜索推荐一般由前端 JS 控制浏览器自带的补全弹层会和下拉建议打架。maxlength30限制关键词长度防止恶意超长输入。button的aria-label搜索是给屏幕阅读器读的按钮里虽然有「搜索」两个汉字但加上aria-label能明确这是「提交搜索」动作而非纯文本。这套结构直接复制进任何站点都能工作不需要额外处理。搜索框这种小模块HTML 写标准了后面接 JS 接后端都很顺。3. CSS 还原从全局样式到涟漪光圈把设计稿变成像素级页面3.1 全局样式初始化reset 与变量后面所有页面都省事浏览器默认样式是前端还原设计稿的第一大障碍body默认有 8px 外边距ul默认有padding-lefth1~h6字号大小不一。这些默认值在不同浏览器里还不完全一样所以第一步永远是做样式初始化。:root { --primary: #ff6700; --text-main: #333; --text-sub: #999; --border-color: #e0e0e0; --bg-gray: #f5f5f5; --header-height: 40px; } * { margin: 0; padding: 0; box-sizing: border-box; } ul, ol { list-style: none; } a { text-decoration: none; color: inherit; } img { display: block; max-width: 100%; }* { box-sizing: border-box; }这条值得单独说默认的content-box下width只算内容宽度padding和border会撑大元素改成border-box后width包含内边距和边框写布局时算尺寸轻松很多不用反复减 padding。:root里的 CSS 变量是全局设计令牌后续所有页面引用var(--primary)就能拿到小米橙#ff6700想换主题色只改这一处。变量名里的--header-height这种尺寸类变量配合后面要做吸顶导航会非常有用。3.2 Flex 和 Grid两种主流布局的选型与参数小米商城首页同时用到两种布局顶部导航和商品信息行用 Flex商品网格用 Grid。这两种方式是现代前端最常用的布局方案选型核心看场景。横向排列元素、元素数量不固定时用 Flex.main-nav { display: flex; justify-content: space-between; align-items: center; height: var(--header-height); } .main-nav .nav-list { display: flex; gap: 24px; } .product-item .info-row { display: flex; justify-content: space-between; align-items: baseline; }justify-content: space-between让子元素两端对齐、中间均分空白align-items: center垂直居中。gap: 24px直接控制子元素间距不需要给每个子元素单独设margin这是 Flex 布局里省事的关键属性。商品网格用 Grid因为网格的行列是规则的。小米商城首页商品区是 4 列布局每行高度由内容撑开.product-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; max-width: 1226px; margin: 0 auto; }repeat(4, 1fr)表示等分成 4 列1fr是「剩余空间的一份」四个1fr就是四等分。max-width: 1226px是小米商城经典的页面内容宽度超过这个宽度就居中保证大屏上内容不会拉得过散。两种布局的边界要分清Flex 适合一维排列一行或一列Grid 适合二维网格多行多列。导航里每个 item 内部也是横向排列但那是局部的一维问题用 Flex 就够了。商品区整体是多行多列用 Grid 最自然。对比项FlexGrid维度一维行或列二维行与列子项对齐主轴交叉轴行轨道列轨道适合场景导航、按钮组、信息行商品网格、图片墙、卡片矩阵间距控制gap 或 margingap 统一控制3.3 涟漪光圈扩散效果CSS 动画与鼠标事件的完整写法热搜里有人专门搜「css涟漪光圈扩散」这效果在小米商城的商品卡片上能看到鼠标移入卡片时从光标位置扩散出一圈柔和的光晕。它不依赖任何库纯 CSS 配合一个mouseenter事件就能做。先写 CSS 部分用::after伪元素承载光圈.product-item { position: relative; overflow: hidden; } .product-item::after { content: ; position: absolute; width: 100px; height: 100px; border-radius: 50%; background: radial-gradient(circle, rgba(255, 255, 255, 0.6) 0%, transparent 70%); opacity: 0; transform: scale(0); pointer-events: none; transition: transform 0.6s ease-out, opacity 0.6s ease-out; } .product-item.ripple-active::after { opacity: 1; transform: scale(3); }.ripple-active这个类名由 JS 动态加上transform: scale(3)让光圈扩大成原来的 3 倍opacity从 0 变 1 再通过 transition 过渡隐藏。pointer-events: none很关键——伪元素不能阻挡鼠标事件否则移入卡片后事件会被它吃掉。JS 负责把光圈定位到光标坐标const items document.querySelectorAll(.product-item); items.forEach((item) { item.addEventListener(mouseenter, function (e) { const rect this.getBoundingClientRect(); const x e.clientX - rect.left; const y e.clientY - rect.top; const circle this.querySelector(::after); this.style.setProperty(--ripple-x, x px); this.style.setProperty(--ripple-y, y px); this.classList.add(ripple-active); }); item.addEventListener(mouseleave, function () { this.classList.remove(ripple-active); }); });getBoundingClientRect()拿到卡片相对视口的位置e.clientX - rect.left算出光标在卡片内部的横坐标存在 CSS 变量里。这里有个细节querySelector(::after)拿不到伪元素 DOM 节点所以光圈的定位要通过自定义属性传进样式。地基 CSS 里伪元素的位置默认是top: 0; left: 0实际要让光圈跟随光标需要把left和top也设成对应的 CSS 变量但核心的扩散逻辑就是「加类名 → 触发动画 → 移出移除类名」。4. JavaScript 交互轮播图、倒计时、吸顶导航三个模块吃透 DOM 操作4.1 轮播图索引、定时器与过渡的三角关系轮播图是电商首页最核心的 JS 模块。小米商城首页轮播自动播放、支持左右切换、底部有小圆点指示器。实现思路不复杂图片横向排成一条长条容器只露出一张的宽度通过改变长条的transform值实现切换。div classcarousel idcarousel div classcarousel-track div classslideimg src./images/banner-1.jpg alt小米15 新品/div div classslideimg src./images/banner-2.jpg alt家电焕新季/div div classslideimg src./images/banner-3.jpg alt雷神新笔记本/div /div button classcarousel-prev aria-label上一张‹/button button classcarousel-next aria-label下一张›/button div classcarousel-dots/div /divconst track document.querySelector(.carousel-track); const slides document.querySelectorAll(.slide); const prevBtn document.querySelector(.carousel-prev); const nextBtn document.querySelector(.carousel-next); const dotsBox document.querySelector(.carousel-dots); let currentIndex 0; const slideWidth 1226; // 每张图片宽度单位 px // 动态生成小圆点 slides.forEach((_, i) { const dot document.createElement(button); dot.classList.add(dot); if (i 0) dot.classList.add(active); dot.addEventListener(click, () goToSlide(i)); dotsBox.appendChild(dot); }); function goToSlide(index) { currentIndex index; track.style.transform translateX(-${currentIndex * slideWidth}px); updateDots(); } function nextSlide() { const lastIndex slides.length - 1; currentIndex currentIndex lastIndex ? 0 : currentIndex 1; goToSlide(currentIndex); } function updateDots() { document.querySelectorAll(.dot).forEach((dot, i) { dot.classList.toggle(active, i currentIndex); }); } nextBtn.addEventListener(click, () { nextSlide(); restartTimer(); }); prevBtn.addEventListener(click, () { currentIndex currentIndex 0 ? slides.length - 1 : currentIndex - 1; goToSlide(currentIndex); restartTimer(); }); let timer setInterval(nextSlide, 3000); function restartTimer() { clearInterval(timer); timer setInterval(nextSlide, 3000); }slideWidth必须和 CSS 里每张图片宽度一致否则位移错位。goToSlide用translateX负值把长条左移-${currentIndex * slideWidth}px这个表达式要理解透索引 0 显示第一张索引 1 左移一张宽度索引 2 左移两张宽度。restartTimer是体验关键——用户点击后如果不重置定时器可能会出现「刚点完下一页1 秒后又被定时器切走」的抢控制权问题。setInterval(nextSlide, 3000)的 3000 是自动播放间隔毫秒数改成 5000 就是 5 秒一切。这里的track.style.transform直接操作行内样式没有过渡动画会显得生硬CSS 里加一行transition: transform 0.5s ease即可。如果要求循环无缝衔接需要把第一张复制一份放到末尾做「假首屏」那就是进阶话题了。4.2 秒杀倒计时setInterval 与时间差的细节小米商城首页有秒杀区块倒计时精确到时分秒。实现倒计时的第一步是厘清一个概念不能每秒钟读一次本地时间再减而是先算好一个毫秒级的时间差然后每秒把差值减 1000。否则本地时间拨动或系统休眠会导致跳秒。function initCountdown(endTime) { const endTimestamp new Date(endTime).getTime(); const countdownEls { hours: document.querySelector(.countdown-hours), minutes: document.querySelector(.countdown-minutes), seconds: document.querySelector(.countdown-seconds), }; function render() { const now Date.now(); let diff Math.floor((endTimestamp - now) / 1000); if (diff 0) { countdownEls.hours.textContent 00; countdownEls.minutes.textContent 00; countdownEls.seconds.textContent 00; clearInterval(timer); return; } const hours Math.floor(diff / 3600); const minutes Math.floor((diff % 3600) / 60); const seconds Math.floor(diff % 60); const pad (num) String(num).padStart(2, 0); countdownEls.hours.textContent pad(hours); countdownEls.minutes.textContent pad(minutes); countdownEls.seconds.textContent pad(seconds); } render(); const timer setInterval(render, 1000); } // 假设今天 24 点结束秒杀 initCountdown(new Date(new Date().setHours(23, 59, 59, 0)).toISOString());diff是剩余秒数Math.floor((endTimestamp - now) / 1000)把毫秒差转成秒差并向下取整避免出现00:00:59.7这种小数。时、分、秒的换算公式要记牢小时 总秒数 / 3600分钟 (总秒数 % 3600) / 60秒 总秒数 % 60。padStart(2, 0)保证个位数显示为09而不是9。有一个容易掉的坑new Date().toISOString()返回的是 UTC 时间如果endTime参数传的是「本地时间 24 点」不做时区处理的话倒计时会差 8 个小时。常见做法是直接传时间戳数字或者后端返回 UTC 时间戳前端统一处理避免本地字符串解析的时区歧义。4.3 吸顶导航与滚动监听scroll 事件的性能注意小米商城首页滚动到一定高度后顶部导航会固定悬浮在视口上方。实现逻辑是监听scroll事件比较scrollY与阈值超过则给header加一个fixed类名。const header document.querySelector(.site-header); const stickyThreshold 200; function handleScroll() { const shouldSticky window.scrollY stickyThreshold; header.classList.toggle(sticky, shouldSticky); } window.addEventListener(scroll, handleScroll, { passive: true });.site-header.sticky { position: fixed; top: 0; left: 0; right: 0; z-index: 100; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); }{ passive: true }是性能关键。scroll 事件触发频率极高默认浏览器要等 JS 执行完才能判断是否调用preventDefaultpassive: true明确告诉浏览器「这个监听器不会取消默认行为」滚动不会被阻塞。这里有个容易被忽略的布局坑header一旦position: fixed它就从文档流里被抽离下面的内容会突然上移一段距离。解决方式是在 CSS 里给body或main预留一个等高的占位或者干脆把吸顶元素固定在页面顶部、用margin-top给内容留出空间——具体方案看设计稿但两者都要处理「脱流后的跳动」问题。4.4 事件委托与数据类型判断面试题背后的实战价值前端面试高频考点「事件委托」和「判断数据类型」在这套教程里都有对应场景。商品列表里可能有几十个卡片如果给每个卡片单独绑定点击事件性能和代码量都不划算。事件委托的做法是把监听器挂到共同的父元素上通过event.target判断实际点中的元素。const productList document.querySelector(.product-list); productList.addEventListener(click, (event) { const item event.target.closest(.product-item); if (item) { const id item.dataset.productId; console.log(点击商品:, id); // 跳转逻辑 } });event.target.closest(.product-item)会从点击的元素开始向上查找匹配product-item的祖先找到才处理找不到返回null。这样不管用户点的是图片、标题还是价格区域都能正确捕获到对应的商品卡片。dataset.productId对应 HTML 里的>function parseEndTime(input) { if (input instanceof Date) { return input.getTime(); } if (typeof input number) { return input; } if (typeof input string) { const time new Date(input); if (!isNaN(time.getTime())) { return time.getTime(); } return Number(input); // 处理纯数字字符串 } throw new Error(无法解析的时间格式); }instanceof Date判断日期对象typeof input number判断时间戳字符串则先尝试解析再尝试转数字。多一层容错倒计时模块就不容易被异常数据击穿。这些知识点面试时是答案但写代码时是真的能用上。5. 避坑指南五个真实翻车记录值得存一份的排查清单5.1 图片加载失败路径与懒加载的边界现象页面打开后商品图片全部裂开控制台 NetWork 面板一片红404 报错。原因图片路径用的绝对路径/images/...本地直接双击 HTML 文件时浏览器把/当成了磁盘根目录根本找不到文件。另一个常见诱因是loadinglazy配了不规范的占位图占位图路径错了也会裂。解决项目目录统一用相对路径./images/...前提是 HTML 文件与 images 文件夹平级。懒加载的loadinglazy只对img生效且不能与srcset混用混用时部分浏览器会直接放弃懒加载。如果用了懒加载位置在首屏内的图片不要加这个属性否则首屏会出现短暂空白。5.2 CSS 样式全部丢失引入顺序与浏览器缓存的坑现象页面结构完整但没有任何样式打开开发者工具 Elements 面板样式表文件是空的。原因一种情况是link relstylesheet标签写在了body里CSS 加载时机错乱另一种情况是多个样式表互相覆盖后引入的覆盖了先引入的而调试时看的是被覆盖的那一行。解决link一律放head里。多个 CSS 文件遵循「基础样式 → 组件样式 → 页面样式」的顺序同名同优先级选择器后面的覆盖前面的所以页面级样式永远放最后。浏览器缓存导致改了 CSS 不生效时强制刷新Ctrl F5或打开开发者工具的 Disable cache 选项。5.3 轮播图跳帧或闪烁定时器叠加现象轮播图在自动播放过程中点了一下下一页按钮紧接着自动播放又切了一次感觉跳了两张。原因setInterval被多次启动。常见写法是在切换函数里直接调setInterval每次点击都创建了一个新定时器旧的没清两个定时器同时跑自然会出现双重切换。解决定时器只允许存在一个。定义唯一的timer变量每次手动切换前先clearInterval(timer)再重新setInterval。更稳的方案是用setTimeout递归实现每次切换完成后才排下一次天然不会叠加。我的习惯是写一个restartTimer()函数点击按钮、拖拽、触屏滑动后都调它保证状态一致。5.4 移动端点击延迟与粘滞click 和 touch 的取舍现象手机上点商品卡片偶尔没反应要按第二次或者点击后图片高亮一下才触发跳转。原因移动端浏览器对click事件有约 300ms 的延迟用来判断用户是单击还是双击缩放。虽然现代浏览器在meta nameviewport设置user-scalableno时会取消延迟但有些浏览器策略不是这样。另一个相关问题是点击高亮-webkit-tap-highlight-color默认是半透明灰色观感很差。解决交互元素统一加touch-action: manipulation这个 CSS 属性明确告诉浏览器「这个元素禁止双击缩放」能消除 300ms 延迟。对于轮播图的左右按钮用touchstart和click双绑也可以但要注意touchstart会先触发不能同时触发两次切换。最省事的方案是样式上处理 事件只绑click因为touch-action: manipulation已经解决了延迟问题。5.5 字体与间距不对版开发者工具检查顺序现象照着真站写字号、颜色、间距都对但页面看起来就是「不太像」。行高明显偏大中文字体和设计稿原生不同。原因多数设计稿用苹方PingFang SC或微软雅黑但系统字体栈没写全渲染时回退到了浏览器默认字体还有行高继承问题——全局设了line-height: 1.6卡片标题也跟着继承了导致文字比设计稿松。解决字体栈写成font-family: PingFang SC, Microsoft YaHei, Arial, sans-serif;并显式声明根元素font-size和line-height。排查间距问题时我会在开发者工具里先选中元素看 Computed 面板确认box-sizing影响的是哪个盒模型再逐项检查padding、margin、line-height。顺序是line-height→padding→margin→border因为前两项对视觉密度影响最大。6. 用开发者工具做像素级校准三个技巧把页面做到和真站一致页面功能写完只是第一步要让人觉得「像那么回事」还得做像素级校准。这一节分享三个我每次复刻站点都必用的开发者工具技巧。第一个技巧是 Element 面板的样式覆盖检查。选中一个元素后Styles 面板里会列出所有命中的 CSS 规则带删除线的是被覆盖的声明。当页面出现「我想改字号但改了没反应」的情况时先看有没有更高优先级的选择器或!important再看是不是同一个属性在前面的规则里被覆盖了。排查顺序永远是「谁覆盖了我」而不是「我写错没有」。第二个技巧是 Computed 面板看盒模型。选中元素切到 Computed 标签页右侧会显示 content、padding、border、margin 四层的实时尺寸。调间距时直接看这里不用反复切换 CSS 文件改数字刷新。比如某个按钮宽度总差 2px大概率是它有默认的border或padding没被重置Computed 面板一眼就能暴露。第三个技巧是模拟移动端视口。开发者工具栏的设备图标点开选 iPhone 或任意自定义尺寸刷新后看布局是否正常。注意这里的模拟不能完全替代真机但能抓出大部分「宽度溢出」「固定定位错位」的问题。配合元素面板上的sensors功能可以模拟触屏测试上面的touch-action是否生效。这套小米商城项目做完我养成了一个习惯每次页面写完强制自己走一遍「Elements → Computed → Console 报错检查 → 模拟视口」的流程最后再跑一次 Lighthouse 看性能分。虽然繁琐但每次都能在交付前捞出至少一个隐藏问题。希望你做完这套教程也能建立自己的检查清单——代码能跑不算完跑得稳、长得像、性能不拖后腿才算真的落地。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑