纯前端购物车简单版:状态管理与本地持久化实战

发布时间:2026/10/14 13:42:50
纯前端购物车简单版:状态管理与本地持久化实战
简介这份购物车案例【简单版】面向Java Web初学者与课程设计者用JSP、Servlet、Tomcat与数据库搭建一个可运行的在线购物车帮助理解动态网页、请求处理与用户状态保持的完整链路。压缩包共62个文件约916KB包含13个java源码、26个class编译文件、6个jsp页面、5个xml配置、4个jar依赖以及少量html、js与工程配置文件覆盖实体、DAO、工具、服务、Servlet等分层模块目录结构清晰便于对照学习。已有670人学习下载。案例将商品展示、加入购物车、删除商品等操作串成闭环购物车数据存放在Session中可观察跨页面跳转时状态如何保持一致同时涉及JDBC持久化与Tomcat部署运行适合作为Java Web入门练手或课程设计参考也能为后续补充安全与性能优化提供基础框架。1. 一个压缩包背后的购物车逻辑为什么简单版反而最难写对「购物车案例【简单版】.rar」这个标题乍看像某个课程配套的练习压缩包实际拆开看它指向的是一个电商前端最核心的状态管理场景商品列表渲染、加购、数量增减、小计与总价联动、本地持久化。很多人第一次写购物车觉得逻辑简单结果一上手就翻车——数量改一个总价不更新删一项勾选状态错乱刷新页面购物车清空。简单版之所以难是因为它把「状态同步」这件事压缩到了最小可运行单元任何一处数据流没理清整个页面就崩。这篇文章面向的是想用最短路径跑通购物车核心逻辑的前端开发者不管你是刚学完 DOM 操作的新手还是想回头补状态管理基本功的熟手。我会用一个纯前端、零依赖的最小实现把商品数据、购物车状态、视图更新、本地存储四件事串起来每一步都给可复制的代码和参数说明。你照着做能在本地跑出一个能加购、能改数量、能算总价、刷新不丢数据的购物车你也能看到哪些地方最容易埋坑以及简单版和真实项目之间的边界在哪。2. 先把数据模型定下来商品、购物车项和总价的关系2.1 为什么先画数据再写 DOM购物车翻车十有八九是数据模型没定清楚就开始写页面。常见做法是先定义商品列表products每个商品有 id、name、price、stock再定义购物车cartcart 里存的是「购物车项」每项包含 productId 和 quantity。注意cart 里不要直接存商品完整对象只存 id 和数量商品详情通过 id 去 products 里查。这样做的好处是商品价格变了购物车总价自动跟着变不会出现「加购时价格是旧的结算时对不上」的玄学问题。我一般会再补一个「选中状态」字段 selected因为真实购物车里用户会勾选部分商品结算。简单版可以先不做勾选但字段留着后面扩展不用改结构。数据模型定好之后所有视图更新都只做一件事根据当前 cart 和 products 重新计算派生数据小计、总价、选中数量然后刷新 DOM。这就是「单向数据流」的最小实践。2.2 用代码把模型写出来// 商品列表模拟从接口拿到的数据 const products [ { id: 1, name: 无线鼠标, price: 89, stock: 10 }, { id: 2, name: 机械键盘, price: 299, stock: 5 }, { id: 3, name: 显示器支架, price: 159, stock: 8 } ]; // 购物车只存 productId 和 quantityselected 预留 let cart [ { productId: 1, quantity: 2, selected: true }, { productId: 3, quantity: 1, selected: true } ]; // 根据 productId 查商品避免 cart 里存冗余数据 function getProductById(id) { return products.find(p p.id id); } // 计算购物车总价遍历 cart累加 price * quantity function calcTotal() { return cart.reduce((sum, item) { const product getProductById(item.productId); if (!product) return sum; // 商品被删除时跳过防止报错 return sum product.price * item.quantity; }, 0); }这段代码里products和cart是仅有的两份「源数据」calcTotal是纯函数不碰 DOM。参数说明reduce的初始值给 0保证空购物车返回 0 而不是 undefinedgetProductById用find而不是filter因为 id 唯一找到就停性能更好。逻辑说明所有金额计算都基于当前 products 里的价格cart 只负责记录「买了什么、买了几件」这样价格变动时总价自动正确。2.3 渲染函数只做一件事把数据翻译成 HTML// 渲染购物车列表和总价 function renderCart() { const cartEl document.getElementById(cart-list); const totalEl document.getElementById(total-price); if (cart.length 0) { cartEl.innerHTML p classempty购物车是空的/p; totalEl.textContent 0.00; return; } // 用 map 生成 HTML 字符串再一次性塞进容器 cartEl.innerHTML cart.map(item { const product getProductById(item.productId); if (!product) return ; return div classcart-item>// 事件委托统一处理加购、增减、删除 document.getElementById(cart-list).addEventListener(click, (e) { const target e.target; const itemEl target.closest(.cart-item); if (!itemEl) return; // 点的不是购物车项直接返回 const productId Number(itemEl.dataset.id); const cartItem cart.find(item item.productId productId); if (!cartItem) return; if (target.classList.contains(plus)) { const product getProductById(productId); if (cartItem.quantity product.stock) { alert(库存不足); return; } cartItem.quantity 1; } else if (target.classList.contains(minus)) { if (cartItem.quantity 1) return; // 最少 1 件减到 1 就不动 cartItem.quantity - 1; } else if (target.classList.contains(remove)) { cart cart.filter(item item.productId ! productId); } renderCart(); // 数据改完立刻重渲染 });逻辑说明closest(.cart-item)从点击的按钮往上找最近的购物车项容器拿到>// 商品列表加购 document.getElementById(product-list).addEventListener(click, (e) { if (!e.target.classList.contains(add-to-cart)) return; const productId Number(e.target.closest(.product-item).dataset.id); const product getProductById(productId); const existing cart.find(item item.productId productId); if (existing) { if (existing.quantity product.stock) { alert(库存不足); return; } existing.quantity 1; } else { cart.push({ productId, quantity: 1, selected: true }); } renderCart(); });逻辑说明先查 cart 里有没有这个商品有就数量加一没有就 push 新项。参数说明selected: true是预留字段简单版不参与计算但加上不影响。注意这里没有做「库存为 0 时禁止加购」的判断真实项目里要在商品列表渲染时就禁用按钮或者在这里加if (product.stock 0) return;。3.3 本地持久化刷新不丢购物车// 保存购物车到 localStorage function saveCart() { localStorage.setItem(cart, JSON.stringify(cart)); } // 页面加载时读取 function loadCart() { const saved localStorage.getItem(cart); if (saved) { try { cart JSON.parse(saved); } catch (err) { console.error(购物车数据解析失败, err); cart []; } } }逻辑说明每次renderCart()之前调用saveCart()或者直接在renderCart()里调用保证数据一变就存。参数说明JSON.parse要包try/catch因为用户可能手动改了 localStorage 里的内容解析失败时回退到空数组避免整个页面白屏。注意localStorage 存的是字符串取出来要反序列化如果以后 cart 结构变了旧数据可能不兼容简单版可以先不管但要知道这个坑。4. 避坑与排查购物车简单版最容易翻车的 5 个地方4.1 总价不更新现象、原因、解决现象改了数量小计变了但总价没变。原因总价是在页面加载时算了一次之后没有重新计算。解决把总价计算放进renderCart()里每次重渲染都重新算不要缓存。如果用了缓存要在数据变更时手动清缓存容易漏。4.2 删除后勾选状态错乱现象、原因、解决现象删了中间一项后面项的勾选状态跑到前面去了。原因用索引做 key 或者用索引判断选中状态删除后索引偏移。解决所有操作基于productId不要用数组下标。渲染时>let derived { total: 0, count: 0 }; function updateDerived() { derived.total cart.reduce((sum, item) { const p getProductById(item.productId); return p ? sum p.price * item.quantity : sum; }, 0); derived.count cart.reduce((sum, item) sum item.quantity, 0); } // 数据变更后调用 updateDerived()渲染时直接用 derived.total逻辑说明把「计算」和「渲染」分开渲染只读 derived不重复算。参数说明count是总件数可以用来显示购物车角标。注意updateDerived()必须在每次数据变更后调用漏调就会显示旧值所以最好把它和saveCart()一起封装成commit()函数数据一变就调commit()。5.2 用模板字符串做 HTML 转义如果商品名来自用户输入比如后台管理系统直接拼进innerHTML会有 XSS 风险。简单版可以先不做但要知道怎么补function escapeHtml(str) { return str.replace(/[]/g, (m) ({ : amp;, : lt;, : gt;, : quot;, : #39; }[m])); }逻辑说明把五个特殊字符替换成 HTML 实体防止标签被解析。参数说明正则里的g表示全局替换不写只替换第一个。注意如果商品名里本来就有转义后会显示成amp;这是正确的浏览器渲染时会还原。5.3 用事件委托处理动态元素时的一个细节事件委托里用e.target判断按钮但如果按钮里包了图标比如buttoni classicon/i/button点图标时e.target是i而不是button。解决用e.target.closest(button)往上找按钮或者判断e.target.closest(.plus)。我一般用closest因为它能穿透多层嵌套代码更稳。const btn e.target.closest(.plus, .minus, .remove); if (!btn) return; if (btn.classList.contains(plus)) { /* ... */ }逻辑说明closest接受选择器列表返回第一个匹配的祖先元素包括自身。参数说明选择器用逗号分隔匹配任意一个即可。注意closest在旧版浏览器IE不支持如果项目要兼容 IE得自己写循环往上找或者用 polyfill。5.4 验证方法用控制台快速检查数据一致性写完购物车别急着点页面先在控制台跑几行验证// 检查 cart 里每一项都能在 products 里找到 console.assert(cart.every(item getProductById(item.productId)), 存在无效商品); // 检查总价和手动计算一致 const manualTotal cart.reduce((s, i) s getProductById(i.productId).price * i.quantity, 0); console.assert(Math.abs(calcTotal() - manualTotal) 0.001, 总价计算不一致);逻辑说明console.assert在条件为假时输出错误不阻断执行。参数说明浮点数比较用差值小于 0.001避免精度问题。这个习惯帮我省了很多「页面看着对、数据其实错了」的后悔药时间。我自己的习惯是每次改完购物车逻辑先跑一遍控制台断言再点页面操作。这样能把「数据错」和「渲染错」分开定位不用在 DOM 里瞎找。希望帮到你。本文还有配套的精品资源点击获取