ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

纯前端复刻小米首页:栅格布局、吸顶导航与性能优化全解析

2026/9/25 22:02:59 拓冰建站 浏览量
纯前端复刻小米首页:栅格布局、吸顶导航与性能优化全解析 简介这份小米官网首页静态页面源码是前端初学者练习HTML与CSS的整体实战案例。资源完整还原了一个电商门户首页的静态布局包含2个HTML页面和8个CSS样式文件CSS中既有reset、base这类基础样式也有index页面主样式并提供min压缩版与开发版对照便于理解代码组织方式。同时配有多张jpg、png、svg图片和ico图标以及ttf、woff、eot等字体文件共38个文件压缩包大小2.64MB结构清晰。下载后可逐模块拆解导航栏、轮播图、商品列表等区块掌握类选择器、盒模型、Flexbox布局、媒体查询和伪类等核心知识点。目前已有667人学习了该资源适合用于课程设计、个人作品集或前端入门阶段的综合练习。通过仿写主流网站首页能有效巩固样式编写思路也为后续进阶开发打下基础。1. 静态页面不等于简单页面小米首页这五个模块才是真正的考点“静态页面”这四个字经常被当成“简单页面”的同义词但做过一次完整的小米官网首页复刻你就知道它不是简单而是“没有后端也能把交互做到位”的挑战。小米官网首页看起来是一张图接一张图地往下滚实际上拆开看有吸顶导航、大图轮播、图片懒加载、楼层跳转电梯导航、以及不同视口下的栅格布局。这套东西用 Vue 或 React 写不算难难的是只用 HTML、CSS、原生 JavaScript 把整页从零搭出来还不能有明显的性能缺陷。对于正在学前端、准备实习作品集、或者想独立负责一个完整页面的开发者来说这是一个绕不开的练手项目。我拿它当标杆做了两轮第一轮做完没过瘾第二轮专门把性能调优和交互细节补齐了。这篇笔记按我自己的落地顺序写先立骨架、再做交互、再优化图片、最后把翻车记录整理出来。新手能一路跟着写熟手可以直接看避坑一节的值参数设置。2. 先用 HTMLCSS 把页面骨架立起来栅格系统与楼层结构是复刻的底座2.1 为什么选 1226px 宽 12 栅格和我实际复刻时的判断打开小米官网首页看布局作业区是居中的一块内容区。自己复刻第一个要决策的就是内容区宽度。我翻过早年官方页面的样式痕迹常见做法是把容器宽度设在 1226px 左右用 12 栅格划分。1226px 不是随手定的它在大屏上留出两侧呼吸空间在 1366px 的笔记本屏幕上不会横向溢出太多在 1920px 屏幕上也有足够的左右留白。栅格划分为 12 列而不是更细的 24 列原因是首页的楼层模块绝大多数是三等分、四等分、八等分这种整数比例。12 可以被 3、4、6、12 整除做三列商品卡片时每列占 4 格做四列图文时每列占 3 格不需要额外的偏移类。24 栅格虽然更灵活但页面重构时心智负担更大投资收益太低。我最终的栅格方案是这样设计的.container控制最大宽度为 1226px水平居中.row去掉栅格间距带来的左右偏移.col-4、.col-6、.col-8分别对应三分、二分、三分之二宽度的模块。这里不采用浮动布局而用 CSS Grid因为 Flex 处理“一行多个卡片等宽换行”需要对每个子项设置flex: 1而 Grid 直接用grid-column控制跨度语义更清楚。2.2 页面骨架代码头部、导航、主轮播、楼层的基本结构先写 HTML 骨架。这个结构按照小米官网首页常见的楼层顺序来组织顶栏用户区、吸顶导航、主轮播、下方若干楼层比如竖排广告、产品卡片、生态链产品最后是底部说明区。div classcontainer !-- 吸顶导航 -- header idsite-header div classheader-inner h1 classlogoa href/小米商城/a/h1 nav classmain-nav ul lia href#phone手机/a/li lia href#tv电视/a/li lia href#appliance家电/a/li lia href#smart智能设备/a/li /ul /nav /div /header !-- 主轮播区 -- section classhero-slider idhero-slider div classslider-track idslider-track div classslide slide-active img srcimages/banner-phone.webp alt手机新品 /div div classslide img srcimages/banner-appliance.webp alt家电专场 /div div classslide img srcimages/banner-tv.webp alt电视新品 /div /div div classslider-dots idslider-dots/div /section !-- 楼层手机 -- section classfloor idphone h2 classfloor-title手机/h2 div classrow div classcol-4 floor-card卡片内容/div div classcol-4 floor-card卡片内容/div div classcol-4 floor-card卡片内容/div /div /section !-- 楼层电视 -- section classfloor idtv !-- 类似结构 -- /section !-- 底部 -- footer classsite-footer p页面说明与版权占位/p /footer /div这个骨架的核心意图是把“楼层”这个概念先立住。楼层的意思是每一个横向区块都是一个独立section有自己的id锚点。后面做电梯导航时JavaScript 只需要遍历这些section的offsetTop就能算出当前楼层。.floor-title的存在也很关键它是后面滚动监听判断“第几屏”的参照物。这里我故意没有在楼层层级里放真实图片地址而是用占位文本卡片内容。原因是第一版骨架的重点是结构不是内容图片素材越早堆进去越容易在调试布局时被大图加载拖慢刷新速度。我一般会先让所有卡片等高、同色把页面整体形状确认之后再逐层填图片。2.3 栅格与楼层间距的落地写法边距、圆角、阴影参数接下来是 CSS 栅格系统。直接把.container、.row、.col-*做成通用类页面里所有楼层复用减少重复代码。:root { --container-width: 1226px; --gutter-width: 14px; --floor-margin: 60px; --primary-color: #ff6700; --card-shadow: 0 2px 6px rgba(0, 0, 0, 0.08); } .container { max-width: var(--container-width); margin: 0 auto; padding-left: var(--gutter-width); padding-right: var(--gutter-width); } .row { display: grid; grid-template-columns: repeat(12, 1fr); gap: var(--gutter-width); } .col-4 { grid-column: span 4; } .col-6 { grid-column: span 6; } .col-8 { grid-column: span 8; } .floor { margin-top: var(--floor-margin); } .floor-card { background: #fff; border-radius: 8px; box-shadow: var(--card-shadow); overflow: hidden; }参数说明--gutter-width我用的是 14px这是从米家卡片间距的习惯值中选取的。14px 比 16px 紧凑在 1226px 宽的内容区里三张卡片排下来不会显得散。卡片阴影用了两层值第一层0 2px 6px负责常规深度如果做悬停上浮效果再叠加一层更大模糊的阴影。圆角 8px 是当前电商卡片设计的常用值6px 偏尖锐12px 偏圆润8px 最接近官方观感。在真机复刻时背景色比卡片色更重要。小米首页的楼层层与层之间是纯白间隔页面整体背景用#f5f5f5做底楼层的卡片背景用白色这样卡片会有自然的浮出感。如果背景色和卡片都是白色页面会变成一大块平板阴影效果也会被吞掉。这轮布局做完页面静止状态下应该已经逼近真站 80% 的观感了。3. 给静态首页装上两个核心交互吸顶导航与轮播图的原生 JS 实现3.1 吸顶导航的三种做法为什么我先用 scroll 监听而不是 sticky吸顶导航是首页的“第一个交互点”。页面滚到导航下方后导航栏固定在视口顶部。实现思路有三个一是 CSSposition: sticky二是 Fixed 定位加 JavaScript 监听三是在导航原本位置放一个占位盒子再把导航 Fixed 出去。position: sticky是最省事的单行方案但它有个坑sticky 生效的前提是父容器的高度要超出视口且父元素不能是overflow: hidden。首页骨架用.container包住所有楼层只要 container 高度足够sticky 就能工作。真正让我放弃 sticky 的原因是导航高亮楼层的交互必须知道当前滚动位置这个事件监听反正都要写干脆用scroll监听直接切fixed定位逻辑更透明。const header document.getElementById(site-header); const headerHeight header.offsetHeight; let isTicking false; function updateHeader() { if (window.scrollY headerHeight) { header.classList.add(header-fixed); } else { header.classList.remove(header-fixed); } isTicking false; } window.addEventListener(scroll, () { if (!isTicking) { window.requestAnimationFrame(updateHeader); isTicking true; } });逻辑说明header.offsetHeight在页面加载完成后读取一次作为滚动阈值的基准。requestAnimationFrame把 scroll 事件高频触发合并成帧级更新避免每次滚动像素都直接操作 DOM 添加和移除 class。这里有一个参数要专门提不要用scrollY 0作为触发条件因为导航本身距页面顶部有间距滚动到 0 的边界条件在慢速滚动时会导致导航反复跳动。拿offsetHeight做阈值滚动一过导航区域底边就吸顶回弹时也等到导航完全归位才移除 Fixed。3.2 轮播图最小可运行实现translateX 滑动、自动播放与循环边界轮播图是首页权重最高的视觉区。用transform: translateX而不是改margin-left是因为 transform 能触发 GPU 合成滑动时不引起整页重排。轮播的最小实现不需要引入任何库核心就三件事切换索引、更新 transform、处理循环边界。const track document.getElementById(slider-track); const slides track.children; const slideCount slides.length; const dots document.getElementById(slider-dots); let currentSlide 0; let timer null; // 动态生成圆点 for (let i 0; i slideCount; i) { const dot document.createElement(button); dot.setAttribute(aria-label, 切换到第 (i 1) 张); if (i 0) dot.classList.add(dot-active); dot.addEventListener(click, () goToSlide(i)); dots.appendChild(dot); } function goToSlide(index) { currentSlide (index slideCount) % slideCount; track.style.transform translateX( (-currentSlide * 100) %); [...dots.children].forEach((dot, i) { dot.classList.toggle(dot-active, i currentSlide); }); } function nextSlide() { goToSlide(currentSlide 1); } function startAutoPlay() { timer setInterval(nextSlide, 3000); } function stopAutoPlay() { clearInterval(timer); } track.addEventListener(mouseenter, stopAutoPlay); track.addEventListener(mouseleave, startAutoPlay); startAutoPlay();参数说明translateX(-currentSlide * 100)里的百分比是相对于当前元素自身宽度的每张轮播图宽度都设置为轮播容器的 100%所以滚动一张正好是移动一个 100%。currentSlide (index slideCount) % slideCount这一句处理了两件事把点击得到的下标限制在合法范围内以及到最后一张继续点击时跳回第 0 张实现无限循环手感。自动播放间隔 3000ms 是电商首页常用的节奏2500ms 会让人觉得焦躁4000ms 又显得拖沓。鼠标移入时停、移出后开是轮播交互的基本礼仪不然用户正在看商品文案时图被切走体验非常劝退。首张图片上有一个slide-active类这个类的主要作用是给第一屏图片设置display: block其余display: none。等到 JavaScript 脚本加载之后统一把图片改为透明叠加显示避免脚本还没执行时三张图竖向罗列破坏页面。3.3 两个交互之间的联动导航高亮与楼层滚动监听导航和轮播交互各自完成后会暴露出一个体验问题滚动页面时用户不知道自己处在哪个楼层。常见做法是把当前楼层的导航项做高亮处理与电梯导航形成呼应。const floors document.querySelectorAll(.floor); const navLinks document.querySelectorAll(.main-nav a); const scrollOffset 150; function updateActiveFloor() { const scrollPosition window.scrollY; let currentFloorId floors[0].id; floors.forEach(floor { if (scrollPosition floor.offsetTop - scrollOffset) { currentFloorId floor.id; } }); navLinks.forEach(link { if (link.getAttribute(href) # currentFloorId) { link.classList.add(nav-active); } else { link.classList.remove(nav-active); } }); } window.addEventListener(scroll, updateActiveFloor);逻辑说明scrollOffset的值是 150px意思是“当楼层顶部进入视口上方 150px 时就算进入这个楼层”。这个偏移量不是拍脑袋吸顶导航高度约 80px再加上 70px 的视觉缓冲正好。如果偏移量设成 0会出现楼层标题刚刚露出顶部一行时导航就高亮楼层标题还在视口外时用户没有任何提示。floors.forEach遍历所有楼层最后满足条件的一定是当前最靠下的那个楼层所以不需要 break直接覆盖currentFloorId即可。4. 图片延迟加载与楼层电梯导航静态页面性能优化的两个关键动作4.1 为什么纯静态页面也需要懒加载首屏字节数与真实加载体验纯静态页面的最大误解是“没有接口图片都是本地文件不用优化”。实际上小米官网首页的信息密度很高一整屏的产品卡片加起来有几十张图。如果全部用img src直接加载首屏加载会同时发起大量图片请求在弱网环境下整个页面白屏时间会非常长。静态页面最大的性能瓶颈就是图片字节数而不是 JavaScript 执行时间。懒加载的核心思路是真正的src先不填放一个>const lazyImages document.querySelectorAll(img[data-src]); const imageObserver new IntersectionObserver((entries, observer) { entries.forEach(entry { if (entry.isIntersecting) { const img entry.target; img.src img.dataset.src; img.classList.add(img-loaded); observer.unobserve(img); } }); }, { rootMargin: 100px 0px, threshold: 0.05 }); lazyImages.forEach(img imageObserver.observe(img));参数说明rootMargin: 100px 0px的意思是图片进入视口下方 100px 的提前量时就开始加载。这个 100px 是从用户视角的“预加载缓冲区”里抽出来的经验值。太小了会导致图片滚到眼前才开始加载出现明显的占位空白太大了会让视口下方的图片提前下载失去懒加载的意义。threshold: 0.05表示图片露出 5% 面积就算进入视口配 0 也可以但 0 会在图片边缘刚出现时就触发0.05 更稳定。observer.unobserve(img)很重要图片一旦加载完就移除监听避免每次滚动都重复触发同样一批图片。加载状态需要一个兜底样式不然图片启动加载时会破版。img[data-src] { background: #f0f0f0; min-height: 100px; } img.img-loaded { background: transparent; animation: fade-in 0.3s ease; }4.3 楼层电梯导航的实现滚动监听 锚点定位电梯导航是首页侧边的一列小圆点或小图标点击后页面平滑跳到对应楼层。它的技术本质是锚点定位加滚动计算但容易做糙的点有两个点击后滚动位置被吸顶导航挡住以及高亮楼层和实际滚动位置对不上。const elevatorDots document.querySelectorAll(.elevator-dot); function handleElevatorClick(dot) { const targetId dot.getAttribute(data-target); const targetSection document.getElementById(targetId); const navHeight document.querySelector(#site-header).offsetHeight; const targetPosition targetSection.offsetTop - navHeight; window.scrollTo({ top: targetPosition, behavior: smooth }); } elevatorDots.forEach(dot { dot.addEventListener(click, () handleElevatorClick(dot)); });逻辑说明targetPosition targetSection.offsetTop - navHeight是这段代码的核心减掉导航高度是为了让楼层标题出现在导航下方而不是被导航遮住。如果不做这一步点完电梯按钮后标题会刚好被导航盖住用户以为楼层不存在。电梯导航的高亮逻辑直接复用上一节updateActiveFloor的思路只是把navLinks的监听换成elevatorDots的监听。这里要注意电梯导航的点击事件不能用默认锚点跳转因为href#phone的默认行为是瞬间跳转且不带偏移必须用scrollTo手动控制位置。电梯导航在页面宽度小于某个阈值时会被隐藏否则会遮挡楼层内容。这个阈值一般和导航栏断点保持一致在 768px 以下用普通滚动不做电梯导航。5. 静态页面避坑五个让新手反复翻车的细节与解决记录5.1 现象吸顶导航滚动后反复闪跳把导航改成 fixed 后滚动到阈值附近时导航上下抖动像在呼吸一样。原因是我把header.offsetHeight拿来做阈值但导航被 fixed 之后原来的导航位置被移除文档流上方的内容瞬间顶到顶部offsetHeight这个动态值随之改变于是阈值不断在变导航被反复添加、移除 class。解决进入 fixed 状态之前把header.offsetHeight缓存到普通变量中后续滚动判断只认这个固定值。同时给 header 的 fixed 状态加top: 0; z-index: 999避免被轮播图的transform属性创建的新层叠上下文盖住。5.2 现象轮播图在窄屏下错位浏览器窗口从宽屏缩到 375px 手机宽度时轮播图内容被挤到左侧右侧露出灰底。原因是轮播图内部的.slide设置了width: 100%但这个百分比是相对于父容器视口宽度而轮播轨道.slider-track的宽度有时被内容撑开导致比例对不上。解决给.hero-slider显式设置width: 100%; overflow: hidden;再给.slider-track设置display: flex; flex-wrap: nowrap;每张.slide { flex: 0 0 100%; }。这样轨道永远按容器宽度排布不会因为内容高度撑开宽度。5.3 现象懒加载图片闪烁图片从占位灰底变成真实图片时页面出现白闪尤其是图片尺寸大时加载成功后瞬间插入导致布局跳动。原因是懒加载替换src之前图片高度没有被正确预留原本的占位灰底高度和真实图片高度不一致。解决每张产品图给外层容器写固定宽高比或者直接在img上写aspect-ratio: 1 / 1方形产品图。现代浏览器对 aspect-ratio 支持很好图片未加载时容器已占好高度加载后不会再产生累积布局偏移CLS。5.4 现象点击电梯导航楼层跳转后标题被导航挡住这个问题在 5.3 里写过一次但从“排查”角度看值得单列电梯导航scrollTo用getBoundingClientRect计算位置时没有考虑导航是 fixed 状态会覆盖内容区。很多新手直接element.scrollIntoView()就发现标题正好被导航盖住。解决统一用element.offsetTop - headerHeight计算目标位置。offsetTop是相对最近的定位祖先计算如果楼层的position被写成relative且向上偏移计算结果会不准所以楼层不要手动设置top偏移让文档流自然排列。5.5 现象移动端轮播图出现半截高度手机浏览器上轮播图只显示了上半部分下半部分被截断。原因是传统 UI 给轮播图设置height: 100vh但移动端地址栏占用了视口高度的一部分100vh 在视觉上其实超出了可视区。解决轮播高度用min-height: calc(100vh - 80px)把地址栏高度的影响削掉一部分或者直接用固定像素高度比如 540px再用 CSS 媒体查询对不同屏幕做微调。别在移动端过度依赖 vh 单位它不包含地址栏的动态伸缩。6. 从能跑到跑好把静态页面性能调进及格线的验证方法与习惯页面功能做完之后下一步是验证性能。打开 Chrome DevTools 的 Network 面板勾住 “Disable cache”刷新页面重点看三点第一首屏加载发起了多少个图片请求第二页面加载完成的累计字节数第三底部楼层图片是否被延迟加载。如果首屏请求里有底部楼层的图片说明懒加载失效优先排查style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />