
简介小米商城商品加入购物车js代码是一份聚焦电商前端交互的轻量示例资源适合Web前端初学者、电商页面开发者以及想借鉴名站交互效果的从业者。它通过一个完整的HTML页面与配套商品图片演示了用户点击“加入购物车”按钮后商品信息如何被捕获、暂存并反馈到界面。资源包共5个文件包括1个index.html主页面和4张jpg商品图压缩后仅30KB结构简洁便于直接运行和阅读。已有1823人学习下载。代码涉及addEventListener事件绑定、通过属性或对象读取商品ID/名称/价格、利用数组维护购物车状态以及操作DOM更新购物车数量或提示信息同时可扩展淡入淡出、滑动等反馈动画对理解现代电商前端基础技能很有帮助。这份资源既可作为课堂演示也可作为前端交互的练手素材帮助开发者快速掌握动态购物车的基础实现思路。1. 名站特效拆解为什么一个小小的加购动画值这么多讲究打开小米商城的商品列表页点击“加入购物车”商品缩略图会以一条抛物线飞进右下角的购物车图标角标数字随之跳动。这个看起来只是“好看”的动效背后涉及事件委托、DOM 定位、贝塞尔曲线插值、状态同步四块基础能力。对于想进阶的前端开发者来说与其去啃复杂框架不如先把这个小场景彻底拆开它体积小、边界清晰但把 JavaScript 在浏览器里最常打交道的几件事——监听、取数、渲染、动画——全部覆盖了。本文要拆的这份“小米商城商品加入购物车 js 代码”是一个完整的静态 demo一个 index.html 配上四张商品图没有任何框架依赖打开就能跑。它适合刚学完 JS 语法、想看看真实交互是怎么串起来的初级开发者也适合需要快速给后台管理系统或活动页加一个加购反馈动效的熟手。下面从页面结构、事件绑定、状态管理到动画实现一层层拆开说清楚。2. 先看骨架商品卡片与购物车区域的 HTML 结构与选型这份资源只有一个 index.html所有结构、样式、逻辑都写在一个文件里。打开后能看到四个商品卡片对应的图片分别是 img01.jpg 到 img04.jpg右下角是一个固定的购物车图标区域。整体布局是典型的电商列表页左侧商品网格区右侧底部悬浮购物车入口。2.1 商品卡片的最小结构四个商品的 DOM 结构完全一致只是图片、名称、价格不同。这是刻意为之的便于用 JavaScript 统一遍历绑定也便于后面用事件委托做优化。每个卡片的模板大致如下div classgoods-item>div classcart-wrapper idcartTarget span classcart-icon/span span classcart-badge idcartBadge0/span /divcartTarget是飞入动画的目标锚点cartBadge是显示数量的角标。这两个元素在动画中会被频繁读写单独设置 ID 便于用getElementById快速获取避免在动画循环里做低效的 DOM 查询。实际项目中如果购物车不止一个入口可以把目标元素改成类名加querySelector但这份 demo 出于简化只保留了一个。2.3 样式上埋的伏笔飞入元素为什么用固定定位看 CSS 的时候会发现里面定义了一个.fly-item类初始是display: none。这个类不会出现在任何商品卡片上它是给动态创建的飞入动画元素用的。飞入元素在动画期间要脱离文档流、从起点移动到终点所以用了position: fixed配合left、top直接控制屏幕坐标不受父级transform或overflow: hidden影响。这是一个很容易踩坑的细节如果动态元素的父容器有filter、transform或perspective属性fixed定位会变成相对于父容器而不是视口坐标就会算错。3. 事件监听与信息提取从点击按钮到拿到商品数据结构搭好之后接下来的核心问题是用户点了“加入购物车”代码怎么知道点的是哪一个商品有两条路给每个按钮单独绑定事件或者利用事件冒泡做事件委托。这份代码选择的是后者这也是大型电商页面更常见的做法。3.1 事件委托只绑一次监听处理所有按钮按传统写法每个按钮都要addEventListener一次四个按钮绑四次四十个按钮绑四十次。事件委托的思路是监听所有商品卡片的共同父容器利用事件冒泡机制在事件对象上判断当前点击的是不是目标元素。document.querySelector(.goods-list).addEventListener(click, function(e) { const target e.target.closest(.add-cart-btn); if (!target) return; const item target.closest(.goods-item); const id item.dataset.id; const name item.dataset.name; const price parseFloat(item.dataset.price); addToCart({ id, name, price }); });这段代码有三层逻辑。e.target.closest(.add-cart-btn)解决的是“点到按钮里的文字或 padding 区域”的情况closest会向上查找最近的匹配选择器的祖先元素只要点在按钮内部无论精确命中哪个子节点都能找到按钮本身。如果点击的是按钮以外的区域closest 返回 null直接跳出。找到了按钮之后再向上找.goods-item拿到商品所在卡片再从dataset里取 ID、名称、价格。价格用parseFloat转成数字类型因为在>let cart []; function addToCart(goods) { const existed cart.find(item item.id goods.id); if (existed) { existed.count 1; } else { cart.push({ id: goods.id, name: goods.name, price: goods.price, count: 1 }); } updateCartBadge(); }find方法按 id 查找购物车里是否已有同款商品。找到则 count 加一没找到则追加一个包含 count 字段的新对象。这里有一个大多数初学 JS 的人会犯的错误直接把goods对象 push 进去而不是创建新对象。如果直接在原对象上挂 count 字段下一次点击同一个商品时dataset里的数据已经被污染而且多次添加之间会互相影响。正确做法是把值拷贝进新对象保住原始数据的干净。4.2 角标数字是商品件数还是商品种类数updateCartBadge函数负责把购物车数量同步到页面上的角标。这里要做一个产品决策角标显示的是“加入了几件商品”还是“加了几种商品”。小米商城实际显示的是件数也就是上面代码里所有商品 count 字段的总和function updateCartBadge() { const total cart.reduce((sum, item) sum item.count, 0); document.getElementById(cartBadge).textContent total; }reduce在这里的作用是把数组里每个商品的 count 累加成总量。如果用cart.length拿到的是种类数重复加同一商品时角标不会变用户体验上不够直观。很多面试题会拿这个做文章给你一个购物车数组怎么算总件数和总种类数。前者是 reduce 累加 count后者是直接取 length。两种逻辑在真实场景里都有业务含义电商页面两种数字甚至可能同时出现在不同位置——购物车角标显示件数结算页列出商品行数。4.3 用 localStorage 做持久化的可选增强原 demo 没有做刷新后保持购物车状态的功能属于可接受的简化。生产环境的购物车状态要么在后端要么在 localStorage纯前端内存数组刷新即失。如果想在这个 demo 上做扩展可以在每次购物车变化后执行一次序列化存储function saveCart() { localStorage.setItem(mi-cart, JSON.stringify(cart)); } function loadCart() { const saved localStorage.getItem(mi-cart); if (saved) { cart JSON.parse(saved); updateCartBadge(); } } loadCart();一个容易踩的坑是 JSON.parse 的容错。如果 localStorage 里的数据被用户手动改坏或者上一个版本的存储结构不兼容JSON.parse会直接抛异常导致整个页面脚本崩溃。稳妥写法是包一层 try-catch解析失败时清空旧数据、重置为空数组。页面上没有明显异常时刷新后购物车空了问题往往出在这里而不是 JS 逻辑。5. 飞入动画与坐标计算的完整实现加购反馈的视觉核心是商品图飞入购物车。这个效果的实现路径是动态创建一个固定定位的图片副本从起点移动到终点到达后移除。实现链路里最难的是坐标计算和动画插值这两块都强依赖浏览器渲染机制。5.1 获取起点与终点坐标getBoundingClientRect 的正确用法起点是商品缩略图的位置终点是#cartTarget的位置。需要的是相对于视口的坐标而不是相对于文档的坐标因为飞入元素用的是position: fixed它的 left 和 top 天然相对于视口。function createFlyElement(imgSrc, startEl, endEl) { const startRect startEl.getBoundingClientRect(); const endRect endEl.getBoundingClientRect(); const flyImg new Image(); flyImg.src imgSrc; flyImg.className fly-item; flyImg.style.left startRect.left px; flyImg.style.top startRect.top px; flyImg.style.width startRect.width px; flyImg.style.height startRect.height px; document.body.appendChild(flyImg); const endX endRect.left endRect.width / 2 - startRect.width / 2; const endY endRect.top endRect.height / 2 - startRect.height / 2; animateFly(flyImg, startRect.left, startRect.top, endX, endY); }这里有一个细节终点坐标并不是endRect.left和endRect.top而是购物车中心点减去飞入元素自身宽度和高度的一半否则图片到达终点时会偏到购物车右下角。起点不用做这种处理因为图片就是从原商品位置原地复制出来的起始坐标直接沿用原图的位置和尺寸即可。还有一个微妙的时序问题如果页面发生了滚动getBoundingClientRect拿到的坐标会随之变化所以必须在创建飞入元素的同一帧内读取坐标和设置初始位置不能先读取坐标再等图片加载完再设置位置中间一旦滚动坐标就错位了。图片加载通常走缓存但严谨的写法是在onload之后再启动动画或者直接用已有的 DOM 节点而不是新建 Image。5.2 贝塞尔曲线与 requestAnimationFrame 动画推进直线飞入的效果太平淡小米商城用的是先上抛后下落的弧线效果。实现弧线的常见做法是使用二次贝塞尔曲线把运动轨迹拆成 X 轴和 Y 轴两条路径X 轴做匀速或缓动插值Y 轴先减速上升再加速下降。下面是简化后的核心动画循环let startTime null; const DURATION 600; const startX startLeft; const startY startTop; const controlX (startX endX) / 2; const controlY Math.min(startY, endY) - 120; function animateFly(el, sx, sy, ex, ey) { const bezierX (t) (1 - t) * (1 - t) * sx 2 * (1 - t) * t * controlX t * t * ex; const bezierY (t) (1 - t) * (1 - t) * sy 2 * (1 - t) * t * controlY t * t * ey; function step(timestamp) { if (!startTime) startTime timestamp; const progress Math.min((timestamp - startTime) / DURATION, 1); el.style.left bezierX(progress) px; el.style.top bezierY(progress) px; if (progress 1) { requestAnimationFrame(step); } else { el.remove(); updateCartBadge(); } } requestAnimationFrame(step); }progress被限制在 0 到 1 之间代表动画从开始到结束的进度乘以时间不会超过设定的 600 毫秒。贝塞尔公式里的controlY决定了弧线上抛的高度数值越小弧顶越高如果设成比起点和终点都大的值弧线会变成先下沉再上浮视觉上像一个“U”型。controlX取中点意味着水平方向上没有额外偏移轨迹是一条平滑的抛物线。整个动画不用 CSStransition因为top和left的 transition 只能做线性或预设的缓动曲线无法表达贝塞尔路径这样 X 和 Y 独立变化的轨迹而且连续修改left、top在动画期间不会触发页面布局重排只要不读取布局属性性能是安全的。5.3 动画结束与角标刷新的衔接时机动画代码里el.remove()和updateCartBadge()放在同一个分支里这个顺序有讲究先移除飞入元素再更新角标。反过来先更新角标再移除元素用户会看到角标已经跳了数字但飞入的图片还悬在半空视觉上不同步。如果购物车旁边有弹出的小气泡提示“加入成功”也建议在同一个回调里触发不要另起 setTimeout避免时间差造成闪烁。对于动画性能还有两个边界情况值得注意连续快速点击多个商品时会同时存在多个飞入元素每个元素各自持有一份requestAnimationFrame循环元素数量多、帧率充足时没有问题但如果同一个商品连点了二十次DOM 里会出现二十张飞行的图片。生产环境合理的约束是同一商品的飞入动画做节流1 秒内重复点击不再创建新元素更新角标即可。用时间戳判断就能实现不需要引入 lodash 的 throttlelet lastFlyTime 0; function handleFly(goods, imgSrc, btn) { const now Date.now(); if (now - lastFlyTime 800) return; lastFlyTime now; createFlyElement(imgSrc, btn, document.getElementById(cartTarget)); }这里的800是节流窗口小于动画时长。如果节流窗口大于动画时长动画还没飞完就点了第二次第二次点击会直接丢弃用户感知不到任何反馈所以节流时间应略小于动画时长。5.4 验证动效正确性的三个检查点写完这份代码后用浏览器的开发者工具可以做三次验证。第一次点击商品加入购物车后切换到 Elements 面板确认.fly-item元素被创建又移除整个生命周期大约 600 毫秒。第二次在控制台输入cart确认数组结构和预期一致重复添加同一商品时 count 是在累加而不是新增记录。第三次手动把页面往下滚一段距离再点加购观察飞入元素的起始位置是否与商品图当前位置重合——如果滚动后起始位置偏移说明坐标读取时机有问题常见原因是读取坐标的时机在图片延迟加载完成之前。验证坐标还有一个更便捷的技巧在createFlyElement里临时给飞入图片加一个高亮边框和background: rgba(255,0,0,0.3)这样每个飞入元素的轨迹会非常直观检查起点是否贴合商品缩略图、终点是否落在购物车中心附近。排查完确认后再去掉调试样式即可。本文还有配套的精品资源点击获取