跳转到内容

从 v0.2 升级到 v0.3

v0.3(版本代号 030)已把购物车模型统一为 Storefront API 的 Cart 结构:默认 adapter、自定义 adapter 以及 onCartChange 回调接收和返回的均为同一套数据结构。v0.2(版本代号 024)基于旧版 Ajax Cart,存量代码需在后台切换店铺版本前完成迁移。

v0.3 不保留旧方法名的兼容别名。按下表替换即可,没有兼容层。

先评估你的改动范围:如果你一直使用默认 adapter,且 onCartChange 仅触发站点自身的只读刷新函数、不直接读取回调参数,则可保留这两项接入方式;同时把 init 收敛为每次页面加载只调用一次,按下表迁移 PP/SP 方法,并把站点写车后的 bindCart 替换为 syncCart。若实现了自定义 adapter 或在回调中读取了 cart 参数,请按本文完成全部四个迁移步骤。

变更项v0.2(024)v0.3(030)
购物车数据结构Ajax CartStorefront Cart
购物车 IDcart.tokencart.id
购物车行列表cart.itemscart.lines.nodes
购物车行标识item.keyline.id(即 CartLine.id)
新增行传入的规格标识merchandiseId 字符串(内部调用可传数字 variantId,由 SDK 归一化)merchandiseId(完整的 ProductVariant GID,发请求前校验)
行属性格式item.propertiesline.attributes({ key, value }[])
金额格式以「主货币单位 × 100」为单位的整数(与币种小数位无关)MoneyV2(十进制字符串 amount + currencyCode)
更新行方法changeLineupdateLines(cartId, lines)
写方法返回值AjaxCart | null(updateAttributes 允许 void)操作后的完整 Cart;缺失或分页不全即拒绝
购物车同步入口bindCart(id) + updateWidget()/updateSP()/updatePP()syncCart(cartId?)——首次绑定、换车、同车刷新都用它
PP 商品报价需额外传入 productIdloadPlans(variantId, variantDetail?),返回 { context, options };loadPlans(null) 返回 null
PP 商品页加购getSelectedPlan() → 写主商品 → bindCart() → addPP({ variantId, quantity })pp.addToCart({ context?, addMainProduct }) 一次完成;回调为 (attributes, context) => { cartId, mainLineId }
PP 核心原语多个分散入口canBePopup / getSelectedPlan / setSelectedPlan / showModal,以及按主商品行写入的 addProtection({ mainLineId, selection })
PP 购物车候选getCartOffers(): Promise<PPCartOffer[]>getCartQuotes(): { enabled, quotes };无候选时为 { enabled: true, quotes: [] }
PP 购物车补购addFromCart({ lineKey, variantId }) 直接写入offerProtection({ mainLineId }) 弹窗确认后写入;addProtection 接收站点自绘列表的显式 selection
删除 PP 保障行removePP({ mainLineKey }) / 按保障行删除统一收敛为 removeProtection({ mainLineId }),返回 { status: 'removed' | 'already_unprotected' }
PP 候选与关联行标识lineKey / ppLineKeylineId / mainLineId / protectionLineId(取消保障传 mainLineId)
SP 保障说明弹窗showModal()showModal()(同名保留;打开不改开关,弹窗内的保障按钮按模式表达意愿,checkout+ 不展示该按钮)
SP 费用行写入addSpFee() / removeSpFee() / declineSpFee()同名保留:addSpFee() / removeSpFee() / declineSpFee()(站点自绘开关时直接调用)
SP 结账前确认resolveSP() + 站点双按钮分支resolveSP();checkout+ 的两个按钮分别调用 addSpFee() / declineSpFee()
初始绑定购物车必须单独调用 bindCart可直接在 init({ cartId }) 中传入
站点修改同一购物车后再次调用 bindCart(cart.id)统一调用 syncCart(cart.id)
重复调用 init会覆盖更新会话配置重复 init 一律拒绝:进行中为 SDK_INIT_IN_PROGRESS,成功后、环境失败后、首次同步失败后或被在途操作拒绝后为 SDK_ALREADY_INITIALIZED(phase=ready / environment_failed / sync_failed / busy);首次绑车、刷新与换车统一调用 syncCart(cartId),晚到的 cartId 用 syncCart(cartId) 绑定
币种校验无统一守卫购物车总价币种必须等于初始化时的 currency,默认与自定义 adapter 规则一致
SDK 写车完成通知onCartChange(cart?: AjaxCart),参数可能为空onCartChange(cart) 只通知已确认的购物车事实,不表示业务成功或同步完成;业务结果以对应 API 的 Promise 为准

已移除或重命名的方法,可对照下表在代码库中全局搜索替换:

v0.2 调用写法v0.3 迁移写法
bindCart(cartId)syncCart(cartId);同步已绑定车时 syncCart()
updateWidget()、updateSP()、updatePP()syncCart(cart.id)——站点写车后调用一次,SP 面板与 PP 补购报价一并刷新
pp.getPlans({ productId, variantId, variantDetail })pp.loadPlans(variantId, variantDetail?)
pp.canBePopup()、pp.getSelectedPlan()canBePopup() 同名不变;getSelectedPlan() 同名,但返回 PPSelection | null(不再有 hasAddPP / planID);提交或清空选择改用 pp.setSelectedPlan(selection | null, context?)
pp.showModal()(返回 { hasAddPP, planID, variantItem })pp.showModal()(返回 PPSelection 或 null)
pp.addPP({ variantId, quantity })pp.addToCart({ addMainProduct })(回调 (attributes, context)),或高级原语 pp.addProtection({ mainLineId, selection })
pp.addFromCart({ lineKey, variantId })pp.offerProtection({ mainLineId });站点自绘列表用 pp.addProtection({ mainLineId, selection })
pp.getCartOffers()pp.getCartQuotes(),需 await;返回 { enabled, quotes },每项 { mainLineId, quantity, options }(v0.2 的 getCartOffers() 同样返回 Promise)
pp.getPPRelations()pp.getRelations(),返回 { mainLineId, protectionLineId }[](两字段均为 CartLine.id)
pp.removePP({ mainLineKey }) 或无参调用pp.removeProtection({ mainLineId }),返回 { status: 'removed' | 'already_unprotected' }
sp.showModal()sp.showModal()(同名保留)
sp.addSpFee() / sp.declineSpFee() / sp.removeSpFee()同名保留;addSpFee() 不可保或写车失败时改为 reject,declined 改为字面量 true。调用处需处理 rejection
sp.resolveSP() + 站点读开关后分流sp.resolveSP()(checkout);checkout+ 的两个按钮分别调用 sp.addSpFee() / sp.declineSpFee()
adapter.changeLineadapter.updateLines

PP 相关类型与字段的逐项变化:

v0.2 类型 / 字段v0.3 类型 / 字段说明
PPShowModalResult(hasAddPP、planID、variantItem)PPSelection(mainVariantId、plan、mainLineAttributes)不再有 hasAddPP / planID;行属性改为 AttributeInput[]
PPGetPlansParams(productId + variantId)PPPlansResult(context、options)context 句柄由 loadPlans 返回,不能手工构造
addPP({ variantId, quantity })PPAddToCartOptions(context?、addMainProduct(attributes, context))不再传 variantId;数量由站点在自己的回调包装层写入
addPP 返回 PPPlanItem | nullPPAddToCartResult(cartId、mainLineId、protection: 'added' | 'skipped')部分失败一律 reject,不再返回 protection: 'failed'
getCartOffers(): Promise<PPCartOffer[]>(lineKey、quantity、plans)getCartQuotes(): { enabled, quotes }(mainLineId、quantity、options)开关状态与「无候选」分开;lineKey → mainLineId,plans → options
removePP({ mainLineKey }) / removeProtection({ protectionLineId })removeProtection({ mainLineId }),返回 PPRemoveProtectionResult参数由保障行改为主商品行;结果是 status 而非 { removed }
addFromCart({ lineKey, variantId }) 返回 { added: boolean }offerProtection({ mainLineId }) 返回 PPOfferProtectionResult({ status: 'added' | 'cancelled' })cancelled 表示顾客取消且尚未写入;失败改为 reject

spWidgetSelector 与 ppWidgetSelector 保持不变;getMode() / getQuote() 名称不变,但 getQuote() 的返回值去掉了 inRange,不可保改用具体错误拒绝。SP 的七个公开方法(addSpFee / removeSpFee / declineSpFee / resolveSP / getQuote / getMode / showModal)名称与 v0.2 一致,行为按新契约收口:addSpFee() 不可保或写车失败时 reject,成功返回 { added: true, extId };removeSpFee() / declineSpFee() 分别返回 { removed } / { declined: true },失败 reject;resolveSP() 返回四种结果:added、removed,以及按 protection 区分的两种 skipped;checkout+ 下拒绝;showModal() 需要至少一个已挂载面板。

  • 与客户经理(AM)确认店铺当前运行的 SDK 版本及计划切换时间。sdk.js 仅作为加载入口,实际加载哪个版本由店铺后台配置决定;仅在本地替换类型文件或刷新浏览器不会改变后台下发的版本。
  • 备份当前的接入代码、后台版本配置及 v0.2 类型文件,以便回退时成套恢复。
  • 下载 v0.3 类型文件。请利用新类型暴露所有不兼容调用,切勿为了消除编译报错而将旧类型改写为大量可选字段。
  • 确认 window.xcottonConfig.currency 与购物车、Storefront 市场三者一致。v0.3 会拒绝币种不一致的购物车。

按上方对照表替换字段访问路径,并注意以下细节:

  • 同一规格可能拆分为多条购物车行。更新和删除操作必须使用当前快照中的 CartLine.id,且写入行属性后行 ID 可能会发生变化。
  • PP 写入与取消参数统一为 mainLineId(值均为 CartLine.id);protectionLineId 只由 getRelations() 返回用于展示关联关系,不再作为 removeProtection 的参数。
  • 金额字段请直接保留 Storefront 返回的原始值,不要在 adapter 中乘以 100,也不要将 0 元替换为其他价格。商品行的应付金额取自 cost.totalAmount,SDK 内部会自动完成报价所需的单位与标识转换。
  • 若站点自己传入 loadPlans 的 variantDetail,价格改为 Storefront 的 price.amount 十进制字符串 + price.currencyCode,商品归属改为 product.id。

仅使用默认 adapter 的项目可跳过此步。

按 使用自己的购物车 API 实现 getCart、addLines、updateLines、removeLines 和 updateAttributes 五个方法。与 v0.2 相比,以下几点最容易出错:

  • SDK 会将当前绑定的购物车 ID 作为首个参数传给各方法,adapter 内部不要再自行缓存「当前购物车 ID」。
  • 每次写操作成功后都必须返回完整的 Cart 对象,且必须拉取全部分页。
  • getCart 仅在确认购物车不存在时返回 null;遇到网络故障、鉴权失败或接口限流时必须 reject。
  • 购物车总价币种必须等于初始化时的 currency。默认与自定义 adapter 使用同一套币种守卫,不一致时 SDK 在报价或写车前就拒绝。
  • 接口限流时 reject 一个至少带 status: 429 或 code: 'THROTTLED' 的 Error,可附 retryAfterMs;SDK 据此进入冷却。不要吞掉错误或自行重试。
  • 请求超时并不代表服务端未完成写入。请关闭接口层的自动重试机制;操作失败后不要盲目重放或自动回滚,应重新查询购物车状态并交由顾客决定下一步操作。
  • 站点自行增删改购物车商品后,必须等待 syncCart(cart.id) 完成,再去读取 getCartQuotes();切换购物车时传入新的购物车 ID,SDK 会判断这是换车还是同车刷新。
  • 站点不再需要维护「上次绑定的购物车 ID」或同步队列:syncCart 覆盖首次绑定、换车与同车刷新三种情况,SDK 在一次同步内按依赖顺序执行内部步骤;外部并发调用会以 OPERATION_IN_PROGRESS 拒绝。
  • onCartChange 回调内只负责刷新站点视图,不得再调用 syncCart。
  • 涉及商品行写入的 SDK 操作每次最多触发一次通知;若写入失败但随后的补读成功,也会触发一次通知。只读操作不触发通知;独立的购物车属性写入确认后同样会触发一次通知。
  • SDK 不等待 onCartChange 返回的 Promise,请在回调内部自行捕获并处理异步错误。
  • 购物车页与购物车抽屉应共享同一份状态源,确保保障行写入完成后所有视图同步刷新,而非仅刷新触发点击的单个组件。

完整同步规则见 购物车同步。

  • PP 商品页报价依然可以在尚未创建购物车时调用。
  • PP 商品页加购收敛为一次 pp.addToCart({ context?, addMainProduct }):回调签名为 (attributes, context),只写主商品并返回 { cartId, mainLineId },SDK 负责选择、同步与保障行。成功只返回 protection: 'added' | 'skipped';主商品已写入但同步或保障失败时一律 reject PP_WRITE_CONFIRMATION_FAILED,此时不要重加主商品。
  • 购物车补加保障使用 addProtection({ mainLineId, selection })(selection 必填),SDK 弹窗编排使用 offerProtection({ mainLineId });移除保障使用 removeProtection({ mainLineId })。三者都不需要再手工同步或重写行属性。
  • SP 结账按钮按模式分流:cart 与 checkout 调用 resolveSP(),按开关意愿对齐费用行后再跳转;checkout+ 的两个按钮分别调用 addSpFee() 与 declineSpFee(),Promise 拒绝时不跳转。
  • SP 保障说明按钮调用 showModal();自绘开关直接使用 addSpFee() / removeSpFee(),不再有独立的设置开关入口。
  • 迁移 adapter 不会改变 cart、checkout 和 checkoutPlus 的行为语义。

当使用默认 adapter 且回调仅负责刷新界面时,v0.3 的标准写法如下。商品页加购用 pp.addToCart,站点其他写车操作后用 syncCart。示例中的 reloadCartView、showCartError、addMainProduct 与 quantity 是站点自己的函数和变量。

await window.xcotton!.init({
cartId: 'gid://shopify/Cart/your-cart-id',
onCartChange: () => {
void reloadCartView().catch(showCartError)
},
})
let cartId: string | undefined
try {
const result = await window.xcotton!.pp!.addToCart({
addMainProduct: async (attributes, context) => {
const { cart, mainLine } = await addMainProduct(
context.mainVariantId,
quantity,
attributes,
)
return { cartId: cart.id, mainLineId: mainLine.id }
},
})
cartId = result.cartId
} catch (error) {
// 主商品可能已写入;只提示,不重加。
showProtectionError(error)
}
// 站点其他写车操作之后:
if (cartId) await window.xcotton!.syncCart(cartId)

若回调内直接读取了传入的 cart 参数,请改为读取 cart.lines.nodes 与 MoneyV2 金额结构。由于回调参数仅包含 SDK 所需的字段子集,商品图片、checkoutUrl 等界面字段请继续通过站点自身的查询获取(详见 Cart)。

请在后台切换店铺版本前后各执行一遍以下核对清单,并留存购物车最终状态与浏览器截图备查:

  • 无购物车时进入商品详情页,PP 正常报价;快速连续切换规格后,面板仅展示最后一次规格的报价。
  • 选中方案后点击加购,主商品只写入一次,购物车中同时出现主商品行与保障行,且数量一致。
  • 将同一规格携带不同属性拆成两行加入购物车,分别执行更新与删除,确认操作不会误伤另一行。
  • 修改主商品数量时保障行数量同步更新;删除主商品后,关联的保障行被自动清理。
  • 在购物车页点击某行加购保障后,购物车抽屉中同一行的加购按钮同步消失。
  • 分别验证三种 SP 适配模式,确认费用行数量准确,且在写入失败时能可靠阻断结账跳转。
  • 使用店铺基础币种与至少一种展示币种分别验证:购物车币种与 window.xcottonConfig.currency 一致时正常;不一致时应被拒绝,而不是带着错误币种写入。
  • 模拟一次写请求响应丢失:确认不会重复加购、不会将失败误报为成功,且在重新读取购物车后可继续正常操作。
  • 模拟一次接口限流(自定义 adapter 抛出带 status: 429、code: 'THROTTLED'、retryAfterMs 的 Error),确认 SDK 在冷却期内拒绝请求,而不是继续发送。

如需回退,必须同时恢复三项内容:店铺后台的 v0.2 版本配置、迁移前的站点接入代码,以及 v0.2 类型声明文件。切勿让 v0.3 的 adapter 或回调逻辑继续接收 v0.2 的数据。