简介这份资源是一套可直接运行的静态购物网站源码面向正在学习前端基础、需要完整小项目练手的初学者与在校学生用来解决“知识点零散、缺少可落地页面案例”的问题。压缩包共48个文件约5.28MB以16个png与14个jpg图片素材、8个js脚本、5个css样式表和4个html页面为主体另含1个json配置页面覆盖首页、商品详情、购物车与登录注册样式分离在base、style、car等独立文件中图片资源集中放置目录结构清晰便于按页面与模块对照阅读。已有1043人学习下载。代码中对HTML5表单校验、localStorage登录状态保持、轮播图与三级菜单展开收起、购物车商品增删与金额实时计算等均有对应实现jQuery版本文件与业务脚本分开放置适合逐文件拆解、改造成自己的课程作业或用作二次开发起点。1. 一个“看起来能跑”的静态站卡在登录这一步某开发者把一份 html5购物网站带登录 的压缩包发过来首页轮播会动、商品卡片排得整整齐齐点“登录”也能跳到登录页。可真正上手点两下就露馅了登录按钮不校验任何东西填个空直接location.href跳回首页购物车加到第三件商品时数字对不上刷新一次全清空。对着这份 html js jQuery css 堆出来的静态购物网站折腾了半天问题其实只有一个——状态没人管。静态站不等于“没有后端就做不了登录”。浏览器给了 localStorage 这个免费仓库注册、登录、会话、购物车、订单快照全都能在里面跑通代价是每一个读写点都得自己收口否则东写一处西写一处改到第三天你自己都记不清哪个 key 才是真的。这份资源适合两类人一类是想把 DOM 操作、事件委托、本地状态管理一次性练熟的前端新手另一类是后端接口还没就绪需要先做一套能点、能走完整流程的页面给业务看的人。下面按我拆这份包的顺序讲。2. 目录结构与技术选型静态站怎么切模块才不烂尾拿到压缩包先别急着双击index.html先看目录。静态购物站最容易烂尾的地方不是样式而是所有逻辑塞进一个main.js两千行里既有轮播又有登录还有金额计算改一个需求要滚半天。这份包的目录分层还算规矩我按自己的习惯重新梳理了一遍结构如下。2.1 按“页面 / 逻辑 / 数据 / 静态资源”四层切shop/ ├── index.html # 首页轮播 分类筛选 商品列表 搜索框 ├── login.html # 登录 / 注册双 Tab同一个页面切换 ├── detail.html # 商品详情?id1001 取参 ├── cart.html # 购物车支持增减数量、删除、全选 ├── order.html # 结算页生成订单快照 ├── css/ │ ├── reset.css # 清默认样式必须第一个引入 │ ├── common.css # 头尾、按钮、色板变量、提示条 │ └── page.css # 各页面私有样式按页面追加 ├── js/ │ ├── common.js # 金额格式化、URL 取参、toast、防抖 │ ├── auth.js # 注册 / 登录 / 会话 / 页面守卫 │ ├── cart.js # 购物车增删改查唯一读写入口 │ └── page-index.js # 首页专属逻辑一个页面一个文件 ├── data/ │ └── products.json # 商品数据源20 条左右够演示 └── images/ └── goods/ # 商品图命名统一用商品 id这套划分的关键在于js/下三个文件的分工common.js只放无状态的纯函数auth.js和cart.js各管一个 localStorage keypage-*.js只负责“取数据、拼 HTML、绑事件”不直接碰存储。你改需求时先判断它属于哪一层就不会出现“登录逻辑藏在轮播代码里”这种血泪现场。data/products.json单独抽出来是个好习惯。商品列表、详情页、购物车回显都要用到商品信息数据源只有一份改价格改库存不用满项目搜字符串。注意images/goods/的命名我用商品 id 对齐1001.jpg对应id: 1001拼路径时不用额外维护映射表。2.2 技术选型jQuery 在这类页面里到底省了什么看到 jQuery 别急着说老。这套页面没有构建工具、没有 npm、没有打包双击就能跑虽然$.getJSON那步得配本地服务器第 5 章细说jQuery 在这种场景下的性价比相当高。我把几个高频操作的写法摆一起对比操作原生写法jQuery 写法选元素document.querySelectorAll(.card)$(.card)事件委托list.addEventListener(click, e { if (e.target.closest(.btn)) ... })$(#list).on(click, .btn, fn)AJAXfetch(url).then(r r.json())$.getJSON(url, cb)批量改样式循环 classList$(.item).addClass(active)元素显隐el.style.display none$el.hide()真正省事的不是选择器那点字符是.on()的事件委托和$.getJSON的回调风格。购物车列表是动态渲染的商品卡片删了又加、加了又删用 jQuery 直接在父容器上挂一个委托渲染一万次也不用重新绑事件。原生写也能做但每处都要记得closest和matches新手很容易在这里翻车。版本上我一般固定用 3.x 的 slim 之外那版别用 4.x 的 beta也别引 CDN 上会随时变的最新版。整个项目只留一份jquery.min.js放js/lib/下本地引入断网也能跑。提示不要把 jQuery 放 CDN 又不在本地留备份。演示环境断网的概率比你想象的高。2.3 页面骨架与公共头部复用五个页面共用同一套头尾最省事的做法是每个 HTML 里复制粘贴但改导航要改五遍。我一般用“静态骨架 JS 注入”的折中方案每个页面留一个空的header idapp-header/header公共片段放partials/header.html用$.get拉进来。!-- index.html 的页面骨架 -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 !-- 移动端必须否则 375 宽度下页面会被缩成一半 -- meta nameviewport contentwidthdevice-width, initial-scale1.0 title商城首页/title link relstylesheet hrefcss/reset.css link relstylesheet hrefcss/common.css link relstylesheet hrefcss/page.css /head body header idapp-header/header main classcontainer div idbanner classbanner/div div idgoods-list classgoods-list/div /main footer idapp-footer/footer !-- 顺序不能乱jQuery 必须在所有业务脚本之前 -- script srcjs/lib/jquery.min.js/script script srcjs/common.js/script script srcjs/auth.js/script script srcjs/cart.js/script script srcjs/page-index.js/script /body /htmlscript的引入顺序是硬约束auth.js里要用common.js的toast()page-index.js里要用auth.js的isLogin()谁在前谁在后错了就是xxx is not defined。CSS 同理reset.css排第一page.css排最后覆盖公共样式。partials/header.html里放导航、搜索框和右上角的登录态区域加载完成后由auth.js补一次状态// js/common.js —— 公共方法不碰存储 window.Shop window.Shop || {}; // 金额分转元永远保留两位别直接用 toFixed 处理原始浮点 Shop.fen2yuan function (fen) { return (fen / 100).toFixed(2); }; // 取 URL 参数detail.html?id1001 里拿 id Shop.getQuery function (name) { var m new RegExp([?] name ([^#]*)).exec(location.search); return m ? decodeURIComponent(m[1]) : ; }; // 顶部轻提示1.8 秒自动消失替代 alert 打断流程 Shop.toast function (msg) { var $t $(#shop-toast); if (!$t.length) { $t $(div idshop-toast classtoast/div).appendTo(body); } $t.text(msg).addClass(show); clearTimeout(Shop._toastTimer); Shop._toastTimer setTimeout(function () { $t.removeClass(show); }, 1800); }; // 页面初始化时注入公共头尾 $(function () { $(#app-header).load(partials/header.html, function () { // 片段加载完再刷新登录态顺序反了就取不到 DOM if (window.Auth) Auth.renderHeaderState(); }); $(#app-footer).load(partials/footer.html); });三个函数都是无状态的纯工具fen2yuan接收以“分”为单位的整数返回两位小数字符串这是第 4 章金额计算的底座getQuery用正则从location.search提取参数toast复用一个 DOM 节点靠clearTimeout保证短时间连点不会串提示。$(function(){})里先load再回调刷新状态是因为.load()是异步的写在回调外面拿到的还是空容器。3. 登录链路localStorage 会话、表单校验与页面守卫这部分是整个包里最值得抄的。静态站的“登录”本质上就是一个本地会话注册时把用户写进数组登录时比对成功就往另一个 key 里写会话对象后续所有页面读这个会话判断身份。没有服务端所以密码怎么存、会话多久过期、未登录怎么拦全得自己想清楚。3.1 用户表结构与注册写入用户表就是一个对象数组每个对象至少四个字段// js/auth.js —— 账号相关的所有读写都收在这个 IIFE 里避免污染全局 (function () { var USER_KEY shop_users; // 全部注册用户 var SESSION_KEY shop_session; // 当前登录会话 var SESSION_TTL 7 * 24 * 3600 * 1000; // 会话有效期 7 天 // 读用户表第一次运行时 localStorage 里没有返回空数组 function getUsers() { var raw localStorage.getItem(USER_KEY); if (!raw) return []; try { var arr JSON.parse(raw); return Array.isArray(arr) ? arr : []; // 防一手脏数据 } catch (e) { return []; // 手动改坏了就当空表不要让整个页面白屏 } } // 极简哈希只为了避免明文密码直接躺在 localStorage 里被人一眼看到 function hash(str) { var h 5381; for (var i 0; i str.length; i) { h ((h 5) h str.charCodeAt(i)) 0; } return h.toString(16); } function register(username, password, confirm) { username $.trim(username); if (username.length 3 || username.length 16) { return { ok: false, msg: 用户名需 3 到 16 个字符 }; } if (!/^[a-zA-Z0-9_\u4e00-\u9fa5]$/.test(username)) { return { ok: false, msg: 用户名含非法字符 }; } if (password.length 6) { return { ok: false, msg: 密码至少 6 位 }; } if (password ! confirm) { return { ok: false, msg: 两次输入的密码不一致 }; } var users getUsers(); // 用户名唯一性检查注意统一转小写比较避免 Admin 和 admin 撞车 if (users.some(function (u) { return u.username.toLowerCase() username.toLowerCase(); })) { return { ok: false, msg: 该用户名已被注册 }; } users.push({ username: username, password: hash(password), createdAt: Date.now() }); localStorage.setItem(USER_KEY, JSON.stringify(users)); return { ok: true, msg: 注册成功 }; } // ... 登录部分见下一节 window.Auth { register: register, getUsers: getUsers, hash: hash }; })();getUsers()里的try/catch不是多余的。开发者调试时会手动去 Application 面板改 localStorage格式改错一次JSON.parse直接抛错整个页面的 JS 就停在那儿了白屏还找不到原因。兜底返回空数组至少页面能正常渲染。用户名唯一性检查必须两边都toLowerCase()否则用户可以注册出Admin和admin两个账号后面按用户名查订单时会取到两条。密码用hash()处理再存这不是加密只是防止别人打开开发者工具一眼看到你的密码聊胜于无。真实项目里这事必须交给服务端静态站没法做真正的安全。3.2 登录校验与会话保持登录要做的三件事找到用户、比对密码、写会话。会话对象里我习惯放username、loginAt、expireAt三个字段过期判断不依赖localStorage自己的生命周期它本来就不过期。// 接着上面的 IIFE 写 function login(username, password, remember) { username $.trim(username); if (!username || !password) { return { ok: false, msg: 用户名和密码不能为空 }; } var users getUsers(); var user users.filter(function (u) { return u.username.toLowerCase() username.toLowerCase(); })[0]; if (!user) { return { ok: false, msg: 用户不存在 }; } if (user.password ! hash(password)) { return { ok: false, msg: 密码错误 }; } // remember 勾选则 7 天不勾选只保留 2 小时 var ttl remember ? SESSION_TTL : 2 * 3600 * 1000; var session { username: user.username, loginAt: Date.now(), expireAt: Date.now() ttl, remember: !!remember }; localStorage.setItem(SESSION_KEY, JSON.stringify(session)); return { ok: true, msg: 登录成功, session: session }; } // 取当前会话顺手做过期清理所有页面都通过这个方法判断登录态 function getSession() { var raw localStorage.getItem(SESSION_KEY); if (!raw) return null; try { var s JSON.parse(raw); if (!s || !s.username || !s.expireAt) return null; if (Date.now() s.expireAt) { localStorage.removeItem(SESSION_KEY); // 过期就地清掉别留垃圾 return null; } return s; } catch (e) { localStorage.removeItem(SESSION_KEY); return null; } } function isLogin() { return !!getSession(); }getSession()是全篇最重要的一个函数因为它把“读会话”和“过期清理”合到了一起。如果每个页面自己去JSON.parse再判断时间早晚有一处漏判断于是出现“明明过期了还能进订单页”的玄学问题。集中到一个入口谁都绕不过去。login里没有提示“密码错误”之外的任何信息比如“密码是 6 位”登录失败一律模糊提示这是基本习惯。勾选框remember影响 TTL勾了 7 天不勾 2 小时。你要是把它做成“永远不过期”演示给别人看的时候很容易忘记退出第二天打开还是登录态自己都懵。登录页的事件绑定很直白两个 Tab 切换加两个表单提交// login.html 底部引入的 js/page-login.js $(function () { // Tab 切换靠>// auth.js 里补一个守卫方法 function guard(redirectBack) { if (isLogin()) return true; var back redirectBack || (location.pathname.split(/).pop() location.search); location.replace(login.html?back encodeURIComponent(back)); return false; } // 退出只清会话不动用户表否则下次还得重新注册 function logout() { localStorage.removeItem(SESSION_KEY); location.href index.html; }购物车页、结算页、订单页的第一行都这么写// js/page-cart.js 开头 $(function () { if (!Auth.guard()) return; // 未登录直接跳走后面的渲染代码一行都别执行 renderCart(); });注意是return而不是if (Auth.isLogin()) { ...一大堆代码... }。用提前返回能把整段业务逻辑保持在第一层缩进后面再加权限判断比如下单前必须填手机号也是继续return不会越缩越深。logout()只删SESSION_KEY保留USER_KEY这点很多人写反。清用户表意味着退出后账号没了下次还得注册一遍自己调试都能被自己气到。退出后跳首页而不是留在原地是为了避免用户按浏览器返回键又看到购物车内容。4. 商品与购物车JSON 渲染、加购状态与金额计算购物链路的三个环节——列表渲染、加购同步、结算算钱——是静态站里最容易出 bug 的地方。核心原则只有一条localStorage 里的购物车是唯一真相页面上的 DOM 只是它的投影。任何一次加减都要先改数据再重渲染别反过来。4.1 商品数据结构与列表渲染data/products.json我建议长这样字段别贪多够页面用就行[ { id: 1001, name: 轻量保温杯 500ml, price: 8900, stock: 32, cat: home, cover: images/goods/1001.jpg }, { id: 1002, name: 折叠双肩包 20L, price: 15900, stock: 8, cat: bag, cover: images/goods/1002.jpg }, { id: 1003, name: 桌面收纳盒三件套, price: 4500, stock: 0, cat: home, cover: images/goods/1003.jpg } ]price单位是分不是元。15900就是 159 块。这是第 4.3 节防浮点误差的前提也是很多人第一次写购物车时踩的坑。stock为 0 时前端要禁掉加购按钮cat用来做分类筛选。渲染用模板字符串拼 HTML一次html()整体替换比循环append快得多也不会出现事件重复绑定的问题// js/page-index.js $(function () { // 全局只拉一次数据放内存里后面筛选直接用 var allGoods []; $.getJSON(data/products.json, function (list) { allGoods list; renderList(list); }).fail(function () { // 关键file:// 打开时这里一定会走 fail给用户一句人话 $(#goods-list).html(p classempty数据加载失败请用本地服务器打开页面/p); }); function renderList(list) { if (!list.length) { $(#goods-list).html(p classempty没有找到相关商品/p); return; } var html list.map(function (g) { var soldOut g.stock 0; return [ div classcard>// js/cart.js (function () { var CART_KEY shop_cart; // 购物车结构[{ id: 1001, qty: 2, addedAt: 1700000000000 }] // 只存 id 和数量商品名价格渲染时再去 goods 里查避免价格改了购物车里还是旧值 function read() { try { var arr JSON.parse(localStorage.getItem(CART_KEY) || []); return Array.isArray(arr) ? arr : []; } catch (e) { return []; } } function write(list) { localStorage.setItem(CART_KEY, JSON.stringify(list)); // 每次写都通知一次头部角标靠这个事件刷新 $(document).trigger(cart:change, [list]); } function add(id, qty) { id Number(id); qty Number(qty) || 1; var list read(); var item list.filter(function (i) { return i.id id; })[0]; if (item) { if (item.qty qty 99) return { ok: false, msg: 单件商品最多 99 件 }; item.qty qty; } else { list.push({ id: id, qty: qty, addedAt: Date.now() }); } write(list); return { ok: true, msg: 已加入购物车 }; } function updateQty(id, qty) { id Number(id); qty Number(qty); if (qty 1) return remove(id); // 减到 0 直接当删除处理 if (qty 99) qty 99; var list read(); list.forEach(function (i) { if (i.id id) i.qty qty; }); write(list); return { ok: true }; } function remove(id) { id Number(id); write(read().filter(function (i) { return i.id ! id; })); return { ok: true, msg: 已移除 }; } window.Cart { read: read, add: add, updateQty: updateQty, remove: remove, write: write }; })();这里有三个设计取舍值得说。第一购物车只存id和qty不存商品名和价格。如果存了快照管理员改了价格用户购物车里还是旧价结算时对不上账只在渲染时关联商品数据永远是当前价。第二write()里触发cart:change自定义事件头部那行“购物车(3)”的角标就靠监听这个事件刷新不用每个页面自己去数。第三add()里id Number(id)的转换不能省。事件委托拿到的>// js/page-cart.js $(function () { if (!Auth.guard()) return; var goodsMap {}; // id - 商品对象避免循环里反复查数组 $.getJSON(data/products.json, function (list) { list.forEach(function (g) { goodsMap[g.id] g; }); render(); }); function render() { var list Cart.read(); if (!list.length) { $(#cart-body).html(p classempty购物车还是空的/p); $(#cart-total).text(0.00); return; } var html list.map(function (item) { var g goodsMap[item.id]; if (!g) return ; // 商品已下架渲染时跳过 return [ tr>// 结算运费、满减都在“分”上算 function calcOrder(cartList, goodsMap) { var goodsFen 0; var count 0; cartList.forEach(function (item) { var g goodsMap[item.id]; if (!g) return; goodsFen g.price * item.qty; // 整数乘整数永远精确 count item.qty; }); // 满 199 减 20门槛和优惠额也用分表示 var discountFen goodsFen 19900 ? 2000 : 0; // 运费满 99 包邮否则 8 元 var freightFen (goodsFen - discountFen) 9900 ? 0 : 800; var payFen goodsFen - discountFen freightFen; if (payFen 0) payFen 0; // 兜底别让订单出现负数 return { count: count, goods: Shop.fen2yuan(goodsFen), discount: Shop.fen2yuan(discountFen), freight: Shop.fen2yuan(freightFen), pay: Shop.fen2yuan(payFen), payFen: payFen }; }参数说明cartList是Cart.read()的结果goodsMap是 id 到商品对象的映射。返回值里同时给了展示用的字符串和payFen整数后者在生成订单快照时存进去以后要做退款、对账都不会因为精度问题对不上。订单快照是结算的最后一步把当前购物车和商品信息冻结成一条记录// js/page-order.js function createOrder(goodsMap) { var list Cart.read(); if (!list.length) return { ok: false, msg: 购物车为空 }; var detail list.map(function (item) { var g goodsMap[item.id]; return { id: g.id, name: g.name, price: g.price, qty: item.qty }; }); var calc calcOrder(list, goodsMap); var order { orderNo: S Date.now() Math.floor(Math.random() * 900 100), // 演示用别当真实单号 user: Auth.getSession().username, items: detail, // 快照存商品名和下单时价格 payFen: calc.payFen, status: unpaid, createdAt: Date.now() }; var orders JSON.parse(localStorage.getItem(shop_orders) || []); orders.unshift(order); // 新订单排前面 localStorage.setItem(shop_orders, JSON.stringify(orders)); Cart.write([]); // 下单成功后清空购物车 return { ok: true, order: order }; }订单里存的是快照商品名 下单时价格不是 id 引用。这是订单和购物车的本质区别购物车跟着商品走订单必须冻结在那一刻否则三个月后商品调价历史订单金额全变用户截图来对账你就说不清了。5. 排查清单静态购物站最容易翻车的五个地方这些坑我基本都在不同项目里踩过一遍按“现象 → 原因 → 解决”排出来遇到问题时对着查。5.1 路径与资源类现象一双击index.html打开商品列表一直是空的控制台报Access to XMLHttpRequest at data/products.json has been blocked by CORS policy。原因是浏览器对file://协议的限制。$.getJSON走的XMLHttpRequest在本地文件协议下被视为跨域请求浏览器直接拦掉跟代码没关系。解决办法是起个本地服务器Python 自带的就够# 在项目根目录执行然后用 http://localhost:8000 访问 python3 -m http.server 8000python3 -m http.server后面跟的8000是端口号被占用就换8080。这条命令必须在有index.html的那一层执行跑到上一级目录去访问会 404。用 Node 的话npx serve也一样。现象二首页图片全裂Network 面板里请求的是images/goods/1001.JPG实际文件叫1001.jpg。Windows 的文件系统不区分大小写macOS 和 Linux 区分。你在 Windows 上开发一切正常传到服务器就全裂。解决方式是统一小写命名并在提交前搜一遍代码里的.JPG / .PNG全改成小写。图片路径也别写绝对路径/images/xxx项目不在根目录部署时会全部 404用相对路径images/xxx最稳。5.2 状态与数据类现象三localStorage.getItem(shop_cart)拿到的是[object Object]JSON.parse直接报错。原因是存的时候写成了localStorage.setItem(shop_cart, cartObj)第二个参数传了个对象setItem会隐式调用toString()。解决办法只有两条存之前JSON.stringify取之后JSON.parse而且必须成对出现在同一个模块里。我一般把这两个动作收进read()和write()两个函数业务代码不许直接碰localStorage从源头上断掉这类问题。现象四购物车加了 3 件商品在另一个标签页删掉一件切回来页面还显示 3 件。localStorage是同源的但变化不会自动同步到已打开的页面。解决是靠storage事件它只在其他标签页修改时触发当前页自己改不会触发这点很多人不知道// 监听其他标签页的购物车变化 $(window).on(storage, function (e) { var ev e.originalEvent; if (!ev.key) return; if (ev.key shop_cart || ev.key shop_session) { // 数据变了就整体重渲染一次简单粗暴但不会错 if (typeof render function) render(); if (window.Auth) Auth.renderHeaderState(); } });ev.key为null的情况是别的页面调了localStorage.clear()这种直接整页刷新最省事。5.3 事件与渲染类现象五购物车里点“”第一次有效点第二次没反应或者商品列表渲染完之后加购按钮点了完全没反应。原因通常是两种。一种是事件绑在了已经被html()替换掉的旧 DOM 上——你绑在.btn-add元素本身重渲染后旧元素消失新元素没绑过事件。解决办法是把事件委托到不会被替换的父容器上也就是$(#cart-body).on(click, button, fn)这种写法第二个参数传选择器才是委托不传就是直接绑定。另一种是同一个按钮被绑了多次点一次加了两件。常见于“筛选商品后重新渲染 重新绑事件”的代码里。判断方法是在处理函数第一行console.log(click)点一下打出三条就是重复绑定了。用委托写法天然免疫这个问题因为绑定只发生一次跟渲染次数无关。5.4 会话与权限类现象 6登录后在购物车页刷新头部的“退出登录”变成了“请登录”。renderHeaderState()在partials/header.html加载完成之前就执行了此时#header-user还不存在.text()作用在空集合上静默失败。解决办法是把状态刷新放进.load()的第二个参数回调保证 DOM 到位后再改。这类“异步加载 同步取 DOM”的组合在静态站里到处都是凡是.load()、$.get()、$.getJSON()后面直接操作 DOM 的都要检查一遍是不是漏了回调。6. 二次开发把这套静态站改成能继续长的骨架如果只是交个课程作业第 5 章结束就够用了。但如果你打算拿它当个人项目的底座我建议先从数据层动刀而不是改页面。第一步是把data/products.json换成一个可替换的接口层。新建js/api.js把$.getJSON(data/products.json)包成Api.getGoods()内部用$.Deferred()返回 Promise这样以后后端就绪只改api.js里那一个$.ajax的 url页面代码一行不用动// js/api.js —— 所有数据来源收口方便从静态 JSON 切到真实接口 window.Api { getGoods: function () { var dfd $.Deferred(); $.getJSON(data/products.json) .done(function (list) { dfd.resolve(list); }) .fail(function () { dfd.reject(new Error(商品加载失败)); }); return dfd.promise(); // 后端就绪后只改这一处 // return $.getJSON(/api/goods).then(r r.data); } };第二步是把散落的localStorage读写抽成js/store.js提供Store.get(key, default)和Store.set(key, val)内部统一做JSON序列化和异常兜底。这一步看着琐碎但它决定了你以后想换成sessionStorage、IndexedDB 或者接口缓存时是改三行还是改三十处。第三步才是加功能。我列了个改造优先级的表按投入产出排改造点改动范围价值商品分页/触底加载page-index.jsApi.getGoods加参数数据量上去后必须有地址簿新增storekey 结算页表单下单流程完整度订单列表页读shop_orders 状态筛选用户能查历史迁移到 Vue/React数据层不动页面层重写彻底摆脱手工 DOM迁移那一步我特别想提醒一句先用Api和Store把数据边界划清楚再谈换框架。很多人的做法是直接重写结果业务逻辑和数据读写搅在一起新框架写完了还是同样的坑。数据层先稳住页面层换多少次都不慌。验证方式也很朴素打开开发者工具 Application 面板看shop_users / shop_session / shop_cart / shop_orders四个 key 的结构对不对把expireAt手动改到过去刷新页面看能不能被踢出去把购物车里某件商品的 id 改成 JSON 里不存在的值看页面是白屏还是跳过。这三步走一遍比写十个测试用例都直接。从那以后我每次接这种“无后端静态站”的活第一件事都是先画一张 key 表写清楚每个 key 存什么结构、谁写谁读、过期怎么清再动手写第一行 JS。没有这张表写到第 4 个页面必定开始混乱。希望帮到你。本文还有配套的精品资源点击获取