
简介一套基于HTML实现的装饰品电商WebApp源码面向前端学习者、移动电商开发新手以及需要快速搭建装饰品商城原型的产品与设计人员。项目围绕线上购物场景完整覆盖商品列表、分类搜索、商品详情、购物车管理、订单确认、用户注册登录与个人中心等常用模块可作为课程设计、毕业设计或轻量级WAP商城模板的参考起点。压缩包共56个文件大小约528KB页面结构以29个HTML文件为主体配合CSS样式表控制响应式布局、JavaScript脚本处理交互逻辑、PHP文件实现简单服务端响应并附带图片、字体等静态资源目录划分清晰便于按模块拆解学习。代码中涉及HTML语义化标签、CSS媒体查询、JavaScript数据校验与Ajax异步通信、购物车和订单状态管理、第三方支付接入思路等关键知识点能够帮助读者理解一个移动电商WebApp从前端展示到后端协作的完整链路。目前已有380人学习浏览对入门Web前端或电商项目开发具有直接参考价值。1. 装饰品电商 WabApp 是什么纯 HTML 这条路还走得通吗“基于 HTML 实现的装饰品电商 WabApp 源码”标题里的 WabApp 通常是 WebApp 的笔误但想做的事很具体不用 Vue、不用 React、不接后端只靠 HTML CSS JavaScript 三个静态层把装饰品店的商品展示、分类筛选、购物车和模拟结算全部跑通。双击 index.html 就能演示课程设计能答辩小工作室能当产品目录页用。也正因为没有构建步骤和依赖这份源码适合两类人一类是刚学完 htmlcssjs 基础语法、想看看完整项目结构的初学者另一类是只需要静态展示、不想每月为服务器付钱的个人卖家。至于“纯 HTML 能不能做电商”答案是有边界的能——商品不超过几十个、不做真实支付和账号体系时静态页比框架方案加载更快、部署更省心。2. 纯 HTML 电商 WabApp 的目录结构与选型边界2.1 先搭目录商品数据、渲染、购物车三层分家常见的做法是把页面职责拆成三个文件index.html 管商品列表cart.html 管购物车明细结算用弹窗模拟。对应的源码目录我会这样组织decor-shop/ ├── index.html # 首页商品网格 分类筛选 搜索 ├── cart.html # 购物车页数量修改与合计 ├── css/ │ ├── base.css # 全局变量、reset、按钮与通用卡片样式 │ └── product.css # 商品网格与筛选栏布局 ├── js/ │ ├── data.js # 商品数据纯 JS 数组 │ ├── render.js # 商品列表渲染、分类筛选、搜索 │ └── cart.js # 购物车读写与跨页面同步 └── assets/ └── img/ # 商品图统一用 webp 或压缩 jpg把 data.js 单独抽出来是这个源码最值得保留的习惯。商品调价、下架、换图时只改数据文件render.js 和 cart.js 完全不用动这比把商品写死在 HTML 里好维护一个量级。index.html 的头部则是另一个容易翻车的地方很多人做 html 网页制作时直接复制残缺模板结果中文乱码、手机打开字小到看不清。!DOCTYPE html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 meta namedescription content装饰品电商 WabApp 示例商品展示与购物车源码 title拾光装饰 · 装饰品电商/title link relstylesheet hrefcss/base.css /head body header classtop-bar h1拾光装饰/h1 a hrefcart.html classcart-link购物车(span idcartBadge0/span)/a /header nav classfilter-bar idfilterBar button>// data.js —— 所有装饰品统一放这里后续加商品只改数组 const products [ { id: 1, name: 粗陶小花瓶, category: ceramic, price: 89, img: assets/img/vase.webp, tag: 新品 }, { id: 2, name: 胡桃木摆件, category: wood, price: 129, img: assets/img/wood.webp, tag: }, { id: 3, name: 黄铜烛台, category: metal, price: 159, img: assets/img/candle.webp, tag: 热卖 }, { id: 4, name: 棉麻桌旗, category: fabric, price: 69, img: assets/img/fabric.webp, tag: } ];几个字段的约定category 的值必须和 index.html 里>// render.js —— 根据传入的商品列表渲染网格 function renderProducts(list) { const grid document.getElementById(productGrid); if (!list.length) { grid.innerHTML p classempty-tip该分类下暂时没有商品/p; return; } grid.innerHTML list.map(p { const tagHtml p.tag ? span classtag${p.tag}/span : ; return article classcard img src${p.img} alt${p.name} loadinglazy h3${p.name}/h3 p classprice¥${p.price}/p ${tagHtml} button classadd-btn>// render.js —— 事件委托父容器统一处理点击 document.getElementById(filterBar).addEventListener(click, e { const btn e.target.closest(button[data-filter]); if (!btn) return; const filter btn.dataset.filter; document.querySelectorAll(#filterBar button) .forEach(b b.classList.toggle(active, b btn)); renderProducts(filter all ? products : products.filter(p p.category filter)); });e.target.closest(button[data-filter]) 负责从点击的元素向上找最近的筛选按钮即使用户点到了按钮里的文字或子元素也能命中。classList.toggle(active, b btn) 的第二个参数是强制布尔值为 true 时加类、false 时移除比先 remove 再 add 少两行代码。事件委托相比逐个绑定优势集中在两点动态渲染出来的按钮不需要重新绑事件整个筛选栏永远只有一个监听器。写法动态新增按钮监听器数量适合场景逐个 addEventListener每次渲染后要重新绑每个按钮一个固定且数量少事件委托无需任何处理父容器一个动态渲染列表同样的委托思路可以直接扩展搜索。在 filter-bar 后面放一个input idsearchInput classsearch-box placeholder搜索装饰品名称然后把筛选逻辑收敛成一个带状态的函数// render.js —— 分类与关键字组合筛选 const state { filter: all, keyword: }; function applyFilter() { const list products.filter(p { const okCategory state.filter all || p.category state.filter; const okKeyword !state.keyword || p.name.includes(state.keyword); return okCategory okKeyword; }); renderProducts(list); } document.getElementById(searchInput).addEventListener(input, e { state.keyword e.target.value.trim(); applyFilter(); });把筛选条件收进 state 对象是为了让分类和搜索能叠加生效而不是互相覆盖。用 input 事件而不是 change 事件的差别在于input 在每敲一个字符时触发搜索是即时反馈的change 要到输入框失焦才触发体验明显差一截。搜索匹配用 includes 是子串匹配够用如果想支持“陶瓷 花瓶”这种空格分词再用 keyword.trim().split(/\s/) 拆开做 every 判断。4. HTML 前端的购物车状态管理localStorage 读写与渲染同步4.1 购物车只存 id 和数量不存价格快照购物车的数据结构直接决定后续代码的复杂度。我建议存成数组每项只有 id 和 quantity 两个字段价格、名称、图片一律不冗余进 localStorage// cart.js —— 购物车读写封装 const CART_KEY decor_cart_v1; function getCart() { try { return JSON.parse(localStorage.getItem(CART_KEY)) || []; } catch (err) { return []; // JSON 损坏时兜底避免整页崩溃 } } function saveCart(cart) { localStorage.setItem(CART_KEY, JSON.stringify(cart)); } function addToCart(productId) { const cart getCart(); const item cart.find(i i.id productId); if (item) { item.quantity 1; } else { cart.push({ id: productId, quantity: 1 }); } saveCart(cart); updateBadge(cart); }这么设计有一个实际好处商品改价后只要 data.js 更新了购物车小计会跟着新价格走不用管旧数据。如果图省事把 name 和 price 一起存进去下次调价会出现“购物车里还是旧价”的脏数据。CART_KEY 里带 v1 版本号也是故意的将来字段结构变更时可以按版本做迁移而不是直接放弃用户已有数据。getCart 的 try/catch 不是多余的localStorage 里的数据可能被用户手改、被其他页面污染JSON.parse 一旦抛异常没有兜底的话整个页面会在加载时就白屏。localStorage 的整套 API 很简单真正容易踩的是类型和容量API用途容易踩的坑setItem(key, value)写入value 必须是字符串对象要先 JSON.stringifygetItem(key)读取key 不存在返回 null不是 undefinedremoveItem(key)删除清空购物车时用它storage 事件跨标签页同步只在另一个标签页写入后触发至于容量localStorage 一般是 5MB 左右存购物车绰绰有余真正会触发 QuotaExceededError 的是往里面塞 Base64 图片这种事在课程设计里经常见到图片请一律走 assets 目录。4.2 购物车页渲染数量修改、小计重算与跨页同步cart.html 的表格在 HTML 里只留一个tbody idcartBody渲染逻辑和商品列表是同一套路根据 getCart() 的结果反查 products拼模板字符串// cart.js —— 购物车页专用渲染 function renderCart() { const tbody document.getElementById(cartBody); const cart getCart(); if (!cart.length) { tbody.innerHTML trtd colspan5购物车还是空的/td/tr; return; } tbody.innerHTML cart.map(item { const p products.find(x x.id item.id); if (!p) return ; // 商品已下架直接跳过 return tr td${p.name}/td td¥${p.price}/td tdinput typenumber min1 value${item.quantity} classqty-input>// cart.js —— 数量修改与移除 document.getElementById(cartBody).addEventListener(input, e { if (!e.target.classList.contains(qty-input)) return; const id Number(e.target.dataset.id); let qty parseInt(e.target.value, 10); if (Number.isNaN(qty) || qty 1) qty 1; // 输入 0、负数或空值一律回退为 1 const cart getCart(); const item cart.find(i i.id id); if (item) item.quantity qty; saveCart(cart); renderCart(); // 重算本行小计和底部合计 }); document.getElementById(cartBody).addEventListener(click, e { const btn e.target.closest(button.remove-btn); if (!btn) return; saveCart(getCart().filter(i i.id ! Number(btn.dataset.id))); renderCart(); });parseInt 之后必须做 NaN 和下限校验因为 number input 允许用户清空输入框item.quantity NaN 会让总价显示成 NaN这是购物车页面最常见的“看起来像 bug”的现象。两个事件分别在 input 和 click 上触发互不干扰remove-btn 用 closest 是为了防误点 span 或子节点。跨页面同步是纯 HTML 多页面结构的痛点好在浏览器提供了 storage 事件当 index.html 和 cart.html 两个标签页同时打开任意一边修改购物车另一边能立即收到通知// cart.js —— 跨标签页同步 window.addEventListener(storage, e { if (e.key CART_KEY) { renderCart(); // 购物车页自动刷新 updateBadge(getCart()); // 首页角标同步 } });storage 事件只在“另一个标签页”写入时触发当前页面不会收到自己写出的变更所以修改后要主动调一次 renderCart二者并不冲突。最后是结算入口用一个 alert 模拟即可真实支付必须走后端下单接口// cart.js —— 模拟结算 document.getElementById(checkoutBtn).addEventListener(click, () { const cart getCart(); if (!cart.length) return alert(请先选购商品); const total cart.reduce((sum, item) { const p products.find(x x.id item.id); return sum (p ? p.price * item.quantity : 0); }, 0); localStorage.removeItem(CART_KEY); alert(订单总价 ¥${total}支付功能需接入服务端); renderCart(); });reduce 里同样做了商品下架判断p ? … : 0保证脏数据不会把总价算成 NaN。到这里一个纯前端电商的完整闭环——浏览、筛选、加购、改数量、模拟结算——已经全部走通。5. 移动端适配与离线验证把源码变成可安装的 WabApp5.1 三个必调的移动端参数装饰品电商的流量大头在手机端纯 HTML 页面最容易出现的问题是网格列数写死。不要在 CSS 里写grid-template-columns: 3fr 3fr 3fr用 auto-fill minmax 让浏览器自己决定列数/* base.css —— 商品网格自适应 */ .product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; padding: 12px; } media (max-width: 480px) { .filter-bar { overflow-x: auto; /* 分类太多时横向滚动 */ white-space: nowrap; -webkit-overflow-scrolling: touch; } .top-bar { padding-top: env(safe-area-inset-top); /* 刘海屏顶部安全区 */ } }minmax(160px, 1fr) 的含义是每列最小 160px有剩余空间就平均分配这样 375px 宽的手机显示 2 列768px 的平板显示 34 列不用写多个媒体查询。safe-area-inset-top 是 iPhone 全面屏的安全区变量没有它固定在顶部的导航栏会顶进刘海。真机验证时用 Chrome DevTools 的设备工具栏CtrlShiftM选 iPhone SE 和 iPhone 12 Pro 两个档位各看一遍重点检查两处一是筛选栏是否被挤到换行二是商品图在 2 列排布下有没有被拉伸变形。5.2 Service Worker 离线缓存让页面秒开可复用纯静态页面最大的运维风险是弱网加载慢而装饰品图通常不小。做法是用 Service Worker 做缓存优先第一次访问后图片和 CSS 都走本地缓存// sw.js —— 缓存优先 运行时补缓存 const CACHE_NAME decor-site-v1; self.addEventListener(install, event { event.waitUntil( caches.open(CACHE_NAME) .then(cache cache.addAll([ ./, ./index.html, ./cart.html, ./css/base.css, ./css/product.css, ./js/data.js, ./js/render.js, ./js/cart.js ])) ); }); self.addEventListener(fetch, event { event.respondWith( caches.match(event.request).then(cached cached || fetch(event.request).then(response { const clone response.clone(); caches.open(CACHE_NAME).then(cache cache.put(event.request, clone)); return response; }) ) ); }); self.addEventListener(activate, event { event.waitUntil( caches.keys() .then(keys Promise.all(keys.filter(k k ! CACHE_NAME).map(k caches.delete(k)))) ); });install 里把页面骨架全部预缓存fetch 里先查缓存、没命中再走网络并顺手写入缓存activate 里清理旧版本。这样断网时商品列表和购物车页依然能打开二次访问速度接近秒开。注册代码放在 index.html 的页面底部if (serviceWorker in navigator) { window.addEventListener(load, () navigator.serviceWorker.register(./sw.js)); }验证方法是 DevTools → Application → Service Workers 面板确认状态显示 activated再勾选 Network 面板的 Offline 复选框刷新页面能正常展示就算成功。注意Service Worker 只在 HTTPS 或 localhost 下生效直接双击 file:// 打开会注册失败这也解释了为什么“html 文件无法预览”类问题总在本地双击时出现。5.3 商品更新后如何绕过缓存离线缓存有个副作用发布新商品图后用户看到的还是旧缓存因为缓存优先策略不主动回源。解法是更新 CACHE_NAME 的版本号到 v2activate 里的清理逻辑会把 v1 旧缓存自动删掉。发布流程就是改版本号、重新上传 sw.js 和 assets用户第二次打开时新版本生效想立即验证就硬刷新一次CtrlShiftR然后到 Application → Cache Storage 里确认旧条目已被清除。本文还有配套的精品资源点击获取