使用自己的购物车 API
默认情况下,SDK 直接调用 Shopify Storefront API 读写购物车,无需配置 adapter。仅当你的购物车读写必须经过站点自有 API 或 BFF(例如需要服务端鉴权、风控校验,或在写车时执行附加逻辑)时,才需要实现 XcottonCartAdapter 并在 init 时传入。
adapter 包含五个方法,签名均与 Storefront Cart API 一一对应:
| 方法 | 对应 Storefront Query / Mutation | 返回值 |
|---|---|---|
getCart(cartId) | cart | Cart 或 null |
addLines(cartId, lines) | cartLinesAdd | 操作后的完整 Cart |
updateLines(cartId, lines) | cartLinesUpdate | 操作后的完整 Cart |
removeLines(cartId, lineIds) | cartLinesRemove | 操作后的完整 Cart |
updateAttributes(cartId, attributes) | cartAttributesUpdate | 操作后的完整 Cart |
其中 cartId 由 SDK 自动传入(取自当前绑定的购物车),adapter 内部无需自行维护当前购物车 ID。
- 店铺运行的是 SDK v0.3。若需从 v0.2 迁移存量 adapter,请先阅读 从 v0.2 升级到 v0.3。
- 你的接口支持查询购物车、新增行、更新行、删除行,以及整体替换购物车属性(
attributes)。 - 写入方法返回完整
Cart;getCart还可以返回null(字段说明见 Cart)。建议使用 xcotton-types.ts 中的XcottonCartAdapter类型对实现做静态类型检查。
- 实现上述五个方法,将 SDK 传入的参数原样转发给你的后端接口,并返回操作后的完整购物车。
- 在页面启动时通过
init({ adapter, onCartChange })注册 adapter(每次页面加载仅注册一次)。即便尚未创建购物车也可先行初始化,PP 商品报价不依赖购物车。 - 站点每次写车成功后(包括首次绑定与换车)调用
syncCart(cart.id),并await等待完成;SDK 自行判断是首次绑定、换车还是同车刷新。 - 在
onCartChange中仅刷新站点界面,切勿再次调用syncCart,也不要将其转发为站点写车事件(详见 购物车同步)。
可以只覆盖部分方法
Section titled “可以只覆盖部分方法”未实现的方法会自动回退到 SDK 内置的默认 Storefront 实现。混用自定义方法与默认实现时需满足以下条件:
- 所有方法操作的是同一个 Storefront 购物车、使用同一套行 ID,且两边均持有合法凭证。
- 若你的 BFF 返回的是自定义行 ID(而非 Storefront 原生的
CartLine.id),内置默认方法将无法识别这些 ID,此时必须完整实现所有涉及行操作的方法。 - 当你实现的自定义方法抛错或 reject 时,SDK 不会回退到默认实现自动重试。
写入的输入规则
Section titled “写入的输入规则”| 场景 | 规则 |
|---|---|
| 新增行 | 使用 merchandiseId(完整的 ProductVariant GID);quantity 可省略,默认为 1 |
| 更新行 | 使用当前购物车快照中的 CartLine.id;quantity 与 attributes 至少传入一项。quantity: 0 表示删除,SDK 会把它原样交给 updateLines,你的实现需要按删除处理 |
| 删除行 | 优先使用 removeLines |
| 定位目标行 | 切勿按规格 ID 反查行——同一规格可能存在多行,且更新属性后行 ID 可能变化;切勿将更新失败降级为新增行 |
| 行属性 | 省略或传 null 表示保留原属性,传 [] 表示清空,传非空数组表示整体替换 |
| 购物车属性 | updateAttributes 总是接收完整的属性数组([] 表示清空)。SDK 内部已合并好业务属性,请直接全量写入,不要再先读后合 |
| 属性值校验 | 属性值必须为字符串。重复的属性 key 及非法数量会在请求发出前被 SDK 直接拒绝 |
| 场景 | 要求 |
|---|---|
getCart 查不到购物车 | 返回 null。注意:仅在确认购物车不存在时才可返回 null;空购物车要返回 lines.nodes: [] 的完整 Cart,SDK 对 null 会以 CART_NOT_FOUND 拒绝依赖真实购物车的操作 |
| 网络异常、鉴权失败、接口限流 | 必须 reject Promise。切勿返回 null,否则 SDK 会误判为购物车不存在 |
| 写入成功 | 返回操作后的完整 Cart。若返回 null 或 undefined 属于违反契约:SDK 会补读一次购物车以恢复状态,但仍会以 ADAPTER_RESPONSE_INVALID 拒绝本次操作 |
| 写入失败 | 必须 reject Promise。失败的写请求可能已在服务端部分生效,SDK 不会自动回滚或重放;请关闭你接口层的自动重试 |
| 接口限流 | reject 一个 Error,至少带 status: 429 或 code: 'THROTTLED'(消息含 429 / THROTTLED 也可识别)。带 retryAfterMs(毫秒)时按该时长冷却,否则按默认 30 秒冷却 |
SDK 会对每次返回的 Cart 执行契约校验。读取返回的 Cart 校验失败时,SDK 会从 adapter 重读一次,重读成功即可恢复;写入返回的 Cart 校验失败时直接拒绝本次操作:
cart.id必须与当前绑定的购物车 ID 完全一致。- 购物车总价币种(
cost.totalAmount.currencyCode)必须与初始化时的window.xcottonConfig.currency一致;默认与自定义 adapter 使用相同规则,不一致时在报价或写车前就被拒绝。 - 购物车行必须拉取全部分页,满足
lines.pageInfo.hasNextPage === false,且行 ID 不得重复。 - 金额字段保留 Storefront 原生的
MoneyV2格式:amount为十进制字符串,所有金额的currencyCode必须与购物车总价币种一致。切勿乘以 100,也不要将0元替换成其他金额。 - 保留 Cart 合同的全部必填字段。报价还会读取配送属性(
requiresShipping)、礼品卡标识(isGiftCard)、SKU、标题、vendor、productType、折扣码(discountCodes)及行成本(cost);商品身份(product.id、product.handle)、行quantity与attributes同样必填。无需返回 v0.2 Ajax Cart 的旧字段。
写入结果按四种状态区分:未发送、明确拒绝且未生效、已确认部分或全部、结果未知。SDK 在能确认时把状态放进错误的 context.writeOutcome。返回 null / undefined 或写入后补页失败时,SDK 只做一次只读补读并拒绝本次操作,不会重放 mutation。
自定义 adapter 自行负责分页,并返回 lines.pageInfo.hasNextPage === false 的完整 Cart。SDK 只校验这份最终快照;跨页车级事实(总价、属性、折扣码)的核对是默认 adapter 补页时的能力,SDK 不会读取自定义 adapter 的中间页。读取快照不完整、分页非法或行 ID 重复时,SDK 会从 adapter 重读一次;这只降低混合快照风险,不代表跨页事务一致。同一购物车的同步与写入由 SDK 串行协调:同步或写入进行中再调用另一个写车入口会以 OPERATION_IN_PROGRESS 拒绝,后到调用不应依赖 adapter 自行排队。
下方示例中,myApi 代表站点的购物车接口模块,示例从 lib/cart.ts 导入它;按你的项目结构调整路径。第二个文件用于替换 快速开始 中的 lib/xcotton.ts,改动是导入 cartAdapter 并在 init 中传入 adapter。
import type { XcottonCartAdapter } from './xcotton-types'import { myApi } from './cart'// SDK 自动传入绑定的 cartId;写入方法返回完整 Cart。export const cartAdapter: XcottonCartAdapter = { getCart: (cartId) => myApi.getCart(cartId), addLines: (cartId, lines) => myApi.addLines(cartId, lines), updateLines: (cartId, lines) => myApi.updateLines(cartId, lines), removeLines: (cartId, lineIds) => myApi.removeLines(cartId, lineIds), updateAttributes: (cartId, attributes) => myApi.updateAttributes(cartId, attributes),}import type { XcottonApi, XcottonCartAdapter } from './xcotton-types'import { cartAdapter } from './cart-adapter'import { reloadCartView } from './cart'
const cartChangeListeners = new Set<() => void>()
export function onXcottonCartChange(listener: () => void): () => void { cartChangeListeners.add(listener) return () => { cartChangeListeners.delete(listener) }}
// 页面只初始化一次:所有组件共享同一个 Promise 与回调引用。const handleCartChange = () => { void reloadCartView().catch(console.error) // 已确认的购物车事实可能伴随业务同步失败,组件需重新读取候选并处理错误。 for (const listener of cartChangeListeners) listener()}
let initPromise: Promise<void> | undefined
export function xcottonInit(): Promise<void> { const sdk = window.xcotton if (!sdk) return Promise.reject(new Error('[XC] sdk.js has not loaded yet')) if (initPromise) return initPromise // 一次性入口:重复调用不会重新初始化;晚到的 cartId 交给 syncXcotton(cartId) 绑定。 initPromise = sdk.init({ adapter: cartAdapter, onCartChange: handleCartChange, }) return initPromise}
// 站点写车后统一交给 SDK:首次绑定、换车、同车刷新都由它判断。// 先确保会话已初始化,站点不维护绑定身份或同步队列。export async function syncXcotton(cartId: string): Promise<void> { const sdk: XcottonApi | undefined = window.xcotton if (!sdk) throw new Error('[XC] sdk.js has not loaded yet') await xcottonInit() await sdk.syncCart(cartId)}import type { XcottonCartAdapter } from './xcotton-types'import { myApi } from './cart'// SDK 自动传入绑定的 cartId;写入方法返回完整 Cart。export const cartAdapter: XcottonCartAdapter = { getCart: (cartId) => myApi.getCart(cartId), addLines: (cartId, lines) => myApi.addLines(cartId, lines), updateLines: (cartId, lines) => myApi.updateLines(cartId, lines), removeLines: (cartId, lineIds) => myApi.removeLines(cartId, lineIds), updateAttributes: (cartId, attributes) => myApi.updateAttributes(cartId, attributes),}import type { XcottonApi, XcottonCartAdapter } from './xcotton-types'import { cartAdapter } from './cart-adapter'import { reloadCartView } from './cart'
const cartChangeListeners = new Set<() => void>()
export function onXcottonCartChange(listener: () => void): () => void { cartChangeListeners.add(listener) return () => { cartChangeListeners.delete(listener) }}
// 页面只初始化一次:所有组件共享同一个 Promise 与回调引用。const handleCartChange = () => { void reloadCartView().catch(console.error) // 已确认的购物车事实可能伴随业务同步失败,组件需重新读取候选并处理错误。 for (const listener of cartChangeListeners) listener()}
let initPromise: Promise<void> | undefined
export function xcottonInit(): Promise<void> { const sdk = window.xcotton if (!sdk) return Promise.reject(new Error('[XC] sdk.js has not loaded yet')) if (initPromise) return initPromise // 一次性入口:重复调用不会重新初始化;晚到的 cartId 交给 syncXcotton(cartId) 绑定。 initPromise = sdk.init({ adapter: cartAdapter, onCartChange: handleCartChange, }) return initPromise}
// 站点写车后统一交给 SDK:首次绑定、换车、同车刷新都由它判断。// 先确保会话已初始化,站点不维护绑定身份或同步队列。export async function syncXcotton(cartId: string): Promise<void> { const sdk: XcottonApi | undefined = window.xcotton if (!sdk) throw new Error('[XC] sdk.js has not loaded yet') await xcottonInit() await sdk.syncCart(cartId)}import type { XcottonCartAdapter } from './xcotton-types'import { myApi } from './cart'// SDK 自动传入绑定的 cartId;写入方法返回完整 Cart。export const cartAdapter: XcottonCartAdapter = { getCart: (cartId) => myApi.getCart(cartId), addLines: (cartId, lines) => myApi.addLines(cartId, lines), updateLines: (cartId, lines) => myApi.updateLines(cartId, lines), removeLines: (cartId, lineIds) => myApi.removeLines(cartId, lineIds), updateAttributes: (cartId, attributes) => myApi.updateAttributes(cartId, attributes),}import type { XcottonApi, XcottonCartAdapter } from './xcotton-types'import { cartAdapter } from './cart-adapter'import { reloadCartView } from './cart'
const cartChangeListeners = new Set<() => void>()
export function onXcottonCartChange(listener: () => void): () => void { cartChangeListeners.add(listener) return () => { cartChangeListeners.delete(listener) }}
// 页面只初始化一次:所有组件共享同一个 Promise 与回调引用。const handleCartChange = () => { void reloadCartView().catch(console.error) // 已确认的购物车事实可能伴随业务同步失败,组件需重新读取候选并处理错误。 for (const listener of cartChangeListeners) listener()}
let initPromise: Promise<void> | undefined
export function xcottonInit(): Promise<void> { const sdk = window.xcotton if (!sdk) return Promise.reject(new Error('[XC] sdk.js has not loaded yet')) if (initPromise) return initPromise // 一次性入口:重复调用不会重新初始化;晚到的 cartId 交给 syncXcotton(cartId) 绑定。 initPromise = sdk.init({ adapter: cartAdapter, onCartChange: handleCartChange, }) return initPromise}
// 站点写车后统一交给 SDK:首次绑定、换车、同车刷新都由它判断。// 先确保会话已初始化,站点不维护绑定身份或同步队列。export async function syncXcotton(cartId: string): Promise<void> { const sdk: XcottonApi | undefined = window.xcotton if (!sdk) throw new Error('[XC] sdk.js has not loaded yet') await xcottonInit() await sdk.syncCart(cartId)}import type { XcottonCartAdapter } from './xcotton-types'import { myApi } from './cart'// SDK 自动传入绑定的 cartId;写入方法返回完整 Cart。export const cartAdapter: XcottonCartAdapter = { getCart: (cartId) => myApi.getCart(cartId), addLines: (cartId, lines) => myApi.addLines(cartId, lines), updateLines: (cartId, lines) => myApi.updateLines(cartId, lines), removeLines: (cartId, lineIds) => myApi.removeLines(cartId, lineIds), updateAttributes: (cartId, attributes) => myApi.updateAttributes(cartId, attributes),}import type { XcottonApi, XcottonCartAdapter } from './xcotton-types'import { cartAdapter } from './cart-adapter'import { reloadCartView } from './cart'
const cartChangeListeners = new Set<() => void>()
export function onXcottonCartChange(listener: () => void): () => void { cartChangeListeners.add(listener) return () => { cartChangeListeners.delete(listener) }}
// 页面只初始化一次:所有组件共享同一个 Promise 与回调引用。const handleCartChange = () => { void reloadCartView().catch(console.error) // 已确认的购物车事实可能伴随业务同步失败,组件需重新读取候选并处理错误。 for (const listener of cartChangeListeners) listener()}
let initPromise: Promise<void> | undefined
export function xcottonInit(): Promise<void> { const sdk = window.xcotton if (!sdk) return Promise.reject(new Error('[XC] sdk.js has not loaded yet')) if (initPromise) return initPromise // 一次性入口:重复调用不会重新初始化;晚到的 cartId 交给 syncXcotton(cartId) 绑定。 initPromise = sdk.init({ adapter: cartAdapter, onCartChange: handleCartChange, }) return initPromise}
// 站点写车后统一交给 SDK:首次绑定、换车、同车刷新都由它判断。// 先确保会话已初始化,站点不维护绑定身份或同步队列。export async function syncXcotton(cartId: string): Promise<void> { const sdk: XcottonApi | undefined = window.xcotton if (!sdk) throw new Error('[XC] sdk.js has not loaded yet') await xcottonInit() await sdk.syncCart(cartId)}import type { XcottonCartAdapter } from './xcotton-types'import { myApi } from './cart'// SDK 自动传入绑定的 cartId;写入方法返回完整 Cart。export const cartAdapter: XcottonCartAdapter = { getCart: (cartId) => myApi.getCart(cartId), addLines: (cartId, lines) => myApi.addLines(cartId, lines), updateLines: (cartId, lines) => myApi.updateLines(cartId, lines), removeLines: (cartId, lineIds) => myApi.removeLines(cartId, lineIds), updateAttributes: (cartId, attributes) => myApi.updateAttributes(cartId, attributes),}import type { XcottonApi, XcottonCartAdapter } from './xcotton-types'import { cartAdapter } from './cart-adapter'import { reloadCartView } from './cart'
const cartChangeListeners = new Set<() => void>()
export function onXcottonCartChange(listener: () => void): () => void { cartChangeListeners.add(listener) return () => { cartChangeListeners.delete(listener) }}
// 页面只初始化一次:所有组件共享同一个 Promise 与回调引用。const handleCartChange = () => { void reloadCartView().catch(console.error) // 已确认的购物车事实可能伴随业务同步失败,组件需重新读取候选并处理错误。 for (const listener of cartChangeListeners) listener()}
let initPromise: Promise<void> | undefined
export function xcottonInit(): Promise<void> { const sdk = window.xcotton if (!sdk) return Promise.reject(new Error('[XC] sdk.js has not loaded yet')) if (initPromise) return initPromise // 一次性入口:重复调用不会重新初始化;晚到的 cartId 交给 syncXcotton(cartId) 绑定。 initPromise = sdk.init({ adapter: cartAdapter, onCartChange: handleCartChange, }) return initPromise}
// 站点写车后统一交给 SDK:首次绑定、换车、同车刷新都由它判断。// 先确保会话已初始化,站点不维护绑定身份或同步队列。export async function syncXcotton(cartId: string): Promise<void> { const sdk: XcottonApi | undefined = window.xcotton if (!sdk) throw new Error('[XC] sdk.js has not loaded yet') await xcottonInit() await sdk.syncCart(cartId)}import type { XcottonCartAdapter } from './xcotton-types'import { myApi } from './cart'// SDK 自动传入绑定的 cartId;写入方法返回完整 Cart。export const cartAdapter: XcottonCartAdapter = { getCart: (cartId) => myApi.getCart(cartId), addLines: (cartId, lines) => myApi.addLines(cartId, lines), updateLines: (cartId, lines) => myApi.updateLines(cartId, lines), removeLines: (cartId, lineIds) => myApi.removeLines(cartId, lineIds), updateAttributes: (cartId, attributes) => myApi.updateAttributes(cartId, attributes),}import type { XcottonApi, XcottonCartAdapter } from './xcotton-types'import { cartAdapter } from './cart-adapter'import { reloadCartView } from './cart'
const cartChangeListeners = new Set<() => void>()
export function onXcottonCartChange(listener: () => void): () => void { cartChangeListeners.add(listener) return () => { cartChangeListeners.delete(listener) }}
// 页面只初始化一次:所有组件共享同一个 Promise 与回调引用。const handleCartChange = () => { void reloadCartView().catch(console.error) // 已确认的购物车事实可能伴随业务同步失败,组件需重新读取候选并处理错误。 for (const listener of cartChangeListeners) listener()}
let initPromise: Promise<void> | undefined
export function xcottonInit(): Promise<void> { const sdk = window.xcotton if (!sdk) return Promise.reject(new Error('[XC] sdk.js has not loaded yet')) if (initPromise) return initPromise // 一次性入口:重复调用不会重新初始化;晚到的 cartId 交给 syncXcotton(cartId) 绑定。 initPromise = sdk.init({ adapter: cartAdapter, onCartChange: handleCartChange, }) return initPromise}
// 站点写车后统一交给 SDK:首次绑定、换车、同车刷新都由它判断。// 先确保会话已初始化,站点不维护绑定身份或同步队列。export async function syncXcotton(cartId: string): Promise<void> { const sdk: XcottonApi | undefined = window.xcotton if (!sdk) throw new Error('[XC] sdk.js has not loaded yet') await xcottonInit() await sdk.syncCart(cartId)}import type { XcottonCartAdapter } from './xcotton-types'import { myApi } from './cart'// SDK 自动传入绑定的 cartId;写入方法返回完整 Cart。export const cartAdapter: XcottonCartAdapter = { getCart: (cartId) => myApi.getCart(cartId), addLines: (cartId, lines) => myApi.addLines(cartId, lines), updateLines: (cartId, lines) => myApi.updateLines(cartId, lines), removeLines: (cartId, lineIds) => myApi.removeLines(cartId, lineIds), updateAttributes: (cartId, attributes) => myApi.updateAttributes(cartId, attributes),}import type { XcottonApi, XcottonCartAdapter } from './xcotton-types'import { cartAdapter } from './cart-adapter'import { reloadCartView } from './cart'
const cartChangeListeners = new Set<() => void>()
export function onXcottonCartChange(listener: () => void): () => void { cartChangeListeners.add(listener) return () => { cartChangeListeners.delete(listener) }}
// 页面只初始化一次:所有组件共享同一个 Promise 与回调引用。const handleCartChange = () => { void reloadCartView().catch(console.error) // 已确认的购物车事实可能伴随业务同步失败,组件需重新读取候选并处理错误。 for (const listener of cartChangeListeners) listener()}
let initPromise: Promise<void> | undefined
export function xcottonInit(): Promise<void> { const sdk = window.xcotton if (!sdk) return Promise.reject(new Error('[XC] sdk.js has not loaded yet')) if (initPromise) return initPromise // 一次性入口:重复调用不会重新初始化;晚到的 cartId 交给 syncXcotton(cartId) 绑定。 initPromise = sdk.init({ adapter: cartAdapter, onCartChange: handleCartChange, }) return initPromise}
// 站点写车后统一交给 SDK:首次绑定、换车、同车刷新都由它判断。// 先确保会话已初始化,站点不维护绑定身份或同步队列。export async function syncXcotton(cartId: string): Promise<void> { const sdk: XcottonApi | undefined = window.xcotton if (!sdk) throw new Error('[XC] sdk.js has not loaded yet') await xcottonInit() await sdk.syncCart(cartId)}| 操作 | 预期结果 |
|---|---|
| 将同一规格拆成两行加入购物车,分别更新与删除 | 每次操作仅作用于目标行,不影响另一行 |
| 购物车行数超过单页上限 | 返回的 lines.nodes 包含全部商品行,且 hasNextPage 为 false |
| 修改主商品数量 | 关联保障行的数量同步更新 |
取消某条主行的保障(removeProtection) | 主商品行保留,该行 _PlanID 被清除、其他属性不变;同组其他主行保障不受影响 |
cart 模式下清空购物车并同步 | 购物车中不残留费用行 |
| 模拟一次写请求响应丢失 | 不产生重复加购,界面不会将失败展示为成功;重新读取购物车后可继续操作 |
| 在一次 SDK 同步或写入进行中并行发起另一个写车请求 | 后到请求以 OPERATION_IN_PROGRESS 拒绝,adapter 只收到一次 mutation |
| 默认 adapter 分页读取期间修改购物车属性或总价 | SDK 丢弃混合结果并有限重读,不会重放写入;自定义 adapter 需自行保证跨页一致 |
| SDK 完成写车后 | 购物车页与抽屉同步刷新,且回调内部的刷新异常得到妥善捕获 |
| 错误信息 | 原因 |
|---|---|
[XC] line.id: duplicate or missing cart line | SDK 要更新或删除的行 ID 不在你返回的购物车中,或在单次请求内重复出现 |
[XC] updateLines: quantity or attributes required | 调用更新行时既未传 quantity 也未传 attributes |
包含 response does not match bound cart | 返回的 cart.id 与当前绑定的购物车 ID 不一致 |
包含 inconsistent currency | 某处金额的 currencyCode 与购物车总价币种不一致 |
包含 cart currency does not match the configured market | 购物车总价币种与 window.xcottonConfig.currency 不一致,被币种守卫拦截 |
[XC] Cart requests are rate limited | adapter 返回了限流信号(429 / THROTTLED),SDK 已进入冷却,或本次请求正被冷却拒绝 |
包含 duplicate line 或 duplicate attribute | 返回的商品行 ID 重复,或同一属性列表中存在重复的 key |
完整的接口类型签名见 SDK 参考。