FEATURED · 精选文章

HTML/CSS/JavaScript实战:还原叮咚买菜动态页面的数据渲染与交互

发布时间 / 2026/9/14 3:44:45
来源 / 创域科博编辑部
栏目 / 资讯中心
HTML/CSS/JavaScript实战:还原叮咚买菜动态页面的数据渲染与交互 简介一份以叮咚买菜为应用场景的动态页面设计源码适合前端初学者练习超文本标记语言、层叠样式表和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先把模块落成header、main、footer三个语义区块。这样整个页面的固定关系和滚动关系一开始就定死后面加动态渲染不用推倒重来。!doctype html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title叮咚买菜动态页面/title link relstylesheet hrefcss/style.css /head body header classapp-header div classlocation上海 · span idaddr浦东新区/span/div div classsearch-bar input typesearch placeholder搜索商品 idsearchInput /div /header main classapp-main idgoodsList !-- 商品卡片由 JavaScript 渲染这里只留空容器 -- /main footer classapp-tabbar a classtab 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-hover类transform: 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.js和main.js分开是为了以后能直接把这套模拟数据替换成 fetch 请求的真实接口页面其他逻辑不用动。顺带说一句商品如果是动态渲染出来的直接抓取 HTML 拿不到内容用 bs4 那类静态解析只能得到一个空壳容器这也是“动态页面”和静态页面在服务端视角下的本质差别。3.2 用模板字符串封装 render 函数一次渲染整屏商品渲染函数是动态页面的心脏。用 ES6 模板字符串把商品对象拼成 HTML再用数组的filter、map、join完成“过滤 → 映射 → 拼接”三个阶段// 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 classgoods-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 配合。注意这里用的是mouseover和mouseout而不是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 提前执行确认 idscript 放 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 看第一条红色报错它往往就是把后续问题拦在发射点的根因在报错行打断点用控制台检查state和goods是否就绪。不要用 alert 调试弹窗会打断所有交互流程而且看不到调用栈。运行时报错排得越熟练写动态页面的速度越快这类项目的大部分开发时间其实花在这里而不是写样式。5. 用 localStorage 与 HBuilderX 收尾让 JavaScript 动态页面刷新不丢状态5.1 购物车持久化localStorage 只存数据不存 DOM动态页面做完交互后第一个“进阶感”的改动是刷新不丢购物车。localStorage 只能存字符串对象必须序列化读取时用|| {}兜底否则首次访问返回 nullObject.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)后刷新页面确认回到初始状态这样能区分是真正的运行时报错还是旧数据干扰。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻