简介这是一份基于HTML、CSS与JavaScript打造的叮咚买菜动态页面设计源码适合前端初学者及电商页面设计练习者用于模拟实战。压缩包内共98个文件含91个PNG素材图片、2个CSS样式文件、1个HTML页面、1个JavaScript脚本、1个GIF动图及1个ICO图标整体约4.54MB目录划分清晰便于按需查看。源码完整呈现商品滑动展示、购物车动态更新、搜索建议弹出等常见电商交互效果HTML搭建页面骨架CSS控制视觉与响应式排版JavaScript负责事件响应与动态渲染三者结合可作为学习前端协作的参考范例。目前已有112人学习浏览适合希望提升动态页面开发能力、快速上手电商类界面设计的读者参考。1. 为什么一套 97 个文件的电商页面源码值得拆一遍这套基于HTML、CSS、JavaScript的叮咚买菜动态页面设计源码打开后第一感受是资源多、代码少97 个文件里 91 张 PNG、1 张 GIF、1 个 ICO真正负责功能的只有一个 index.html、两个 CSS 和一个 JS 文件。页面模拟了生鲜电商首页那套高频交互——搜索建议实时弹出、Banner 自动轮播、购物车数量与总价即时变化都是真实项目里的前端基本功没引入框架和构建工具适合逐行读完。适合准备课设的学生以及写惯了 Vue、React、想补原生 DOM 操作的开发者拆完会发现难的从来不是某个语法而是三个文件之间的协作边界。2. index.html 骨架与 base.css、index.css 的样式分工2.1 语义化标签把首页拆成五个功能区打开 index.html 先看 DOCTYPE 和头部。文件首行是!DOCTYPE html配合html langzh-cn告诉浏览器按标准模式解析并声明页面语言为简体中文这对字体渲染和搜索引擎语义识别都有实际影响。meta charset 指定 UTF-8 编码下面再用两个 link 把 favicon.ico 和两份 CSS 引入。这里有个细节值得注意favicon.ico 即使不写link relicon浏览器也会默认去站点根目录请求但显式声明可以把图标路径固定下来项目拷贝到子目录部署时不会因为路径变化而丢图标。页面主体按语义化标签切成头部、导航、Banner、商品主体和页脚五个功能区。下面这个骨架就是按常见电商首页结构整理的实际源码里的 class 名称可能会略有不同但模块划分思路一致!DOCTYPE html html langzh-cn head meta charsetUTF-8 title叮咚买菜/title link relicon hreffavicon.ico typeimage/x-icon link relstylesheet hrefcss/base.css link relstylesheet hrefcss/index.css /head body header classheader div classlogo img srcimages/下载.png alt叮咚买菜 /div nav classnav ul lia href#新鲜蔬菜/a/li lia href#时令水果/a/li lia href#肉禽蛋品/a/li /ul /nav div classsearch input typetext idsearchInput placeholder搜点什么 button typebutton搜索/button /div /header main section classbanner idbanner div classtrack div classslideimg srcimages/pic_008.png altbanner1/div div classslideimg srcimages/pic_024.png altbanner2/div /div button classbanner-prev‹/button button classbanner-next›/button /section section classgoods-list idgoodsList!-- 商品列表 --/section /main footer classfooter叮咚买菜动态页面练习/footer script srcjs/index.js/script /body /html这段结构里有两个常被忽略的点。第一CSS 全部放在 head 里JS 放在 body 末尾是因为样式表先加载可以减少页面闪烁脚本后执行则避免阻塞 DOM 解析如果 JS 必须放 head那就得等 DOMContentLoaded 事件触发后再操作节点。第二main标签内部用section做功能分区每个section配一个 id给 JS 提供稳定的查询入口。像搜索框命名 searchInputBanner 容器叫 banner商品列表叫 goodsList这种“语义化 功能命名”的方式后续维护时比用一串div套起来容易定位得多。2.2 base.css 负责底子index.css 负责脸面源码里两份 CSS 文件的分工是典型的“公共样式与页面样式分离”思路。base.css 解决不同浏览器默认样式不一致和全局变量统一的问题index.css 只写首页自己用到的布局与组件样式。两者叠加才组成最终渲染效果如果只引入其中一个页面要么样式错乱要么布局全丢。base.css 常见内容/* base.css重置默认样式并定义主题变量 */ * { margin: 0; padding: 0; box-sizing: border-box; } :root { --primary-color: #13b42b; /* 生鲜电商的绿色主色 */ --price-color: #ff5000; /* 价格强调色 */ --text-main: #323233; /* 正文颜色 */ --bg-gray: #f5f6f8; /* 页面底色 */ } ul, li { list-style: none; } a { text-decoration: none; color: inherit; } img { display: block; max-width: 100%; }写在这里的每条规则都有明确目的box-sizing: border-box让 padding 和 border 不再撑大元素宽度这是做响应式布局时最省心的盒模型设置:root里定义 CSS 变量后续在 index.css 里直接用var(--primary-color)取值改主题色时只动这一处img { display: block }消除图片下方的间隙否则单张商品图底下总会多出几个像素。a { color: inherit }则避免每个导航链接都去重写颜色。index.css 常见内容/* index.css页面级布局与组件样式 */ .header { display: flex; align-items: center; justify-content: space-between; height: 56px; padding: 0 16px; background: #fff; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); position: sticky; top: 0; z-index: 100; } .banner { overflow: hidden; } .banner img { width: 100%; height: 300px; object-fit: cover; } .goods-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; padding: 16px; }这段样式里重点看两个属性。position: sticky使头部在页面滚动时固定在视口顶部这比position: fixed更合适——sticky 不脱离文档流父容器范围未结束前自然参与布局不会出现 fixed 遮挡内容的问题object-fit: cover让高度不一的商品图统一裁切成 300px 高的容器且不变形。商品列表用 grid 的repeat(auto-fill, minmax(240px, 1fr))实现响应式换行屏幕越宽一行展示越多商品不需要写媒体查询就能适配常见分辨率。两份 CSS 的分工可以梳理成一张表文件关注点典型内容base.css重置与全局约定盒模型、列表去点、链接去下划线、CSS 变量index.css当前页面布局与组件header 定位、banner 尺寸、商品网格、按钮样式2.3 选择器组织与层级关系写页面样式时最头疼的是类名冲突。这套源码的类名逻辑可以总结为“块-元素”命名header、nav、search 是块goods-list、goods-item、add-btn 是块与元素的组合。父子选择器尽量只隔一层例如.goods-list .goods-item明确限定直接子级而不是在 CSS 里写长串后代选择器。伪类选择器用得最多的是:hover和:nth-child鼠标移入商品卡片时改变阴影和边框这部分直接对应很多人搜的“css 鼠标移入事件”——其实它不是事件而是:hover的样式切换真正的事件绑定在 JS 侧完成。如果发现鼠标移上去没有反馈优先检查的不是 JS而是选择器是否被更高优先级的规则覆盖或者.goods-item是否真的触发了 hover 的父级条件。动手验证时用浏览器开发者工具勾选规则看每一行样式实际作用在哪个元素上。改乱时直接撤销一行规则再刷新页面就能快速体会这两份 CSS 的边界在哪里。3. JavaScript 交互搜索建议、轮播与购物车动态更新3.1 事件监听与查询方式js/index.js 是这套动态页面的“发动机”。围绕电商首页脚本要解决三类问题找元素、绑事件、改内容。原生 DOM 里找元素有两套 APIdocument.getElementById按 id 精确查找document.querySelector则可以接受任意 CSS 选择器。实际项目中我更常用 querySelector因为一个方法能覆盖类名、属性、伪类多种查找但高频循环里 getElementById 会更快常规页面用哪种都没有感知差异。事件触发时机典型场景click鼠标单击元素添加购物车、切换 Banner 箭头mouseenter鼠标移入元素且不冒泡商品卡片浮层、菜单展开input表单值变化搜索建议、价格区间即时筛选scroll滚动位置变化返回顶部按钮显隐注册监听时如果商品列表是动态渲染出来的直接给每个按钮绑 click 事件会在数据刷新后失效常见做法是交给外层容器做事件委托监听.goods-list的 click再通过e.target.closest(.goods-item)判断实际点击的节点。事件委托让脚本只注册一次监听无论商品数量是 10 个还是 100 个代码量都保持不变。3.2 Banner 轮播索引、取模与定时器轮播是这类动态页面最常见的视觉组件。实现思路是把所有轮播图放在一个横向轨道里每次滚动一张图的宽度核心是维护当前索引并通过取模运算防止索引越界let currentIndex 0; const banner document.querySelector(#banner); const track banner.querySelector(.track); const itemWidth banner.clientWidth; function goTo(index) { const maxIndex track.children.length - 1; // 索引小于 0 时跳到末张超过最大下标时回到 0 currentIndex index 0 ? maxIndex : index % track.children.length; track.style.transform translateX( (-currentIndex * itemWidth) px); } // 每 3000ms 自动切换下一张 setInterval(() { goTo(currentIndex 1); }, 3000); document.querySelector(.banner-next).addEventListener(click, function () { goTo(currentIndex 1); });这里的几个参数决定了轮播行为itemWidth必须等于可视区宽度否则轨道位移会错位3000是自动播放间隔单位毫秒改小则切换更快但过快的轮播会干扰用户点击index % track.children.length是取模运算当索引到达最后一张时回到 0实现无限循环。手动点击和自动播放共用 goTo避免两套逻辑各写一份导致索引不同步。建议再补充 mouseenter 暂停定时器、mouseleave 恢复防止鼠标停留在图上时被自动切换打断阅读。3.3 购物车数量与总价计算购物车交互一定包含加号、减号和总价联动。实现时有几个容易出错的细节从 DOM 里取出的数量是字符串直接相加会变成1 1 11价格乘以数量后可能出现浮点误差负数数量要钳制到 1。下面这段是典型的处理方式function changeCount(step, currentCount) { currentCount step; // 数量下限为 1防止用户减出 0 或负数 return currentCount 1 ? 1 : currentCount; } document.querySelector(.goods-list).addEventListener(click, function (e) { const btn e.target; if (!btn.classList.contains(add-btn) !btn.classList.contains(reduce-btn)) { return; } const item btn.closest(.goods-item); const countEl item.querySelector(.count); const next changeCount(btn.classList.contains(add-btn) ? 1 : -1, Number(countEl.textContent)); countEl.textContent next; updateTotal(); }); function updateTotal() { let total 0; document.querySelectorAll(.goods-item).forEach(function (item) { const price Number(item.dataset.price); const count Number(item.querySelector(.count).textContent); total price * count; }); document.querySelector(.total-price).textContent total.toFixed(2); }这段代码最值得学习的是事件委托的写法监听范围是商品列表容器只判断按钮有无 add-btn 或 reduce-btn 类不影响列表内其他元素的点击item.closest(.goods-item)向上找最近的商品卡片使得每行数据天然隔离dataset.price对应 HTML 里>const goodsData [ { name: 有机西红柿, tags: [蔬菜, 爆款] }, { name: 山东红富士, tags: [水果, 新品] }, { name: 土鸡蛋, tags: [禽蛋, 早餐] } ]; document.querySelector(#searchInput).addEventListener(input, function () { const keyword this.value.trim(); const suggestBox document.querySelector(#suggestBox); if (!keyword) { suggestBox.style.display none; return; } const matched goodsData.filter(function (item) { return item.name.includes(keyword); }); suggestBox.innerHTML matched .map(function (item) { return li item.name /li; }) .join(); suggestBox.style.display block; });trim()去掉首尾空格避免用户只敲一个空格也触发请求filter加includes做包含匹配只要商品名中出现关键词就算命中渲染时用字符串拼接生成li最后统一join()塞进 innerHTML数据量在几十条内没有任何问题。要注意 innerHTML 拼接如果来源是用户输入会存在 XSS 注入风险这里的搜索数据来自本地常量数组所以没问题真实项目中建议先做 HTML 转义再渲染。输入为空时用style.display none隐藏建议框比反复删除子元素更轻量。上面四段代码覆盖了动态页面最常见的交互模块。调试时优先打开浏览器控制台脚本运行时报错会直接给出错误行号看到TypeError: Cannot read properties of null八成是 querySelector 的选择器在 HTML 里找不到对应节点先回 index.html 确认 id 和 class 是否一致。提示把用户输入的内容直接拼进 innerHTML 会引入 XSS 风险这里的搜索数据来自本地常量数组所以没问题真实项目中建议先做 HTML 转义再渲染。4. 图片素材组织PNG、ICO、GIF 在页面里的真实作用4.1 images 目录的路径规划与引用这套源码的素材文件全部集中在 images 目录下页面里引用时用的是相对路径。所谓相对路径是以 index.html 所在目录为基准去描述目标文件位置images/pic_001.png表示当前目录下 images 文件夹里的 pic_001.png如果页面文件在子目录路径就要写成../images/pic_001.png。带着 90 多张图片的项目路径写错是加载失败的头号原因而且这类错误不会在控制台报 JavaScript 异常只会显示图片裂图排查时要先在 Network 面板看失败请求的完整路径。文件名大小写也是一个隐蔽的坑。云服务器和 Linux 环境下文件名区分大小写Pic_001.png和pic_001.png是两个文件在 Windows 本地预览时看不出来一旦打包上传到 Linux 部署图片就会突然丢失。拿到这份源码后建议先检查 HTML 里的引用与实际文件名是否完全一致顺手把所有路径列出来核对一遍!-- 相对路径引用示例 -- img srcimages/pic_001.png alt时令蔬菜 img srcimages/pic_095.png alt招牌商品# 列出源码中实际存在的图片文件与 HTML 引用做对比 ls images/ | sort actual.txt grep -o images/[^\]*\.\(png\|gif\) index.html | sort used.txt diff actual.txt used.txt两条命令在 Linux、macOS 和 Windows 的 Git Bash 里都能直接执行。ls images/导出磁盘上真实存在的文件名grep -o只提取 HTML 里引用的图片路径diff对比两个文件的差异多出来的行就是路径写错或引用了不存在图片的地方。这种用命令行做静态资源巡检的思路比逐个睁眼看 90 张图高效得多。注意Windows 本地双击 index.html 虽然也能预览但涉及 fetch 或 ES Module 时会受到 file:// 协议限制。建议用第 5 章的本地静态服务器方式打开。4.2 PNG、ICO、GIF 各自的选型理由91 张 PNG 是绝对主力覆盖商品缩略图、Logo、按钮图标和装饰元素。PNG 采用无损压缩放大缩小不产生锯齿最关键的是支持透明通道电商页面里商品图去掉背景直接叠在白色卡片上视觉效果比 JPG 的白色底块干净。代价是文件体积大一张高分辨率 PNG 经常上几百 KB所以真实项目里商品大图会转成 WebP 或 AVIF。这套源码用 PNG 是为了教学场景下素材通用、浏览器兼容性最好不必为格式兼容做额外处理。favicon.ico 是唯一的浏览器标签页图标尺寸通常只有 16×16 或 32×32却承担着品牌识别功能。浏览器加载页面时会自动请求/favicon.ico如果服务器上不存在这个文件控制台会多一条 404 记录。源码把 favicon.ico 放在项目根目录并用link显式声明就是为了避免这个多余的请求。GIF 是这些图片里唯一支持逐帧动画的格式主要用于促销角标、加载动画这类小而短的效果。GIF 的限制是只能显示 256 色彩色写实图片用它做动画会明显失真但如果只是让一个小元素动起来GIF 比用 CSS 写动画省事得多也不需要额外的 JS。格式压缩方式透明通道动画这套源码里的用途PNG无损支持不支持商品图、Logo、按钮图标ICO容器格式内含多尺寸位图视内部编码而定不支持浏览器标签页图标GIF无损但仅 256 色支持支持逐帧促销动图、加载动画4.3 图片加载失败时的容错与占位即使路径全部正确网络波动或文件被误删也可能导致图片裂图。给 img 标签写 alt 是必须的在图片无法加载时显示替代文字同时提升无障碍访问体验。更进一步可以用 onerror 事件做降级处理让裂图自动替换成占位图img srcimages/pic_028.png alt新鲜蔬菜 onerrorthis.onerrornull; this.srcimages/placeholder.png;onerror属性里先执行this.onerror null这是防递归的关键如果占位图本身也不存在onerror 会再次触发把 onerror 置空后第二次加载失败就不会继续循环请求。this.src直接替换当前节点的图片地址页面结构不会改变商品卡片布局也不会因为换图而抖动。真实项目中这种做法更多会放到一个统一的错误图片处理函数里避免每张图都写一段内联事件。4.4 用懒加载缓解 90 张图的压力图片多到 90 张时首次打开页面如果把全部图片都请求一遍移动端会明显卡顿。原生方案是给 img 加loadinglazy属性浏览器会自己判断图片离视口多远时开始加载零 JS 成本但需要更精细控制时通常配合 IntersectionObserver 实现进入视口再赋值img>const observer new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { const img entry.target; img.src img.dataset.src; observer.unobserve(img); } }); }); document.querySelectorAll(.lazy).forEach(function (img) { observer.observe(img); });这里的思路是先把真实图片地址存在>const goodsList [ { id: 1, name: 有机西红柿 500g, price: 8.9, img: images/pic_028.png }, { id: 2, name: 山东红富士 1kg, price: 12.8, img: images/pic_095.png }, { id: 3, name: 土鸡蛋 30枚, price: 19.9, img: images/pic_045.png } ]; function renderGoods() { const wrap document.querySelector(#goodsList); wrap.innerHTML goodsList.map(function (item) { return div classgoods-item>const defaultConfig { autoplay: true, interval: 3000, loop: true }; const customConfig { interval: 5000 }; // 只覆盖需要变的值 const config Object.assign({}, defaultConfig, customConfig); // 结果{ autoplay: true, interval: 5000, loop: true }Object.assign 第一个参数是空对象后面的配置依次覆盖同名属性customConfig 没写的字段保留默认值。改造轮播代码时把 setInterval 的时间参数换成 config.interval关闭自动播放的判断换成if (config.autoplay)以后调整偏好就只改一处。验证这套改造不需要额外工具。在项目根目录开一个静态服务避免 file:// 协议下部分浏览器对模块脚本和 fetch 的限制# 任选其一 python3 -m http.server 8080 # 或 npx serve -l 8080 .浏览器访问 http://localhost:8080检查商品列表是否按数组顺序渲染、加减号是否正常更新总价、轮播速度是否变成了 5 秒。用 HBuilderX 打开项目后点击运行本质上也是起一个本地静态服务效果相同。控制台切到 Network 面板刷新一次确认图片只有进入视口的被请求说明懒加载配置没有和原有逻辑冲突再把 goodsList 数组里某个 price 改成 99.9页面刷新后价格立即变化而 HTML 文件一行都不用动到这里数据驱动改造才算闭环。本文还有配套的精品资源点击获取