跳转到内容

快速开始

完成本页接入后,购物车页与抽屉将显示邮包险(SP)面板;顾客拨动开关时,费用行会自动加入或移出购物车。cart 模式还需由站点在结账前调用 resolveSP() 确认费用行已就绪,并在失败时阻止跳转,详见 适配模式。checkout 与 checkout+ 的差异集中在结账按钮的点击处理,费用行写入时机见适配模式页。

  • 已完成 接入前指引 中的准备清单。
  • 客户经理(AM)已开启 SP。若店铺使用的是 checkout 或 checkout+ 适配模式,请先按本页完成基础接入,再阅读 适配模式。
  • 下载类型声明文件 xcotton-types.ts 并放入项目的 lib/ 目录,下文示例均从该路径导入类型。

下面是一份不依赖任何前端框架的最小可运行页面,涵盖 SP 接入的全部三个步骤。后续小节将展示各主流框架下的对应写法。

<!-- 第 1 步:先声明配置,再加载 SDK -->
<script>
window.xcottonConfig = {
shop: 'example.myshopify.com',
country: 'US',
currency: 'USD',
language: 'en',
publicAccessToken: 'your-storefront-public-token',
spWidgetSelector: '.xc-sp-container',
}
</script>
<script
src="https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js"
crossorigin="anonymous"
></script>
<!-- 第 2 步:SDK 把 SP 面板渲染进这个容器 -->
<div class="xc-sp-container"></div>
<script type="module">
// 第 3 步:初始化,并同步页面当前的购物车
await window.xcotton.init({
cartId: 'gid://shopify/Cart/your-cart-id',
onCartChange: () => reloadCartView(),
})
// 之后站点每次自己改完购物车,同步一次即可:
// 首次绑定、换车、同车刷新都由 SDK 判断
// await window.xcotton.syncCart(cart.id)
</script>

其中 reloadCartView() 是站点自身的函数,用于重新查询购物车并刷新界面。

这份最小页面是直接单入口示例:init({ cartId }) 会顺带完成该购物车的首次同步。后面的框架示例使用共享 helper:xcottonInit() 只做无车装配,syncXcotton(cartId) 负责首次绑定、同车刷新与换车。两种写法都只调用一次 init。

页面必须先声明 window.xcottonConfig,再加载 sdk.js,顺序不可颠倒。在 SSR 或全栈框架中,这两段脚本都必须在框架水合(hydrate)之前执行。

window.xcottonConfig 配置字段:

字段必填说明示例
shop是Shopify 店铺域名example.myshopify.com
country是当前市场的国家代码,必须与 Shopify Markets 一致US
currency是当前市场实际结算使用的币种,必须与购物车币种一致USD
language是当前界面语言,决定面板文案,并作为 Storefront 查询与报价的语言参数en
publicAccessToken是Storefront API Public Access Token—
spWidgetSelector否SP 面板容器的 CSS 选择器.xc-sp-container
ppWidgetSelector否PP 面板容器的 CSS 选择器#xcotton-pp-panel

脚本地址固定为 https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js,URL 中不含版本号。实际下发的 SDK 版本由后台店铺配置决定,升级时无需修改脚本地址。

app/layout.tsx
import Script from 'next/script'
const config = {
shop: 'example.myshopify.com',
country: 'US',
currency: 'USD',
language: 'en',
publicAccessToken: 'your-storefront-public-token',
spWidgetSelector: '.xc-sp-container',
ppWidgetSelector: '#xcotton-pp-panel',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<Script id="xcotton-config" strategy="beforeInteractive">
{`window.xcottonConfig = ${JSON.stringify(config)}`}
</Script>
<Script
src="https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js"
strategy="beforeInteractive"
crossOrigin="anonymous"
/>
{children}
</body>
</html>
)
}

在购物车页和购物车抽屉中各放置一个与 spWidgetSelector 匹配的空容器元素。容器内部无需填充内容,SDK 检测到容器挂载后会自动将面板渲染进去。

多个匹配容器可以同时保留在页面中,包括暂时隐藏的抽屉。SDK 会在每个容器内分别创建面板,共享开关状态与报价;在任一面板操作开关后,其余面板同步更新。

components/cart-protection.tsx
export function CartProtection() {
return <div className="xc-sp-container" />
}

当购物车抽屉关闭后重新打开,或 SPA 路由切换回购物车页时,容器重新挂载后 SDK 会自动恢复面板,无需手动调用任何方法,也不会重复发起购物车或报价请求。

PP 面板容器位于商品详情页,由第 3 步的启动组件负责渲染。

这一步只需遵循两条规则:

  1. 共享组件示例的页面启动时:调用一次无参 init()(即 xcottonInit()),只完成 SDK 装配,不传 cartId。
  2. 站点修改当前购物车后:每次完成加购、改数量、删除或清空操作后,调用一次 syncCart(cart.id)(共享 helper 即 syncXcotton(cart.id))。首次绑定、切换购物车与同车刷新都由它处理。

站点不需要维护绑定状态,SDK 自行判断本次是首次绑定、同车刷新还是换车。

先创建一个共享模块,所有组件统一通过它访问 SDK:

lib/xcotton.ts
import type { XcottonApi } from './xcotton-types'
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({ 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)
}
  • xcottonInit() 在页面启动时只执行一次,不接受 cartId;它只完成 SDK 装配,首次绑定交给 syncXcotton(cartId)。helper 内部缓存同一个 Promise,多个组件共享结果。init 是一次性入口:进行中、成功后或失败后再次调用都会被明确拒绝(SDK_INIT_IN_PROGRESS / SDK_ALREADY_INITIALIZED),不会复用进行中的 Promise 冒充成功,也不能替换首次登记的 adapter 或 onCartChange。环境装配失败(包括 PP 应启用但初始化失败)时 SDK 不保留半初始化能力,需修正原因后重新加载页面。
  • syncXcotton(cartId) 先以无参 xcottonInit() 等待初始化完成,再调用 sdk.syncCart(cartId)。首次绑定、同车刷新与换车都走它;站点写完购物车后统一调用即可,无需判断是否换车,晚到的 cartId 也走这里,不要重复 init。SDK 会串行执行公开的 syncCart 调用;相邻且入参相同的并发调用会合并为一次同步,并共享同一个结果。与面板开关等不经公开入口的内部写车重叠时,调用可能以 OPERATION_IN_PROGRESS 拒绝;按 recovery 等当前操作结束后重试。syncCart 成功返回完成必要同步后的独立 Cart,业务是否完成以该 Promise 为准。
  • onCartChange 回调中只调用 reloadCartView(),切勿调用 syncXcotton 或 syncCart,否则会造成无限循环(详见 购物车同步)。

接着在页面根部挂载启动组件。它负责三件事:页面启动时以无参 xcottonInit() 装配 SDK;已有购物车或站点写完购物车时,把当前购物车 ID 交给 syncXcotton(cartId),由它调用 syncCart;在商品页将当前规格 ID 传给 PP 报价(非商品页传 null)。

components/xcotton-startup.tsx
'use client'
import { useEffect } from 'react'
import { xcottonInit, syncXcotton } from '@/lib/xcotton'
import { onCartUpdatedByStorefront } from '@/lib/cart'
export function XcottonStartup({
variant,
cartId,
}: {
variant: string | number | null
cartId?: string
}) {
useEffect(() => {
return onCartUpdatedByStorefront((cart) =>
syncXcotton(cart.id).catch(console.error),
)
}, [])
useEffect(() => {
void (cartId ? syncXcotton(cartId) : xcottonInit()).catch(console.error)
}, [cartId])
useEffect(() => {
void xcottonInit()
.then(() =>
variant === null
? window.xcotton?.pp?.loadPlans(null)
: window.xcotton?.pp?.loadPlans(variant),
)
.catch(console.error)
return () => {
void xcottonInit()
.then(() => window.xcotton?.pp?.loadPlans(null))
.catch(console.error)
}
}, [variant])
return <div id="xcotton-pp-panel" hidden={!variant} />
}

启动组件持续观察 cartId:挂载后异步恢复已有购物车或切换购物车时,更新该属性会调用 syncXcotton(cartId);同值重渲染不会重复请求。站点自己写车仍通过独立的订阅调用 syncXcotton(cartId),不要把 SDK 通知转发到该订阅。

示例从 lib/cart.ts 导入了两个需要你按站点购物车逻辑实现的函数:

函数作用
reloadCartView()重新查询购物车并刷新站点界面。只读操作,不得写入购物车。
onCartUpdatedByStorefront(handler)站点自身写完购物车后触发 handler(cart),并返回取消订阅函数。SDK 发起的写入不得触发该事件。

启动组件的 variant 参数为商品页当前选中的规格 ID(字符串或数字),用于触发 PP 报价,详见 接入延保险。未开启 PP 时传 null 即可。

若希望在面板以外的位置放置「保障详情」按钮,可调用 sp.showModal()。返回的 Promise 在弹窗打开后即完成,不会等待顾客关闭弹窗。弹窗内的保障按钮会按当前适配模式表达意愿:cart 模式写费用行,checkout 模式只记录意愿,checkout+ 不展示该按钮。

components/protection-details-button.tsx
'use client'
import { xcottonInit } from '@/lib/xcotton'
async function onShowSpDetailsClick() {
await xcottonInit()
const sp = window.xcotton?.sp
if (!sp) throw new Error('[XC] SP is not ready')
await sp.showModal()
}
export function ProtectionDetailsButton() {
return (
<button
type="button"
onClick={() => void onShowSpDetailsClick().catch(console.error)}
>
Protection details
</button>
)
}

以下检查按 cart 模式。checkout 与 checkout+ 的费用行只在结账时写入,验证步骤见 适配模式。

打开购物车页,在浏览器控制台执行:

window.xcotton.sp?.getMode()
// 'cart'
await window.xcotton.sp?.getQuote()
// { extId: 45678901234, totalPrice: '1.99', totalPriceInt: 199, currencyCode: 'USD', currencySymbol: '$' }

然后按表逐项验证:

操作预期结果
打开购物车页正常渲染 SP 面板
拨动面板开关费用行加入或移出购物车,站点购物车界面随之刷新
站点加购一件商品并调用 syncCart(cart.id)面板报价随之更新
清空购物车或让金额超出可保范围,并调用 syncCart(cart.id)费用行自动移除
关闭再打开抽屉,或 SPA 路由切回购物车面板自动恢复,Network 面板中无新增的购物车或报价请求
拨动开关后同时查看购物车页和抽屉两处显示的购物车内容保持一致
现象原因处理方法
报错 [XC] sdk.js has not loaded yet调用示例模块时 sdk.js 尚未执行确认两段脚本在 hydrate 前加载,且脚本地址可正常访问
init() 拒绝,错误以 [XC] SDK initialization failed 开头SDK 环境初始化失败,括号内为具体模块与阶段检查 shop 与 publicAccessToken;仍失败时将完整报错发给客户经理(AM)。修正后重新加载页面,不要重复 init
init() 拒绝,code=SDK_INIT_SYNC_FAILED环境已就绪,但首次购物车同步失败按错误中的 cartId 调用 syncCart(cartId) 恢复;不要重复 init
init() 拒绝,code=OPERATION_IN_PROGRESS首次同步被在途操作占用等当前操作结束后调用 syncCart(cartId) 恢复;不要重复 init
init() 拒绝,code=SDK_ALREADY_INITIALIZED组件重复调用了 init页面只初始化一次;晚到的 cartId 改走 syncXcotton(cartId)(直接调用 syncCart(cartId) 也可)
syncCart 报错以 [XC] SDK not initialized 开头在 init() 完成前调用了 syncCart统一通过 syncXcotton 调用,并先 await xcottonInit()
syncCart() 报 [XC] syncCart requires a cartId before the first synchronization未传 cartId 且还没有绑定任何购物车首次同步时传入当前购物车 ID
syncCart 报 [XC] cart currency does not match the configured market (…)购物车币种与 window.xcottonConfig.currency 不一致按当前市场传 currency,并确认购物车属于同一市场
window.xcotton.sp 为 undefined尚未完成带 cartId 的首次同步,或店铺未开启 SP先等待 syncCart 完成;若仍为空,联系客户经理(AM)
sp 存在但页面未出现面板没有与 spWidgetSelector 匹配的 DOM 元素、容器在 shadow DOM 内,或当前购物车不满足展示条件(如空车、金额超出可保范围)用 document.querySelectorAll() 确认匹配;容器放在 Light DOM,并确认购物车满足 SP 展示条件
购物车与报价请求循环发出、无法停止在 onCartChange 中调用了 syncCart回调中仅刷新站点界面,不要调用同步方法
面板报价与结账页金额不一致country 或 currency 与 Shopify Markets 不一致按当前市场传值,见 接入前指引

完整的错误信息列表见 SDK 参考。