HTML/CSS/JavaScript实战:还原叮咚买菜动态页面的数据渲染与交互
2026/9/14 3:44:44 网站建设 项目流程

简介:一份以叮咚买菜为应用场景的动态页面设计源码,适合前端初学者练习超文本标记语言、层叠样式表和JavaScript脚本的综合运用。压缩包共98个文件,大小约4.54MB,内含1个主HTML页面、2个层叠样式表文件、1个JavaScript脚本文件、91张PNG商品图与图标、1个GIF动画以及站点收藏图标,素材与代码分离,解压后即可在浏览器中打开主页面查看完整效果。项目重点展示电商首页中商品陈列、图片切换、购物车更新、搜索提示等常见交互逻辑;超文本标记语言负责搭建页面结构,层叠样式表完成色彩、排版与响应式适配,JavaScript脚本处理点击和动态更新,三部分串起从静态布局到动态交互的完整流程。目前已有112人浏览学习,适合想要通过真实综合案例巩固前端基础、了解电商页面模块拆分与素材组织方式的开发者,拿到后可以直接运行、逐段阅读,或替换图片素材继续扩展功能。

1. 用 HTML、CSS、JavaScript 还原叮咚买菜动态页面,先搞清它动态在哪

把“叮咚买菜动态页面”做成一套本地可跑的源码,第一步不是抄界面,而是想清楚它“动态”在哪里。用 HTML、CSS、JavaScript 只画出一个静态首页,和一张长截图没有区别;真正值钱的,是商品数据变化之后页面随之变化的能力。这类源码通常分三层:HTML 负责语义骨架,CSS 负责布局和动效,JavaScript 负责商品渲染、购物车状态和鼠标移入、点击等交互反馈。

刚学完 html网页制作基础、想验证自己能否独立完成一个多模块页面的新手,适合拿它当练手项目;写惯了模板引擎、想补原生 JavaScript 数据渲染能力的从业者,也能在这里看到状态管理、事件委托和渲染函数的组织方式。读这套源码时盯住三件事:数据怎么组织、视图如何跟着数据重绘、事件怎样绑在动态生成的节点上。把这三条理顺,页面上那些抢购倒计时、优惠券锯齿和购物车角标,其实都是同一个套路在不同位置的重复——改数据,然后重渲染。

2. HTML 结构与 CSS 布局:先搭出叮咚买菜页面的源码骨架

2.1 先定信息架构:首页模块划分与 DOM 设计

叮咚买菜首页从上到下大致是四块:顶部定位与搜索栏、中部分类入口和限时抢购区、商品信息流、底部 Tab 导航。做 html 网页制作时,不要一上来就堆 div,先把模块落成headermainfooter三个语义区块。这样整个页面的固定关系和滚动关系一开始就定死,后面加动态渲染不用推倒重来。

<!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>叮咚买菜动态页面</title> <link rel="stylesheet" href="css/style.css"> </head> <body> <header class="app-header"> <div class="location">上海 · <span id="addr">浦东新区</span></div> <div class="search-bar"> <input type="search" placeholder="搜索商品" id="searchInput"> </div> </header> <main class="app-main" id="goodsList"> <!-- 商品卡片由 JavaScript 渲染,这里只留空容器 --> </main> <footer class="app-tabbar"> <a class="tab active">/* css/style.css —— 三行式布局核心 */ html, body { margin: 0; height: 100%; /* 高度链路必须从 html 开始 */ } body { display: flex; flex-direction: column; font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; } .app-header { height: 96px; flex-shrink: 0; /* 防止被压缩 */ padding: 8px 12px; box-sizing: border-box; } .app-main { flex: 1; /* 占满剩余高度 */ overflow-y: auto; /* 只有这一层滚动 */ -webkit-overflow-scrolling: touch; background: #f5f6f7; } .app-tabbar { height: 56px; flex-shrink: 0; display: flex; border-top: 1px solid #e5e5e5; padding-bottom: env(safe-area-inset-bottom); /* iPhone 底部安全区 */ }

这套布局有两个高频坑。第一个是html, body忘了设height: 100%flex: 1的参照高度不确定,结果 main 比视口还高,整个页面在 body 上滚动而不是在 main 里滚,返回顶部、吸底导航全部错位。第二个是box-sizing: border-box遗漏,header 写了height: 96px再加padding: 8px 12px,实际占用变成 112px,视觉上顶部会多出一截。建议全局统一* { box-sizing: border-box; }

2.3 用 CSS 伪类选择器做细节:优惠券圆切、删除线与字体渐变

动态页面“像不像”,往往取决于几个 CSS 细节。叮咚买菜页面里最常见的是优惠券圆切、原价删除线、价格字体渐变和卡片悬浮。以下是推荐实现方式:

视觉效果推荐实现关键代码
优惠券圆切伪元素::before画半圆盖住边缘border-radius: 50%; background: #f5f6f7
原价删除线文本装饰线,最简单可靠text-decoration: line-through; color: #999
价格渐变字背景裁切到文字background-clip: text; color: transparent
卡片悬浮反馈伪类:hover或追加.is-hovertransform: translateY(-2px)

具体代码:

.coupon { position: relative; overflow: hidden; } /* 左侧圆切:用同色半圆“盖”出优惠券缺口 */ .coupon::before { content: ""; position: absolute; left: -6px; top: 50%; width: 12px; height: 12px; transform: translateY(-50%); border-radius: 50%; background: #f5f6f7; } .origin-price { text-decoration: line-through; color: #999; font-size: 12px; } .price-text { background: linear-gradient(90deg, #ff2e2e, #ff7a00); -webkit-background-clip: text; background-clip: text; color: transparent; }

圆切有两种实现,一是这里写的伪元素遮盖,二是用 CSS mask 真正裁掉一个半圆。伪元素方案兼容性最好,代价是遮盖圆点必须和页面背景色一致,背景不是纯色时会穿帮,那时再换 mask。渐变文字的关键是color: transparent配合background-clip: text,少一个都看不到效果,WebKit 内核还要带-webkit-前缀。伪类选择器不只:hover::before::after:disabled在这个页面里都会用到,先把这几个用熟,优惠券这类细节基本不卡人。

3. JavaScript 数据渲染:让商品列表跟着 state 动态更新

3.1 数据先行:商品数组与页面状态的结构设计

动态页面和静态页面的分水岭,是数据有没有从 HTML 里抽出来。常见做法是维护两份结构:一份是goods商品数组,模拟接口返回;一份是state页面状态。渲染时只读 state,绝不直接去改 DOM 上的文字。后续做搜索、分类、购物车,全部收敛成“改数据 → 重渲染”一个流程。

// js/data.js —— 商品数据,模拟后端接口返回值 const goods = [ { id: 1, name: "进口香蕉", tag: "热卖", price: 9.9, originPrice: 15.8, unit: "500g", stock: 36 }, { id: 2, name: "鲜鸡蛋 30枚", tag: "限时", price: 19.9, originPrice: 25.9, unit: "盒", stock: 0 }, { id: 3, name: "宁夏菜心", tag: "时令", price: 5.9, originPrice: 8.8, unit: "300g", stock: 120 } ]; const state = { currentCategory: "all", // 当前分类 cart: {}, // 以商品 id 为键、数量为值 keyword: "" // 搜索关键词 };

这里的结构设计有三个值得留意的点。cart用对象而不是数组,因为按 id 查找和修改都是 O(1),后面算角标直接用Object.values(state.cart)求和就行。stock: 0表示缺货,渲染时按钮要置灰、文案改成“已售罄”,这是真实电商页面必须处理的分支。data.jsmain.js分开,是为了以后能直接把这套模拟数据替换成 fetch 请求的真实接口,页面其他逻辑不用动。顺带说一句,商品如果是动态渲染出来的,直接抓取 HTML 拿不到内容,用 bs4 那类静态解析只能得到一个空壳容器,这也是“动态页面”和静态页面在服务端视角下的本质差别。

3.2 用模板字符串封装 render 函数:一次渲染整屏商品

渲染函数是动态页面的心脏。用 ES6 模板字符串把商品对象拼成 HTML,再用数组的filtermapjoin完成“过滤 → 映射 → 拼接”三个阶段:

// js/main.js —— 渲染商品列表 const listEl = document.getElementById("goodsList"); function renderGoods() { const html = goods .filter(function (item) { return item.name.includes(state.keyword); }) .map(function (item) { const soldOut = item.stock === 0; return ` <div class="goods-card">function setState(patch) { // 合并两个对象:patch 里的字段覆盖旧状态 Object.assign(state, patch); renderGoods(); } // 分类按钮点击(事件委托,监听稳定的父容器) document.getElementById("categoryBar").addEventListener("click", function (e) { const btn = e.target.closest(".cat-btn"); if (!btn) return; setState({ currentCategory: btn.dataset.cat }); });

对应的状态与页面表现关系如下:

state 字段对应 DOM 表现更新时机
currentCategory分类按钮的激活样式点击分类按钮
cart购物车角标、加入按钮文案加入或减少商品
keyword商品列表过滤结果搜索框 input 事件

Object.assign是浅拷贝,当前 state 是扁平结构,直接合并没问题;如果以后 state 里出现嵌套对象,patch 时要手动展开再覆盖,否则整个子对象会被替换掉。常见误用是“改 DOM”和“改 state”两套逻辑各写各的,视图和状态一旦不同步,bug 极难复现。正确顺序永远是:用户操作 → 更新 state → 触发渲染函数。分类按钮的激活样式交给 CSS 判断.active类,JS 只负责切currentCategory,视图差异全部由渲染函数统一输出。

4. JavaScript 交互:鼠标移入事件、购物车联动与运行时排错

4.1 动态生成的节点不能直接绑事件:用事件委托

新手最容易在这个环节卡住:第一次渲染后按钮能点,一旦搜索或切换分类触发重渲染,所有点击都失效。原因在第三章说过,innerHTML重建 DOM,旧监听器跟着旧节点一起销毁。解法是事件委托,把监听器挂在不变的父节点goodsList上,用closest找到实际点击的按钮:

listEl.addEventListener("click", function (e) { const btn = e.target.closest(".btn-cart"); if (!btn || btn.disabled) return; const card = btn.closest(".goods-card"); const id = Number(card.dataset.id); if (state.cart[id]) { // 已在购物车里,再点一次就是减一 state.cart[id]--; if (state.cart[id] === 0) delete state.cart[id]; } else { state.cart[id] = 1; } updateCartBar(); });

e.target.closest('.btn-cart')从触发事件的那个元素向上找,即使点击的是按钮里的文字节点也能命中;找不到返回 null,所以必须判空。Number(card.dataset.id)很关键,data-* 属性读出来永远是字符串,直接拿去做对象键会和数字键产生歧义。这里实现的交互是“没加入过就加一,已加入再点就减一”,数量减到 0 时用delete清掉键,避免购物车里残留数量为 0 的脏数据。

4.2 用 mouseover 实现 CSS 鼠标移入事件与悬浮卡片

桌面端预览时,商品卡片悬浮上浮,是最常见的 css 鼠标移入事件应用。纯样式用:hover就够,但“移入后切换类名、触发后续逻辑”需要 JavaScript 配合。注意这里用的是mouseovermouseout,而不是mouseenter,因为后者不冒泡,挂在父级上收不到子节点触发的事件,委托会直接失效:

listEl.addEventListener("mouseover", function (e) { const card = e.target.closest(".goods-card"); if (card) card.classList.add("is-hover"); }); listEl.addEventListener("mouseout", function (e) { const card = e.target.closest(".goods-card"); if (card) card.classList.remove("is-hover"); });
.goods-card { transition: transform .2s ease, box-shadow .2s ease; } .goods-card.is-hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0, 0, 0, .08); }

mouseover会频繁触发,但这里只做classList切换,性能开销可以忽略;如果悬浮浮层里有异步请求,记得加防抖。CSS 侧的核心是transition配合.is-hover类,让“浮起”是过渡而不是瞬变。translateY(-2px)和阴影同时变化,视觉上才像一整张卡片被抬起来,只改其中一个会显得很生硬。

4.3 购物车角标与数量联动:reduce 统计总量

购物车角标是全局状态,任何一处增减都要反映到角标上。把它收敛成一个函数,所有修改购物车的地方都调用它,避免每个按钮各写一套更新逻辑:

function updateCartBar() { const total = Object.values(state.cart) .reduce(function (sum, n) { return sum + n; }, 0); const badge = document.getElementById("cartBadge"); badge.textContent = total > 0 ? total : ""; // 为 0 时置空 }

Object.values把购物车对象的值抽成数组,reduce求和,初始值 0 保证空对象时不会报错。角标为 0 时赋空字符串而不是 “0”,视觉上更干净。赋值用textContent而不是innerHTML,虽然 total 是数字不存在注入问题,但养成这个习惯能少踩很多安全坑。另外建议每次updateCartBar后顺手把 state.cart 持久化,这就是第五章要做的收尾。

4.4 JavaScript 运行时报错对照表与排查顺序

动态页面的运行时报错多集中在 DOM 未找到、事件绑定失效、数据未就绪三类,对照关系如下:

报错信息常见原因处理方式
Cannot read properties of nullgetElementById的 id 拼错,或 script 提前执行确认 id;script 放 body 末尾;加if (!el) return兜底
xxx is not a functiondata.js未引入,goods未定义检查两个 script 的引入顺序,data 必须在 main 之前
e.target.closest is not a functione.target是文本节点或 SVG 元素先判断e.target.nodeType === 1,或统一用closest判空

排查顺序建议固定下来:打开 DevTools Console 看第一条红色报错,它往往就是把后续问题拦在发射点的根因;在报错行打断点,用控制台检查stategoods是否就绪。不要用 alert 调试,弹窗会打断所有交互流程,而且看不到调用栈。运行时报错排得越熟练,写动态页面的速度越快,这类项目的大部分开发时间其实花在这里,而不是写样式。

5. 用 localStorage 与 HBuilderX 收尾:让 JavaScript 动态页面刷新不丢状态

5.1 购物车持久化:localStorage 只存数据不存 DOM

动态页面做完交互后,第一个“进阶感”的改动是刷新不丢购物车。localStorage 只能存字符串,对象必须序列化,读取时用|| {}兜底,否则首次访问返回 null,Object.values直接报错:

function saveCart() { localStorage.setItem("ddmc_cart", JSON.stringify(state.cart)); } function restoreCart() { state.cart = JSON.parse(localStorage.getItem("ddmc_cart")) || {}; }

要点是只存cart这个纯数据,不要把整个 state 塞进去。分类和搜索词属于临时状态,刷新后重置符合用户预期;购物车是用户主动维护的数据,丢了才会被骂。持久化写入放在updateCartBar末尾统一调用,保持“单点写入”,不要每个点击事件里各存一次。localStorage 上限约 5MB,存购物车绰绰有余,但如果以后要存商品详情,要换成 IndexedDB。

5.2 返回顶部与调试:滚动容器是 main 不是 window

这个项目里滚动的是.app-main,不是 window,所以返回顶部不能用window.scrollTo,写了会毫无反应。正确作用对象是滚动容器本身:

document.getElementById("backTop").addEventListener("click", function () { listEl.scrollTo({ top: 0, behavior: "smooth" }); });

本地调试这类源码,我一般用 HBuilderX 直接打开项目文件夹,右键“运行 → 运行到浏览器”,它内置了本地静态服务,不需要额外配置 html、css、javascript 的运行环境,保存后自动刷新,比手动起服务省一步。最后给一个验证技巧:在 Console 里执行state.cart = {1: 3, 3: 2}; updateCartBar();模拟购物车数据,检查角标是否正确;再执行localStorage.removeItem("ddmc_cart")后刷新页面,确认回到初始状态,这样能区分是真正的运行时报错还是旧数据干扰。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询