原生JavaScript手写购物车:数据驱动视图与事件委托实践
2026/9/8 7:16:10 网站建设 项目流程

简介:针对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字段,加入购物车时判断数量是否超过库存,超过则提示。这个逻辑不用写太多代码,但对业务理解很有帮助,尤其是你后面做管理后台时,库存这个概念会一直伴随你。

这个项目虽然叫“简单版”,但它覆盖了前端开发中最常见的思考路径:数据从哪里来、数据怎么变、数据变了页面怎么变。把这条路径练成本能,再看框架源码或文档,你会发现自己突然就豁然开朗了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询