ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

原生JavaScript手写购物车:数据驱动视图与事件委托实践

2026/9/8 7:16:11 拓冰建站 浏览量
原生JavaScript手写购物车:数据驱动视图与事件委托实践 简介针对Java Web初学者这份购物车案例简单版压缩包演示了JSP、Servlet、Tomcat与数据库协同开发的基本流程旨在解决初学者对会话跟踪与业务逻辑分层不清晰的问题。压缩包共62个文件涵盖13个Java源文件、26个class字节码、6个JSP页面、5个XML配置、4个jar依赖及少量HTML/JS静态文件整体体积仅916KB结构上包含entity、dao、utils、servlet等典型分层便于按模块学习。目前已有668人学习下载。通过该案例可掌握商品列表展示、加入购物车、修改数量、删除商品等常用操作理解Session在跨页面保持购物车状态中的作用同时了解Servlet接收请求并更新Session、再由JSP渲染响应的完整链路。案例还涉及数据库存储商品信息与购物车数据演示了基于JDBC的持久化过程并包含对SQL注入、XSS等安全问题的初步提示适合作为Java Web课程设计或入门练手项目。1. 项目拆解先搞清楚这个“简单版”到底解决了什么问题收到“购物车案例【简单版】.rar”这个压缩包的时候我第一反应是清理硬盘时顺手打开扫一眼结果发现这个看似基础的项目其实把前端购物车场景里最核心的几条链路都串起来了。之前带新人时经常强调一点购物车功能是所有电商前端入门的“必修课”它不像登录注册那样依赖后端接口也不需要复杂的权限体系但恰恰能把数据管理、状态同步、事件绑定、本地存储这些基本功一次性练到位。先说清楚这个简单版购物车适合谁。如果你正在学原生JavaScript刚能把数组和对象用明白想找一个“不用框架也能跑起来”的完整示例或者你在做课程设计、个人作品集需要一个界面干净、逻辑清晰的前端案例又或者你是刚转行做前端的同学想看看一个真实的购物车页面在代码层面到底是怎么组织的——这个压缩包都可以作为参考。它不是什么商业级的大厂方案但它麻雀虽小五脏俱全。我以前带过一个实习生上来就用Vue写购物车结果问他“全选反选为什么用computed不用watch”答不上来。为什么因为框架的响应式帮他把最基础的“数据变化后手动更新页面”这件事给藏起来了。而简单版购物车的价值恰恰在于它“笨”它逼着你用原生JS去处理每一次数据变更去手动调用渲染函数去思考事件委托怎么写。搞懂这一层后面再上框架你理解响应式原理会快很多。这个案例的整体功能预期也交代一下商品列表展示、加入购物车、修改数量、删除商品、单选/全选/反选、合计金额计算。数据用本地数组模拟持久化交给localStorage刷新页面不丢数据。从零开始实现不依赖任何第三方库浏览器打开就能跑。2. 整体架构设计为何采用“数据驱动 手动渲染”这种朴素方案2.1 核心设计思路一切页面变化都源于数据变化拿到项目后我先看它的代码目录和整体结构发现它的思路非常清晰所有的商品信息、勾选状态、数量变化全部存放在一个JavaScript对象里页面上所有可见的变动都是先改数据再重新渲染列表区域。这种方式其实就是现在所有前端框架背后的核心思想——数据驱动视图只不过这里没有框架帮你做依赖收集需要自己“手动”把数据和DOM同步起来。具体来说案例里大概是这样的数据模型// 购物车数据模型简化示意 let cartData { items: [ { id: 1, name: 商品A, price: 29.9, count: 2, selected: true }, { id: 2, name: 商品B, price: 49.9, count: 1, selected: false } ], totalPrice: 0, selectedCount: 0 }所有操作——点加号、点减号、点删除、点复选框——本质都是修改这个对象的属性修改完之后再调用一次渲染函数。这个模式简单到近乎朴素但它极其适合学习也极其适合小项目。我建议你自己玩这个案例时一定要在控制台打印一下每次操作后的cartData观察数据和页面变化之间的对应关系。这个体验比任何文档都管用。2.2 为什么不用框架或UI库取舍背后的真实原因很多初学者看到这种项目第一反应是“这不就是个数据CRUD吗我用Vue几分钟就写完了为什么要这么麻烦”。这个质疑很合理但我个人认为这个简单版购物车坚持用原生JavaScript恰恰是它最值钱的地方。原因一降低理解门槛。如果直接上Vue或React你看到的会是template、v-model、computed这些封装好的API底层的数据变更和DOM更新过程全被隐藏了。而原生JS版本里每一次渲染都是你亲自调用的你会真正理解“页面是数据的投射”这句话的含义。原因二方便调试和移植。没有任何依赖意味着你不需要npm install不需要构建工具随便找个浏览器打开HTML文件就能跑。我之前收到过不少带着node_modules的“简单项目”一个购物车案例解压出来几百MB那才叫离谱。原因三容易进行二次改造。当你理解了原生实现后面想换成Vue、React或者把数据对接真实后端接口都能非常迅速地上手。因为业务逻辑不变变的只是“渲染”这一层。当然这个方案也有明显的短板手动管理DOM更新在大型项目中容易混乱页面复杂后性能会下降代码复用性差。这些缺点在你真正开发大型应用时才会暴露而那时你已经有一定的工程化经验知道该怎么用框架去解决这些问题。所以简单版的“朴素”不是缺陷而是一种刻意的教学取舍。3. 核心细节解析与实操要点从页面布局到事件绑定的关键环节3.1 页面结构设计列表渲染和底部结算栏怎么分工打开HTML文件你会发现页面结构分成了三个主要区域顶部标题栏、中间的商品列表容器、底部的结算操作栏。这种结构本身没什么稀奇但它在DOM布局上的处理是有讲究的。中间商品列表区域在JavaScript渲染前是一个空的容器节点真正的商品条目是运行时通过拼接HTML字符串或createElement动态生成并插入的。底部结算栏包含全选按钮、已选商品数量、合计金额和结算按钮这些数据会随列表变化一起更新。我在整理这个案例时最关注的是两个细节一是DOM结构不能写死。很多人写列表页会先在HTML里手动写好几条商品数据做占位后面再改成动态渲染。这种做法我强烈不推荐因为手动占位容易让你在写逻辑时不自觉地“迁就”静态结构的写法。正确做法是一开始就把列表容器留空所有条目一律由JavaScript生成这样后续增删改才不会出岔子。二是结算栏的位置。简单版案例通常会用fixed定位固定在页面底部这样无论商品多少结算按钮始终可见。这个交互体验是从真实电商App里学来的虽然代码简单但很值得保留。3.2 数据管理技巧如何用本地数组模拟真实后端购物车案例在不接后端的情况下数据全部存在前端变量里这没什么好说的。但有一个点需要特别留意数组里对象的引用关系。假如你有两份商品数据一份是全量商品列表比如用来展示“推荐商品”区域一份是购物车列表。加入购物车时直接把商品对象从商品列表push进购物车数组这会出现一个问题你在购物车里修改数量会同步修改商品列表里的原对象导致两个区域的显示都变掉。正确做法是加入购物车时创建一个新对象或者至少把基本字段拷贝一份function addToCart(product) { // 检查是否已存在 let existing cartData.items.find(item item.id product.id) if (existing) { existing.count } else { // 深拷贝一份避免引用共享导致的数据错乱 cartData.items.push({ id: product.id, name: product.name, price: product.price, count: 1, selected: true }) } saveCartData() renderCart() }这个坑在新手项目里出现频率极高我见过的购物车案例demo里至少三分之一有这个问题。写项目时一旦发现某个商品数据在页面多处同时变化第一时间检查是不是对象引用没切断。3.3 事件绑定方式为什么推荐用事件委托事件绑定是购物车案例里最容易写乱的地方。如果给每个加号、减号、删除按钮都单独绑定一个click事件代码会变成这样先遍历所有按钮一个一个addEventListener还要考虑后来新增的商品按钮没有事件绑定。这个问题在老式写法里几乎必现。解决办法是事件委托。把监听器绑定在列表容器或者document上利用事件冒泡机制通过事件源的class或data属性来区分点击的是什么按钮document.querySelector(.cart-list).addEventListener(click, function(e) { let target e.target if (target.classList.contains(btn-increase)) { let id Number(target.dataset.id) changeCount(id, 1) } else if (target.classList.contains(btn-decrease)) { let id Number(target.dataset.id) changeCount(id, -1) } else if (target.classList.contains(btn-remove)) { let id Number(target.dataset.id) removeItem(id) } })这样做的好处显而易见无论列表如何增删事件绑定始终有效不需要重复绑定代码结构也更清爽。我在解析这个简单版案例时发现几乎所有关键交互按钮都带有data-id属性这就是为了配合事件委托取id用的。4. 实操过程与核心功能实现从零手写一个能跑的购物车4.1 数据初始化与本地存储恢复先说整个流程的入口。页面加载后第一步从localStorage里读取上次保存的购物车数据如果读取不到就使用默认的示例数据初始化。这个逻辑保证了用户体验刷新页面后购物车状态不丢。function loadCartData() { let saved localStorage.getItem(simple_cart) if (saved) { try { cartData JSON.parse(saved) } catch (e) { console.error(本地数据解析失败使用默认数据) cartData getDefaultCartData() } } else { cartData getDefaultCartData() } }注意这里有一个JSON.parse的异常处理。我见过不少案例直接从localStorage取值然后parse一旦数据被破坏整个页面直接白屏。这个try-catch看着不起眼但在真实项目中特别重要。任何从外部获取的数据都不值得信任。4.2 数据保存与渲染函数的设计每次数据变化后要做的两件事保存到localStorage重新渲染页面。这两步通常合成一个函数来调用。function saveCartData() { localStorage.setItem(simple_cart, JSON.stringify(cartData)) }渲染函数则负责把cartData里的数据映射成HTML字符串再通过innerHTML插入到页面中。这里有一个性能认知需要掰扯清楚innerHTML整个替换列表在小数据量几十条以内的情况下性能完全不成问题完全没有必要去搞虚拟DOM那套。但如果你要做大量数据的表格那还是老老实实用createElement加DocumentFragment吧一次性插入避免频繁的DOM重排。合计金额和选中数量在渲染函数里一起计算。计算方式也很直观遍历购物车数据只累加selected为true的商品function calcTotal() { let total 0 let count 0 cartData.items.forEach(item { if (item.selected) { total item.price * item.count count item.count } }) cartData.totalPrice total cartData.selectedCount count return { total, count } }4.3 核心交互逐个拆解加减、删除、单选全选、结算加号和减号的逻辑是对称的。加号就是把对应商品的count加1减号则要判断如果count大于1就减1如果count等于1可以有两种策略——要么直接删除该商品要么把按钮禁用。我看到的这个简单版案例选择的是减到1后继续点减少会删除该商品这个交互也符合不少电商App的实际做法。删除功能最简单用filter方法过滤掉对应的id即可。但删除前最好弹一个确认框不然用户误触之后只能重新添加——虽然这个案例里没有我建议你自己加上。单选全选这块要花点心思。单个商品的selected状态在点击复选框时翻转全选按钮的状态则根据“所有商品是否都被选中”来判断。注意一个逻辑陷阱如果购物车里一件商品都没有全选按钮应该是什么状态我见过不少案例在这里处理不当空列表时全选按钮还显示为选中状态用户再点一下反而把所有不存在的商品都“取消选中”了看起来非常奇怪。正确做法是空列表时全选按钮取消选中并禁用或者至少置为未选中状态。结算按钮的逻辑最简单也最容易忽略边界情况。如果选中的商品数量为0点击结算应该给出提示而不是直接弹出“结算成功”。这个案例里虽然只是一个alert但这种边界判断思维要锻炼出来。5. 常见问题速查表与排查技巧实录5.1 高频问题与处理方案我在复现和检查这类购物车案例时整理了一份高频问题清单做成了表格方便你对症下药。问题现象大概率原因处理办法刷新后购物车数据丢失没有调用localStorage.setItem或调用时机不对检查每次数据变更后是否都调用了保存函数页面报错Cannot read property xxx of undefined从localStorage解析出的数据为空或数据结构与预期不一致给parse包try-catch并用Array.isArray校验点加号没反应事件绑定写在渲染函数里重复绑定导致混乱或按钮没有data-id属性改用事件委托绑定在容器上全选按钮状态不对全选判断逻辑写错空列表时判断条件不成立增加空列表判断主动设置全选按钮为未选中合计金额计算错误没有只计算选中的商品把未选中的也算进去了遍历时加selected条件判断修改数量后页面不更新改了数据但没有调用渲染函数统一封装一个update()内部先保存再渲染商品列表出现重复数据加入购物车时直接push了已有商品而未检查加入前用find查找存在则调整数量5.2 一个值得重点提醒的坑渲染函数里嵌套模板字符串的转义问题不少人在写渲染函数时会在HTML字符串里嵌套比较复杂的模板字符串一旦某个字段包含特殊字符整个页面渲染就崩了。比如商品名称里有单引号或双引号直接拼进HTML字符串可能导致DOM解析错乱。我在实际调试时就遇到过商品名称里带着符号渲染后变成一个奇怪的实体字符。处理办法是写一个简单的escapeHtml函数把特殊字符转义一下function escapeHtml(str) { let div document.createElement(div) div.appendChild(document.createTextNode(str)) return div.innerHTML }虽然这个简单版案例里的商品名称是写死的没有这种风险但你后面接真实数据时一定会遇到。提早养成转义习惯能省下不少调试时间。5.3 排查思路分享从现象倒推数据流遇到页面表现和预期不一致时我的排查顺序固定是先打开控制台打印cartData看数据本身是否正确。数据正确则问题在渲染层数据错误则问题在事件处理层。这个方法说起来简单但能解决90%以上的购物车逻辑bug。比如点减号后页面上的数量没变我会先确认cartData里count值变没变。如果变了说明事件处理逻辑没问题问题出在渲染函数没有重新生成对应的DOM节点。如果count值没变说明事件没有正确触发或者触发后找到了错误的商品id这时候就要回头检查data-id的取值。这个“先数据后呈现”的排查思路是我认为这个案例能教给你的最重要的一项调试能力。它帮助你建立一种有序的问题解决方式而不是东点一下西点一下瞎试。6. 简单版购物车的三个实用扩展方向看懂了基础案例之后如果你觉得不过瘾或者想拿它做课程设计、作品集我建议你往这三个方向扩展性价比最高。一是对接真实后端接口。把localStorage替换成axios请求把增删改查映射成后端API调用。这一步做完你的购物车就从纯前端Demo变成了真正能上线的完整功能模块。二是增加商品推荐模块。在购物车页面的商品列表下方展示一组“猜你喜欢”的商品点击可以加入购物车。这就要用到前文提到的对象拷贝注意事项两套数据必须避免引用共享。三是加上库存校验。每个商品增加stock字段加入购物车时判断数量是否超过库存超过则提示。这个逻辑不用写太多代码但对业务理解很有帮助尤其是你后面做管理后台时库存这个概念会一直伴随你。这个项目虽然叫“简单版”但它覆盖了前端开发中最常见的思考路径数据从哪里来、数据怎么变、数据变了页面怎么变。把这条路径练成本能再看框架源码或文档你会发现自己突然就豁然开朗了。本文还有配套的精品资源点击获取