
简介这是一份仿小米商城商品列表页的加入购物车特效代码适合前端初学者、电商页面设计者以及对交互感兴趣的开发者用来快速掌握商品点击后被动态加入购物车的完整流程。资源共五个文件包含一个 HTML 页面和四张商品示意图压缩包大小约 30KB结构精简无需依赖任何框架即可直接打开运行。整个页面由一个 HTML 文件承载主要逻辑四张商品图作为展示素材便于提取后对照学习与二次改造。代码重点演示了事件监听、商品信息读取、购物车数组更新、页面角标同步等关键步骤配合图片素材可清晰还原商品卡片与加购反馈的视觉效果。已有 1823 人浏览学习可用于课堂作业、原型演示或作为原生 JavaScript 与 DOM 操作实践的参考样例并可在实际项目中直接借鉴其交互思路。1. 小米商城加入购物车本质是一条可复现的异步请求在小米商城的商品详情页点击“加入购物车”浏览器做的不是刷新页面而是向后端发一条异步请求把当前规格和数量写进购物车数据。做前端自动化、接口调试或电商测试的人经常会遇到“不点按钮能不能直接用 js 代码完成加入购物车”的需求批量整理购物车、给测试账号预置购物车数据、或者监控某款商品的加购状态都需要把这条请求从点击事件里剥离出来。这件事真正的难点不在写 fetch而在于定位真实接口、复刻请求头、维持登录态。下面从 DevTools 抓包开始讲一条能落地的路径先拿到真实请求再在页面上下文复刻最后移植成 Node.js 脚本。2. 用 DevTools 抓出小米商城加入购物车的真实接口2.1 抓包前先登录并开启 Preserve log避免请求丢失加入购物车接口要求登录态未登录状态下点击按钮会先跳登录页真正的加购请求根本不会发出去。所以抓包前先正常登录一次并清空购物车里已有的商品让后面的验证结果更干净。打开 DevTools 的 Network 面板后第一件事是勾选 Preserve log保留日志。小米商城详情页加入购物车成功后购物车角标等区域会做局部更新而一旦发生前端框架内部的路由切换未勾选保留日志时请求列表会被清空再回找那条 POST 就难了。过滤栏建议先输入 cart 或 addNetwork 会把 URL 路径里包含关键字的请求全部筛出来缩小查找范围。准备好之后回到商品页选好规格点击加入购物车按钮。2.2 从 Network 面板筛选请求按方法和 payload 认出加入购物车点击按钮后Fetch/XHR 过滤条件下的请求列表一般会新增 3 到 5 条记录其中只有一条是加入购物车本身。判断依据按下表逐项核对判断特征命中标准请求方法必须是 POST加购是写操作GET 只是读数据URL 路径路径包含 cart、add 或类似语义片段域名是 mi.com 或其 api 子域Payload 内容能找到 skuId、count、productId 这类业务参数响应格式返回 JSON而不是整段 HTML 或跳转地址常见的干扰项是埋点上报和统计类请求它的 URL 也可能带 cart 字样但 payload 是日志串。点开每条请求看 Payload 标签页能对上你刚选的规格 ID 的那条就是加购接口。提示接口路径和参数名会随小米商城前端版本变化后续都以你实际抓到的为准不要照抄任何文章里写的固定 URL。另一个常见干扰项是按规格查库存的请求它也包含 sku 参数。区分方式很直接加购请求的 Payload 里有 count库存查询的响应里才有 stock看请求体而不是响应体。2.3 拆解加入购物车请求体参数skuId、count 与校验字段选中那条加购请求切到 Payload 标签页多半是 Key-Value 形式的表单数据或 JSON 字符串。关键参数按作用分三类商品定位参数、数量参数、风控与埋点参数。参数名以抓到的为准作用是否随请求变化skuId / sku定位具体规格颜色、版本、套餐的区分都在这个 ID 上不变productId / itemId商品 ID部分接口版本可选通过 skuId 可反查不变count加购数量会带入购物车和结算页随需求变sid / trackid / 签名 token 类参数来源跟踪或风控签名缺失时可能被拦截频繁变化对脚本来说skuId 和 count 是业务参数变化有规律校验类参数是麻烦的来源。如果直接复刻请求返回风险校验码优先检查是不是漏了签名参数。常见做法是不手工维护这类参数而是从页面初始数据里整体提取这个思路在后面的页面数据读取部分展开。2.4 Copy as cURL 导出请求头和 body作为 js 代码的还原依据找到加购请求后不要急着手抄请求头。在请求记录上右键选择 Copy → Copy as cURL会得到一条完整的 curl 命令URL、Cookie、Headers、Data 全部按原样打包好了。这是后续还原脚本最可靠的原始材料比对着 Network 面板手抄准确得多尤其是 Cookie 这种长字符串。复制出来的命令大致长这样curl https://www.mi.com/buy/cart/add \ -H authority: www.mi.com \ -H cookie: 一串很长的登录态cookie \ -H referer: https://www.mi.com/buy/detail?skuidxxxxcidxxxx \ -H content-type: application/x-www-form-urlencoded; charsetUTF-8 \ --data-raw skuIdxxxxcount1不同浏览器导出的 curl 在引号和大小写上略有差异不影响语义。把这条命令保存下来后面移植脚本时会用到现在只需要确认命令里有多条 -H 开头的请求头和一条 --data-raw 开头的请求体材料就完整了。如果发现 curl 里缺 Cookie重新登录一次再复制。3. 在商品页控制台用 fetch 复刻加入购物车的 js 代码3.1 先写浏览器版本的原因同源、免 CORS、cookie 自动携带拿到请求信息后第一版 js 代码建议直接在商品详情页的 DevTools Console 里跑而不是立刻写 Node.js。原因有三个第一页面上下文与接口同源不受 CORS 跨域限制fetch 发出去不会被预检拦截第二浏览器会自动携带当前域名的 Cookie登录态这一项免费解决第三调试成本低改完一段代码直接回车就能验证不用在 Node 侧反复处理会话问题。这段代码的定位是验证“接口可复现”。只要它能在控制台里把商品加进购物车就证明加购流程能被脚本调用接下来才需要处理会话保持和环境迁移。很多人在这一步直接用 Postman 或 Node 跑一上来就被 CORS 或 Cookie 问题卡住反而分不清是接口访问不了还是代码有 bug。3.2 fetch 版加入购物车最小代码参数怎么拼、header 怎么设以下代码在商品详情页控制台执行执行前确保已登录并把 ADD_CART_URL 换成像前面 curl 里那种实际抓到的 URL// 加入购物车接口地址以自己抓到的为准可以用相对路径 const ADD_CART_URL /buy/cart/add; async function addToCart({ skuId, count 1 }) { // URLSearchParams 把对象拼成 x-www-form-urlencoded 格式 const body new URLSearchParams({ skuId, // 规格 ID必须与页面当前规格一致 count, // 加购数量默认 1 }); const response await fetch(ADD_CART_URL, { method: POST, credentials: include, // 显式声明携带 cookie headers: { Content-Type: application/x-www-form-urlencoded; charsetUTF-8, X-Requested-With: XMLHttpRequest, // 声明这是一个 ajax 请求 }, body: body.toString(), }); if (!response.ok) { throw new Error(HTTP ${response.status}); } return response.json(); }这里有几个参数值得细说。URLSearchParams 会把对象序列化成 skuIdxxcount1 这样的字符串正好匹配 content-type 里声明的表单格式如果 content-type 改成 application/jsonbody 就得用 JSON.stringify两者混搭会导致后端取不到参数。credentials: include 在跨域场景是必需项同源时加上也没有副作用后续把代码搬到别的域名下不会忘记补这一项。X-Requested-With 是很多电商后端识别 ajax 请求的约定字段个别版本缺失时会直接拒绝。3.3 响应校验与购物车数量更新解析 code 和 cartCount调用完成后不要只看有没有报错要解析响应体里的业务码。电商接口通常返回 JSON结构大致是 { code: 0, message: success, data: { ... } }code 为 0 代表加购成功非 0 则是业务层拦截未登录、限购、规格失效等。const result await addToCart({ skuId: 你的skuId, count: 1 }); console.log(加购结果, result); if (result.code 0) { // 部分版本会把购物车总数放在 data.cartCount 里 const cartNum document.querySelector(.cart-num); if (cartNum result.data result.data.cartCount) { cartNum.textContent result.data.cartCount; } } else { console.warn(加购失败, result.message || result.code); }响应字段的语义以抓包看到的实际响应为准上面列的是最常见形态。验证是否真的加购成功最直接的办法是刷新页面看购物车角标数字有没有变化不要只看 console 有没有打印成功。3.4 从页面初始数据自动读取 skuId避免手写死参数手动在代码里写死 skuId 有两个问题换一个商品就得改一次代码页面规格切换后旧 skuId 可能已经不在可加购列表里。电商页面通常把商品初始数据挂在 window 上的某个全局对象里比如MI_INITIAL_STATE。提取来源适用情况取值示例window 上的全局状态对象SSR 直出的详情页数据完整state.product.currentSkuId页面元素>function getCurrentSkuId() { // 优先从全局初始状态里取 const state window.__MI_INITIAL_STATE__ || {}; if (state.product) { return state.product.currentSkuId || state.product.defaultSkuId; } // 兜底从页面上带>const CART_ADD_URL https://www.mi.com/buy/cart/add; const headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.mi.com/buy/detail?skuidxxxx, Origin: https://www.mi.com, Content-Type: application/x-www-form-urlencoded; charsetUTF-8, Cookie: process.env.MI_COOKIE, // 从环境变量读别写死在代码里 }; async function addToCart(skuId, count 1) { const res await fetch(CART_ADD_URL, { method: POST, headers, body: skuId${skuId}count${count}, }); return res.json(); }这里把 Cookie 从代码里剥离出来放入环境变量是为了避免把登录态提交进 Git多人协作或定时任务场景下换账号只需要更新环境变量不需要改代码。4.2 用 cookie 管理组件维持登录会话不维护写死的 Cookie 串curl 里的 Cookie 不会一直有效过期后接口会返回未登录业务码。如果脚本只需要短时间运行把 Cookie 做成配置项就够了如果要长期跑常见做法是用 CookieJar 管理会话让每个请求自动携带 Cookie也方便对接后续的登录刷新流程。axios 生态里配合 tough-cookie 和 axios-cookiejar-support 是最常见组合const axios require(axios); const { CookieJar } require(tough-cookie); const { wrapper } require(axios-cookiejar-support); const jar new CookieJar(); // wrapper 让 axios 实例支持 jar 选项自动读写 set-cookie const client wrapper(axios.create({ baseURL: https://www.mi.com, headers: { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.mi.com/buy/detail, Origin: https://www.mi.com, }, jar, withCredentials: true, })); // 首次可以从环境变量注入登录态 cookie jar.setCookieSync(process.env.MI_COOKIE, https://www.mi.com);与 4.1 直接塞 headers 的做法相比CookieJar 的好处是响应里的 set-cookie 会被自动记录到 jar后续加购、查询请求携带的都是最新 Cookie不需要反复手动更新。如果是多账号并行给每个账号建一个独立的 client 实例即可。4.3 小米商城必填请求头清单与缺失报错对照Node 环境没有浏览器帮忙补请求头少任何一个都可能让接口拒绝服务。下表按调试时常见的缺失表现整理具体以接口返回为准Header作用缺失时常见表现User-Agent声明客户端类型403 或触发人机校验Referer标识来源页面业务码报非法来源或跳转Origin跨域来源校验403服务端安全中间件直接拦Content-Type声明请求体格式参数解析为空报参数缺失Cookie登录态载体业务码提示未登录或需要验证调试顺序有经验可循先从 403 这类传输层错误入手检查 User-Agent 和 Origin再处理业务码错误此时多半是 Cookie 失效或 Referer 不对。提示调试时每次只改一个请求头改完重跑一次避免多个变量同时变化时定位不到真正原因。4.4 给 Node 版加上限速与退避重试降低风控概率脚本脱离浏览器运行后请求特征的集中度比真实用户高频繁请求容易触发风控。做两件事可以把风险降下来请求间隔限速、失败退避重试。const sleep (ms) new Promise((resolve) setTimeout(resolve, ms)); async function addToCartWithRetry(skuId, count 1, retries 3) { for (let attempt 0; attempt retries; attempt) { try { const { data } await client.post(/buy/cart/add, { skuId, count, }); if (data.code 0) return data; // 业务码明确的失败如限购、未登录不需要重试 throw new Error(business error: ${data.code} ${data.message || }); } catch (err) { if (attempt retries - 1) throw err; const waitMs 500 * (attempt 1); // 500ms、1000ms、1500ms 递增 console.warn(第 ${attempt 1} 次失败${waitMs}ms 后重试, err.message); await sleep(waitMs); } } } // 批量加购时每次请求之间至少间隔 1.2s for (const sku of skuList) { await addToCartWithRetry(sku.skuId, sku.count); await sleep(1200); }重试逻辑要区分场景网络超时和 5xx 适合重试业务码非 0 的失败多半是参数或登录态问题重试也白费直接抛出来让人处理。限速间隔按任务紧迫程度调整定时任务建议放宽到 1.5 秒以上。这类脚本建议只用于自己的账号和正常测试场景高频请求会影响其他用户正常使用也会让账号进入风控名单。5. 脚本长期可用的关键加入购物车前先做入参自检5.1 登录过期、skuId 失效、数量超限三类失败的前置拦截脚本跑一段时间后最常见的三类失败基本固定登录态过期、skuId 对应的商品已下架、count 超过限购数量。这三类问题的共同点是加购请求发出前就能判断出来没必要等接口报错再排查。登录过期用轻量的用户信息接口探测该接口响应非 0 说明 Cookie 已失效。skuId 失效时SKU 校验或详情接口会返回商品不存在加购前查一次就能拦截。数量超限更容易判断详情页或列表页会标注限购数量脚本里维护一份商品级限购配置即可。三项都过了再发加购请求日志会干净很多排查问题时不用翻几十条失败记录去定位同一类原因。5.2 用自检函数把失败拦截在网络请求之前自检逻辑收敛成一个 preflight 函数加购前统一调用。下面代码里的接口路径是示例换成你实际抓到的对应接口即可。async function preflight(skuId, count) { // 1. 登录态检查调一个轻量的用户信息接口 const me await client.get(/user/info); if (me.data.code ! 0) { return { ok: false, reason: login_expired, detail: 登录态失效请更新 Cookie }; } // 2. SKU 有效性检查商品下架或规格删除时详情接口会报错 const skuInfo await client.get(/product/sku/detail?skuId${skuId}); if (skuInfo.data.code ! 0) { return { ok: false, reason: sku_invalid, detail: skuId ${skuId} 不存在或已下架 }; } // 3. 数量上限检查超过限购数量直接拦截 const limit skuInfo.data.data?.purchaseLimit || 99; if (count limit) { return { ok: false, reason: count_over_limit, detail: 限购 ${limit} 件 }; } return { ok: true }; } const check await preflight(skuId, count); if (!check.ok) { console.error([拦截] ${check.reason}: ${check.detail}); return; } const result await addToCartWithRetry(skuId, count); console.log(加购成功购物车数量, result.data?.cartCount);自检的目的不是省一次请求而是让失败原因可解释、可告警。把 reason 字段作为统计 key能很快看出哪类问题占比最高登录失效就去刷新 CookieskuId 失效就去同步商品列表数量超限就调整任务参数。配合前面章节的限速和退避逻辑这套自检加重试的组合就是一个加购脚本长期不维护也能稳定运行的底线配置。本文还有配套的精品资源点击获取