Checkout extension 场景教程
本页收录基于 Checkout Extension 的实战场景。基础开发流程见 创建一个结账扩展;扩展点与 API 速查见 Checkout extension 参考。
场景一:在所有扩展点插入自定义内容
-
适用场景:开发调试阶段,快速确认每个扩展点在页面上的实际位置。
-
实现方式:遍历所有扩展点,在每个位置渲染一个显示自身名称的红色标签。
src/index.js:
import { extend } from 'shoplazza-extension-ui';
const extensionPoints = [
'Checkout::RenderBefore',
'Checkout::RenderAfter',
'Checkout::Head::RenderAfter',
'Checkout::FilledInformation::RenderAfter',
'Checkout::SpecialInstruction::RenderAfter',
'Checkout::Summary::RenderBefore',
'Checkout::Navigate::RenderBefore',
'Checkout::Navigate::RenderAfter',
'Checkout::ContactInformation::RenderBefore',
'Checkout::ContactInformation::RenderAfter',
'Checkout::ShippingLinesTitle::RenderBefore',
'Checkout::ShippingLinesTitle::RenderAfter',
'Checkout::ShippingList::RenderAfter',
'Checkout::ProductList::RenderBefore',
'Checkout::ProductList::RenderAfter',
'Checkout::Reductions::RenderBefore',
'Checkout::Reductions::RenderAfter',
'Checkout::SectionPayment::RenderBefore',
'Checkout::SectionPayment::RenderAfter',
'Checkout::ThankyouHeader::RenderBefore',
'Checkout::ThankyouContent::RenderBefore',
];
function renderLabel(extensionPoint) {
return `
<div style="color:#dc2626;font-size:12px;">
${extensionPoint}
</div>
`;
}
extensionPoints.forEach(extensionPoint => {
extend({
extensionPoint,
component: renderLabel(extensionPoint),
});
});
确认各扩展点的位置后,将 renderLabel 返回的 HTML 替换为实际业务内容即可。
示例截图:

场景二:API 调用与事件监听
-
适用场景:全面了解
CheckoutAPI的可用方法与事件,可作为开发新扩展时的调试工具和参考起点。 -
实现方式:在页面顶部渲染一组操作按钮,触发各类 API;同时监听价格、地址、步骤变化并在控制台输出。页面加载完成时打印所有 API 的初始值。
src/index.html:
<style>
.checkout-btn {
margin: 4px;
padding: 2px 4px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
background-color: #f0f0f0;
color: #333;
font-size: 12px;
line-height: 1.4;
transition: all 0.3s ease;
}
.checkout-btn:hover {
background-color: #e0e0e0;
}
</style>
<div data-coe-toolbar>
<button class="checkout-btn" data-action="stepNavToInformation">跳转至联系信息</button>
<button class="checkout-btn" data-action="stepNavToShipping">跳转至运输信息</button>
<button class="checkout-btn" data-action="stepNavToPayment">跳转至支付信息</button>
<button class="checkout-btn" data-action="doLogin">登录</button>
<button class="checkout-btn" data-action="doRegister">注册</button>
<button class="checkout-btn" data-action="doLogout">退出登录</button>
<button class="checkout-btn" data-action="goToHomePage">跳转到首页</button>
<button class="checkout-btn" data-action="locationHref">跳转到指定路径</button>
</div>
src/index.js:
import { extend } from 'shoplazza-extension-ui';
import template from './index.html';
extend({
extensionPoint: 'Checkout::RenderBefore',
component: template,
});
// ⚠️ 以下 log / console.log 仅用于演示,生产请删除或改为可关闭的日志开关
const LOG = '[checkout-event-api]';
const REGISTRY_KEY = '__CHECKOUT_EVENT_API__';
function log(message, ...args) {
console.log(`${LOG} ${message}`, ...args);
}
function getRegistry() {
if (!window[REGISTRY_KEY]) {
window[REGISTRY_KEY] = { shippingAddress: {} };
}
return window[REGISTRY_KEY];
}
function getApi() {
return window.CheckoutAPI;
}
function makeAction(name, fn) {
return async () => {
log(`[action] ${name} → 开始执行...`);
await fn();
log(`[action] ${name} → 完成`);
};
}
const actions = {
stepNavToInformation: makeAction('跳转至联系信息步骤', () => getApi().step.stepNavToInformation()),
stepNavToShipping: makeAction('跳转至运输信息步骤', () => getApi().step.stepNavToShipping()),
stepNavToPayment: makeAction('跳转至支付信息步骤', () => getApi().step.stepNavToPayment()),
doLogin: makeAction('登录', () => getApi().user.doLogin()),
doRegister: makeAction('注册', () => getApi().user.doRegister()),
doLogout: makeAction('退出登录', () => getApi().user.doLogout()),
goToHomePage: makeAction('跳转至首页', () => getApi().step.goToHomePage()),
locationHref: makeAction('跳转至指定路径', () => getApi().step.locationHref('/')),
};
// 用事件委托替代 inline onclick,避免全局命名空间污染
document.addEventListener('click', (event) => {
const btn = event.target.closest('[data-action]');
if (!btn || !btn.closest('[data-coe-toolbar]')) return;
const action = actions[btn.dataset.action];
if (action) action();
});
const MOUNT_INTERVAL_MS = 100;
const MOUNT_MAX_ATTEMPTS = 50; // 100ms * 50 ≈ 5 秒
function mount(attempt = 0) {
const api = getApi();
if (!api) {
if (attempt >= MOUNT_MAX_ATTEMPTS) {
console.warn(`${LOG} CheckoutAPI 未在预期时间内挂载,已放弃等待`);
return;
}
// 用 setTimeout 而不是 requestAnimationFrame,避免 60fps 忙等占用结账页 CPU
window.setTimeout(() => mount(attempt + 1), MOUNT_INTERVAL_MS);
return;
}
const reg = getRegistry();
if (reg.handlePricesChange) api.store.removePricesChangeCb(reg.handlePricesChange);
if (reg.handleAddressChange) api.address.removeShippingAddressChangeCb(reg.handleAddressChange);
if (reg.handleStepChange) api.step.removeStepChangeCb(reg.handleStepChange);
reg.handlePricesChange = (prices) => log('[prices-change]', prices);
reg.handleAddressChange = (patch) => {
reg.shippingAddress = { ...reg.shippingAddress, ...patch };
log('[address-change]', patch);
};
reg.handleStepChange = () => log('[step-change]', api.step.getStep());
api.store.onPricesChange(reg.handlePricesChange);
api.address.onShippingAddressChange(reg.handleAddressChange);
api.step.onStepChange(reg.handleStepChange);
console.group(`${LOG} 初始化完成`);
log('step:', api.step.getStep());
log('orderInfo:', api.store.getOrderInfo());
log('orderStatus:', api.store.getOrderStatus());
log('referInfo:', api.store.getReferInfo());
log('prices:', api.store.getPrices());
log('products:', api.summary.getProductList());
log('user.isLogin:', api.user.isLogin());
log('user.info:', api.user.getUserInfo());
log('shippingAddress:', api.address.getShippingAddress());
console.groupEnd();
}
mount();
关键点说明:
-
事件委托替代 inline onclick:所有按钮挂
data-action="xxx",统一在document监听click事件并通过closest('[data-action]')定位目标。 -
makeAction:统一包装异步操作,执行前后打印日志。 -
mount:用setTimeout+ 最大重试次数(约 5 秒)等待CheckoutAPI挂载,避免requestAnimationFrame60fps 忙等占用结账页 CPU。 -
注册事件监听前先调用对应的
remove*方法清除旧回调,防止热更新时重复注册。 -
getRegistry:将回调函数引用存储在window全局对象上,确保remove*时传入的是同一个函数引用。
场景三:地址自动填充到信用卡姓名
-
适用场景:买家在联系信息步骤填写收货地址后,跳转到支付步骤时,自动将姓名填入信用卡持卡人姓名输入框,减少重复输入。
-
结账布局说明:Shoplazza 支持三种结账布局,可在店铺后台「结账页编辑器 → 结账页基础配置 → 结账布局」中切换。本扩展通过同时监听
onShippingAddressChange和onStepChange两个事件,并在mount时判断当前步骤主动触发填充,覆盖三种布局下的不同填充时机,无需针对各布局单独适配。
核心逻辑:
-
监听收货地址变化,将每次变化的字段合并到本地缓存
-
监听步骤变化,当切换到
payment_method时触发填充 -
mount时若当前已经处于payment_method或contact_information步骤,直接尝试填充(覆盖刷新落到这两步的场景) -
填充时通过 DOM 查询信用卡姓名输入框,若输入框尚未渲染则每 100ms 重试,最多等待 5 秒
⚠️ DOM 选择器警告:
#card_first_name/#card_last_name等是 Shoplazza 结账页当前版本的内部 DOM 结构,可能随平台升级而变化。生产扩展应:(1) 尽量通过CheckoutAPI等公开接口实现需求;(2) 必须直接操作 DOM 时,加版本检测和优雅降级,并定期回归。
src/index.js:
// ⚠️ 以下 log / console.log 仅用于演示,生产请删除或改为可关闭的日志开关
const LOG = '[checkout-name-autofill]';
const REGISTRY_KEY = '__CHECKOUT_NAME_AUTOFILL__';
function log(message, ...args) {
console.log(`${LOG} ${message}`, ...args);
}
function getRegistry() {
if (!window[REGISTRY_KEY]) {
window[REGISTRY_KEY] = { shippingAddress: {} };
}
return window[REGISTRY_KEY];
}
function getApi() {
return window.CheckoutAPI;
}
function trimText(value) {
return typeof value === 'string' ? value.trim() : '';
}
function setInputValue(input, value) {
if (!input) return;
input.value = value;
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}
function tryFillCardholderName(address, remaining = 50) {
const firstName = trimText(address?.firstName);
const lastName = trimText(address?.lastName);
if (!firstName && !lastName) return;
const firstNameInput =
document.querySelector('#card_first_name') ||
document.querySelector('input[name="card_first_name"]');
const lastNameInput =
document.querySelector('#card_last_name') ||
document.querySelector('input[name="card_last_name"]');
if (!firstNameInput || !lastNameInput) {
if (remaining <= 0) return;
window.setTimeout(() => tryFillCardholderName(address, remaining - 1), 100);
return;
}
if (firstName) setInputValue(firstNameInput, firstName);
if (lastName) setInputValue(lastNameInput, lastName);
log('[cardholder-autofill] 填充完成', { firstName, lastName });
}
function handleAddressChange(patch) {
const reg = getRegistry();
reg.shippingAddress = { ...reg.shippingAddress, ...patch };
log('[address-change]', patch);
tryFillCardholderName(reg.shippingAddress);
}
function handleStepChange() {
if (getApi().step.getStep() === 'payment_method') {
tryFillCardholderName(getRegistry().shippingAddress);
}
}
const MOUNT_INTERVAL_MS = 100;
const MOUNT_MAX_ATTEMPTS = 50; // 100ms * 50 ≈ 5 秒
function mount(attempt = 0) {
const api = getApi();
if (!api) {
if (attempt >= MOUNT_MAX_ATTEMPTS) {
console.warn(`${LOG} CheckoutAPI 未在预期时间内挂载,已放弃等待`);
return;
}
// 用 setTimeout 替代 requestAnimationFrame,避免 60fps 忙等占用结账页 CPU
window.setTimeout(() => mount(attempt + 1), MOUNT_INTERVAL_MS);
return;
}
const reg = getRegistry();
if (reg.handleAddressChange) api.address.removeShippingAddressChangeCb(reg.handleAddressChange);
if (reg.handleStepChange) api.step.removeStepChangeCb(reg.handleStepChange);
reg.handleAddressChange = handleAddressChange;
reg.handleStepChange = handleStepChange;
reg.shippingAddress = api.address.getShippingAddress();
api.address.onShippingAddressChange(handleAddressChange);
api.step.onStepChange(handleStepChange);
// 当前已在支付步骤或联系信息步骤时,主动尝试一次填充(覆盖刷新/直接进入这些步骤的场景)
const currentStep = api.step.getStep();
if (currentStep === 'payment_method' || currentStep === 'contact_information') {
tryFillCardholderName(reg.shippingAddress);
}
}
mount();
关键点说明:
-
tryFillCardholderName:查找信用卡姓名输入框,若未渲染则每 100ms 重试,最多尝试 50 次(约 5 秒)。同时兼容 ID 选择器和 name 属性选择器两种 DOM 结构。 -
setInputValue:填充 input 值后手动触发input和change事件,确保页面框架能感知到值的变化。 -
handleAddressChange:地址每次变化时合并最新 patch,并尝试立即填充(覆盖单步结账布局场景)。 -
handleStepChange:步骤切换到支付页时触发填充(覆盖多步结账布局场景)。 -
mount中的初始填充:处理页面直接加载在特定步骤时的填充(覆盖直接进入支付页的场景)。 -
本扩展无需渲染 UI,不调用
extend,仅在后台运行逻辑。
场景四:根据购物车金额展示免运费进度条
-
适用场景:引导买家凑单。当商品小计低于阈值时,提示还差多少金额解锁免运费;达标后展示已享免运费。购物车变化时进度条实时更新。
-
实现思路:通过
CheckoutAPI.store.getPrices()读取商品小计,在某个结账扩展点渲染提示条,并在CheckoutAPI.store.onPricesChange()回调里重新渲染。
本场景聚焦业务逻辑。
CheckoutAPI的挂载等待写法(mount)和完整 API 巡览,参见上文场景二。
本场景只是展示提示条,并不会真的免运费。真正的免运费规则需由商家在店铺后台中配置。请让 FREE_SHIPPING_THRESHOLD 与该规则保持一致,否则可能出现“进度条说免运、结账却照收运费”的不一致。
src/index.js:
import { extend } from 'shoplazza-extension-ui';
// 免运费阈值(按店铺币种)。请按你的活动修改
// (例如改为从扩展设置读取,而非写死)。
const FREE_SHIPPING_THRESHOLD = 120;
// `CheckoutAPI` 只有在结账页完全加载后才挂载到 `window` 上。
// 过早访问 `window.CheckoutAPI` 会得到 `undefined`,
// 因此用轮询等待,而非直接读取。
function mount(cb) {
let tries = 0;
const timer = setInterval(() => {
if (window.CheckoutAPI) {
// 已就绪:停止轮询,把 API 交给回调。
clearInterval(timer);
cb(window.CheckoutAPI);
} else if (++tries > 50) {
// 约 5 秒后(50 次 × 100ms)放弃,避免无限循环。
clearInterval(timer);
console.warn('CheckoutAPI not mounted in time');
}
}, 100);
}
// 根据当前商品小计生成提示条的 HTML 字符串。
function render(subtotal) {
const remaining = FREE_SHIPPING_THRESHOLD - subtotal;
// 低于阈值:提示还差多少金额;达标:提示已解锁免运费。
const text =
remaining > 0
? `Spend $${remaining.toFixed(2)} more to unlock free shipping`
: 'You have unlocked free shipping!';
return `<div style="padding:12px;background:#f5f5f5;">${text}</div>`;
}
mount((api) => {
// 在固定的结账扩展点渲染(或重渲染)提示条。
// `subtotalPrice` 是字符串,运算前先 `parseFloat`。
const draw = (prices) =>
extend({
extensionPoint: 'Checkout::Summary::RenderBefore',
component: render(parseFloat(prices.subtotalPrice)),
});
// 先用初始价格画一次……
draw(api.store.getPrices());
// ……之后每当购物车金额变化就自动重画。
api.store.onPricesChange((prices) => draw(prices));
});
