快速开始
完成本页接入后,购物车页与抽屉将显示邮包险(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。
第 1 步:加载 SDK
Section titled “第 1 步:加载 SDK”页面必须先声明 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 版本由后台店铺配置决定,升级时无需修改脚本地址。
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> )}import { Links, Meta, Outlet, Scripts } from '@remix-run/react'
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 App() { return ( <html lang="en"> <head> <Meta /> <Links /> <script dangerouslySetInnerHTML={{ __html: `window.xcottonConfig = ${JSON.stringify(config)}`, }} /> <script src="https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js" crossOrigin="anonymous" /> </head> <body> <Outlet /> <Scripts /> </body> </html> )}<script setup lang="ts">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',}
useHead({ script: [ { innerHTML: `window.xcottonConfig = ${JSON.stringify(config)}`, tagPosition: 'head', }, { src: 'https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js', tagPosition: 'head', }, ],})</script>
<template> <NuxtPage /></template><script> window.xcottonConfig = { shop: 'example.myshopify.com', country: 'US', currency: 'USD', language: 'en', publicAccessToken: 'your-storefront-public-token', spWidgetSelector: '.xc-sp-container', ppWidgetSelector: '#xcotton-pp-panel', }</script><script src="https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js" crossorigin="anonymous"></script><script> window.xcottonConfig = { shop: 'example.myshopify.com', country: 'US', currency: 'USD', language: 'en', publicAccessToken: 'your-storefront-public-token', spWidgetSelector: '.xc-sp-container', ppWidgetSelector: '#xcotton-pp-panel', }</script><script src="https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js" crossorigin="anonymous"></script><script> window.xcottonConfig = { shop: 'example.myshopify.com', country: 'US', currency: 'USD', language: 'en', publicAccessToken: 'your-storefront-public-token', spWidgetSelector: '.xc-sp-container', ppWidgetSelector: '#xcotton-pp-panel', }</script><script src="https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js" crossorigin="anonymous"></script><script> window.xcottonConfig = { shop: 'example.myshopify.com', country: 'US', currency: 'USD', language: 'en', publicAccessToken: 'your-storefront-public-token', spWidgetSelector: '.xc-sp-container', ppWidgetSelector: '#xcotton-pp-panel', }</script><script src="https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js" crossorigin="anonymous"></script>第 2 步:放置面板容器
Section titled “第 2 步:放置面板容器”在购物车页和购物车抽屉中各放置一个与 spWidgetSelector 匹配的空容器元素。容器内部无需填充内容,SDK 检测到容器挂载后会自动将面板渲染进去。
多个匹配容器可以同时保留在页面中,包括暂时隐藏的抽屉。SDK 会在每个容器内分别创建面板,共享开关状态与报价;在任一面板操作开关后,其余面板同步更新。
export function CartProtection() { return <div className="xc-sp-container" />}export function CartProtection() { return <div className="xc-sp-container" />}<template><div class="xc-sp-container"></div></template>export function CartProtection() { return <div className="xc-sp-container" />}<template><div class="xc-sp-container"></div></template><div class="xc-sp-container"></div>export function CartProtection() { return <div class="xc-sp-container" />}当购物车抽屉关闭后重新打开,或 SPA 路由切换回购物车页时,容器重新挂载后 SDK 会自动恢复面板,无需手动调用任何方法,也不会重复发起购物车或报价请求。
PP 面板容器位于商品详情页,由第 3 步的启动组件负责渲染。
第 3 步:初始化并同步购物车
Section titled “第 3 步:初始化并同步购物车”这一步只需遵循两条规则:
- 共享组件示例的页面启动时:调用一次无参
init()(即xcottonInit()),只完成 SDK 装配,不传cartId。 - 站点修改当前购物车后:每次完成加购、改数量、删除或清空操作后,调用一次
syncCart(cart.id)(共享 helper 即syncXcotton(cart.id))。首次绑定、切换购物车与同车刷新都由它处理。
站点不需要维护绑定状态,SDK 自行判断本次是首次绑定、同车刷新还是换车。
先创建一个共享模块,所有组件统一通过它访问 SDK:
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)}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)}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)}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)}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)}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)}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)。
'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} />}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} />}<script setup lang="ts">import { onMounted, onUnmounted, watch } from 'vue'import { xcottonInit, syncXcotton } from '~/lib/xcotton'import { onCartUpdatedByStorefront } from '~/lib/cart'
const props = defineProps<{ variant: string | number | null cartId?: string}>()let cleanup: (() => void) | undefinedonMounted(() => { cleanup = (() => { return onCartUpdatedByStorefront((cart) => syncXcotton(cart.id).catch(console.error), ) })() watch( () => props.cartId, (cartId) => { void (cartId ? syncXcotton(cartId) : xcottonInit()).catch(console.error) }, { immediate: true }, ) watch( () => props.variant, (variant) => { void xcottonInit() .then(() => variant === null ? window.xcotton?.pp?.loadPlans(null) : window.xcotton?.pp?.loadPlans(variant), ) .catch(console.error) }, { immediate: true, flush: 'post' }, )})onUnmounted(() => { cleanup?.() void xcottonInit() .then(() => window.xcotton?.pp?.loadPlans(null)) .catch(console.error)})</script>
<template> <div id="xcotton-pp-panel" v-show="variant !== null"></div></template>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} />}<script setup lang="ts">import { onMounted, onUnmounted, watch } from 'vue'import { xcottonInit, syncXcotton } from '../lib/xcotton'import { onCartUpdatedByStorefront } from '../lib/cart'
const props = defineProps<{ variant: string | number | null cartId?: string}>()let cleanup: (() => void) | undefinedonMounted(() => { cleanup = (() => { return onCartUpdatedByStorefront((cart) => syncXcotton(cart.id).catch(console.error), ) })() watch( () => props.cartId, (cartId) => { void (cartId ? syncXcotton(cartId) : xcottonInit()).catch(console.error) }, { immediate: true }, ) watch( () => props.variant, (variant) => { void xcottonInit() .then(() => variant === null ? window.xcotton?.pp?.loadPlans(null) : window.xcotton?.pp?.loadPlans(variant), ) .catch(console.error) }, { immediate: true, flush: 'post' }, )})onUnmounted(() => { cleanup?.() void xcottonInit() .then(() => window.xcotton?.pp?.loadPlans(null)) .catch(console.error)})</script>
<template> <div id="xcotton-pp-panel" v-show="variant !== null"></div></template><script lang="ts"> import { onMount } from 'svelte' import { xcottonInit, syncXcotton } from '$lib/xcotton' import { onCartUpdatedByStorefront } from '$lib/cart'
let { variant, cartId, }: { variant: string | number | null; cartId?: string } = $props() onMount(() => { return onCartUpdatedByStorefront((cart) => syncXcotton(cart.id).catch(console.error), ) }) $effect(() => { const current = cartId void (current ? syncXcotton(current) : xcottonInit()).catch(console.error) }) $effect(() => { const current = variant void xcottonInit() .then(() => current === null ? window.xcotton?.pp?.loadPlans(null) : window.xcotton?.pp?.loadPlans(current), ) .catch(console.error) return () => { void xcottonInit() .then(() => window.xcotton?.pp?.loadPlans(null)) .catch(console.error) } })</script>
<div id="xcotton-pp-panel" hidden={!variant}></div>import { onMount, onCleanup, createEffect } from 'solid-js'import { xcottonInit, syncXcotton } from '../lib/xcotton'import { onCartUpdatedByStorefront } from '../lib/cart'
export function XcottonStartup(props: { variant: string | number | null cartId?: string}) { onMount(() => { const cleanup = (() => { return onCartUpdatedByStorefront((cart) => syncXcotton(cart.id).catch(console.error), ) })() onCleanup(() => { cleanup() void xcottonInit() .then(() => window.xcotton?.pp?.loadPlans(null)) .catch(console.error) }) }) createEffect(() => { const cartId = props.cartId void (cartId ? syncXcotton(cartId) : xcottonInit()).catch(console.error) }) createEffect(() => { const variant = props.variant void xcottonInit() .then(() => variant === null ? window.xcotton?.pp?.loadPlans(null) : window.xcotton?.pp?.loadPlans(variant), ) .catch(console.error) }) return <div id="xcotton-pp-panel" hidden={!props.variant} />}启动组件持续观察 cartId:挂载后异步恢复已有购物车或切换购物车时,更新该属性会调用 syncXcotton(cartId);同值重渲染不会重复请求。站点自己写车仍通过独立的订阅调用 syncXcotton(cartId),不要把 SDK 通知转发到该订阅。
示例从 lib/cart.ts 导入了两个需要你按站点购物车逻辑实现的函数:
| 函数 | 作用 |
|---|---|
reloadCartView() | 重新查询购物车并刷新站点界面。只读操作,不得写入购物车。 |
onCartUpdatedByStorefront(handler) | 站点自身写完购物车后触发 handler(cart),并返回取消订阅函数。SDK 发起的写入不得触发该事件。 |
启动组件的 variant 参数为商品页当前选中的规格 ID(字符串或数字),用于触发 PP 报价,详见 接入延保险。未开启 PP 时传 null 即可。
可选:自定义 SP 详情按钮
Section titled “可选:自定义 SP 详情按钮”若希望在面板以外的位置放置「保障详情」按钮,可调用 sp.showModal()。返回的 Promise 在弹窗打开后即完成,不会等待顾客关闭弹窗。弹窗内的保障按钮会按当前适配模式表达意愿:cart 模式写费用行,checkout 模式只记录意愿,checkout+ 不展示该按钮。
'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> )}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> )}<script setup lang="ts">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()}
function onClick() { void onShowSpDetailsClick().catch(console.error)}</script>
<template> <button type="button" @click="onClick">Protection details</button></template>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> )}<script setup lang="ts">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()}
function onClick() { void onShowSpDetailsClick().catch(console.error)}</script>
<template> <button type="button" @click="onClick">Protection details</button></template><script lang="ts"> 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() }
function onClick() { void onShowSpDetailsClick().catch(console.error) }</script>
<button type="button" onclick={onClick}>Protection details</button>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 参考。
- 购物车同步(必读):了解
syncCart与onCartChange的方向与边界。 - 适配模式为
checkout或checkout+:阅读 适配模式。 - 已开启 PP:阅读 接入延保险。
- 购物车读写经过自有 API 或 BFF:阅读 使用自己的购物车 API。