# 结账扩展参考

> Shoplazza 结账扩展机制参考：extend 函数、它渲染的 HTML 模板，以及用 deleteTarget 隐藏原生结账模块。

# Checkout extension 参考

本页讲 Checkout Extension 的运行机制：`extend` 函数、它渲染的 HTML 模板，以及怎么隐藏结账页的原生模块。扩展点清单见[扩展点](/docs/app/extensions/checkout/extension-points)，数据与事件接口见 [CheckoutAPI 参考](/docs/app/extensions/checkout/checkout-api)。开发流程见 [创建一个结账扩展](/docs/app/extensions/checkout/add-checkout-extension)；实战场景见 [Checkout extension 场景教程](/docs/app/extensions/checkout/extension-recipes)。

## 扩展点（Extension Points）

扩展点决定自定义内容插入到页面的具体位置。在 `extend()` 的 `extensionPoint` 字段中指定对应名称，内容即会渲染在该位置。

结账页和感谢页各有一组固定的扩展点，按所在的页面区域分组。其中一部分是动态扩展点：它们跟着数据走，每个商品行、每个物流方案、每个弹窗各渲染一次，名字里带着那一行的 id，要在运行时拼出来。

完整清单、每个扩展点所属的区域，以及可以配合隐藏的原生模块，见[扩展点](/docs/app/extensions/checkout/extension-points)。

## 隐藏原生模块（deleteTarget）

`deleteTarget` 用于隐藏结账页的原生模块，比如各区块标题、地址簿、步骤面包屑。

在扩展目录下新建 `extension.json`，与 `shoplazza.extension.toml` 同级：

```text
extensions/
└── my-checkout/
    ├── shoplazza.extension.toml
    ├── extension.json            # 你只需要写 deleteTarget
    ├── package.json
    └── src/
```

**`extension.json`**：

```json
{ "deleteTarget": ["contactInformationHeader", "shippingAddressHeader"] }
```

让配置生效：

1. 把 Shoplazza CLI 升级到 2.0.10 或更高版本。
2. 重启 `shoplazza app dev` 预览，或执行 `shoplazza app deploy` 发布。`shoplazza app dev` 不监听 `extension.json`，每次改完该文件都要重启。
3. 恢复显示：把数组清空成 `"deleteTarget": []`，再部署一次。
4. CLI 会自动往 `extension.json` 回写 `extensionId` 和 `version`。这两个字段保留即可，不用手写，也不要删。

### 可隐藏的模块

| Target | 位置 | 说明 |
|---|---|---|
| `header` | 页头 | 顶部店名 / logo 区 |
| `navigate` | 页头 | 步骤面包屑，如 Information、Shipping、Payment |
| `loginOrLogout` | 页头 | 登录 / 退出链接 |
| `returnBtn` | 页头 | 返回链接，与提交按钮并列渲染 |
| `contactInformation` | 联系信息 | 整个联系信息模块 |
| `contactInformationHeader` | 联系信息 | 「Contact」标题 |
| `contactEmail` | 联系信息 | 邮箱输入框 |
| `contactPhone` | 联系信息 | 手机号输入框 |
| `emailOrPhone` | 联系信息 | 邮箱与手机号合并输入框，店铺配成二选一时使用 |
| `contactSubscribe` | 联系信息 | 订阅营销邮件的勾选项 |
| `shippingAddress` | 地址 | 整个收货地址模块 |
| `shippingAddressHeader` | 地址 | 「Shipping address」标题 |
| `shippingAddressBook` | 地址 | 地址簿，用于选择已存地址 |
| `securityIdentifier` | 地址 | 安全与隐私标识 |
| `addressCard` | 地址 | 已填信息卡片，出现在配送步和支付步 |
| `billingAddress` | 地址 | 支付步的账单地址模块 |
| `deliveryMethod` | 配送与自提 | 配送方式模块，用于切换快递和自提 |
| `deliveryMethodHeader` | 配送与自提 | 配送方式标题 |
| `shippingList` | 配送与自提 | 物流方案列表 |
| `shippingLinesTitle` | 配送与自提 | 物流方案列表标题 |
| `pickupInformation` | 配送与自提 | 自提信息模块 |
| `pickupInformationHeader` | 配送与自提 | 自提信息标题 |
| `pickupAddress` | 配送与自提 | 自提点地址列表 |
| `pickupAddressHeader` | 配送与自提 | 自提点地址标题 |
| `paymentHeader` | 支付 | 支付方式列表标题 |
| `payPalExpress` | 支付 | PayPal 快捷支付按钮 |
| `infoSubmit` | 提交按钮 | 信息步的「继续到配送」 |
| `shippingSubmit` | 提交按钮 | 配送步的「继续到支付」 |
| `paymentSubmit` | 提交按钮 | 支付步的「提交订单」 |
| `orderSummary` | 订单摘要 | 整个订单摘要，桌面端在右栏，移动端在顶部 |
| `orderSummaryHeader` | 订单摘要 | 订单摘要标题，移动端可折叠 |
| `productList` | 订单摘要 | 商品行列表 |
| `ProductListCover` | 订单摘要 | 商品行缩略图 |
| `productListSkuProperties` | 订单摘要 | 商品行的自定义属性，`properties` 非空才渲染 |
| `priceList` | 价格明细 | 整个价格明细区 |
| `priceListSubTotal` | 价格明细 | 小计 |
| `priceListShippingTotal` | 价格明细 | 运费 |
| `priceListTaxTotal` | 价格明细 | 税费 |
| `priceListShippingTaxTotal` | 价格明细 | 运费税 |
| `priceListGiftCard` | 价格明细 | 礼品卡抵扣 |
| `totalPrice` | 价格明细 | 合计 |
| `reductions` | 优惠与备注 | 优惠码与礼品卡输入区 |
| `mobileCoupon` | 优惠与备注 | 移动端的优惠码输入框，桌面端隐藏 |
| `giftCardTag` | 优惠与备注 | 已应用的优惠码或礼品卡标签，带移除按钮 |
| `specialInstruction` | 优惠与备注 | 配送步的订单备注输入框 |
| `thankyouHeader` | 感谢页 | 结账完成页头部 |
| `thankyouContent` | 感谢页 | 结账完成页内容 |
| `thankyouFooter` | 感谢页 | 结账完成页底部 |
| `thankyouShippingInfo` | 感谢页 | 物流信息块，有物流的订单才展示 |
| `thankyouFailOrderStatus` | 感谢页 | 订单取消或失败时展示的失败信息 |
| `thankyouPageGiftCardAddress` | 感谢页 | 虚拟商品订单展示的账单地址与礼品卡信息 |
| `thankyouPagePickupAddress` | 感谢页 | 自提订单展示的自提地址 |

## CheckoutAPI 参考

`CheckoutAPI` 是结账页平台挂在 `window.CheckoutAPI` 上的全局对象，结账页和感谢页都有。扩展通过它读取订单和顾客已经做出的选择、监听这些数据的变化，以及驱动结账页的部分界面。

它的方法按命名空间分组：`address`、`base`、`config`、`coupon`、`exception`、`extension`、`order`、`payment`、`pickup`、`step`、`store`、`summary`、`track`、`user`、`utils`。

全部命名空间、类型定义，以及调用之前要知道的几条规矩——对象什么时候才挂上、`on*` 和 `remove*` 怎么配对、`register*` 为什么不是监听器——见 [CheckoutAPI 参考](/docs/app/extensions/checkout/checkout-api)。

## extend 函数与 HTML 模板

### extend 函数

`extend` 是将内容注册到扩展点的核心函数，从 `shoplazza-extension-ui` 引入：

```javascript
import { extend } from 'shoplazza-extension-ui';

extend({
  extensionPoint: 'Checkout::Navigate::RenderBefore',
  component: '<h1>Hello, Shoplazza!</h1>',
});
```

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `extensionPoint` | `string` | 是 | 内容渲染在哪个扩展点，清单见[扩展点](/docs/app/extensions/checkout/extension-points) |
| `component` | `string` 或 `Promise<string>` | 是 | 要渲染的 HTML。普通字符串可以单独使用；传 `Promise<string>` 需要同时设 `type: 'spz'` |
| `type` | `'spz'` | 否 | 最小形式不传这个参数。设成 `'spz'` 会启用 LessJS 组件库和下面说的占位符替换 |
| `localeMap` | `object` | 否 | 按语言分组的文案，用来替换 `component` 里的 `${i18n('key')}` 占位符。只在 `type` 为 `'spz'` 时生效 |
| `id` | `string` | 否 | 扩展 id。传 `__EXTENSION_ID__`，CLI 构建时会把它替换成扩展名 |

不传 `type` 时，`component` 就是一段 HTML 字符串，原样渲染。这是上面那个最小形式，多数扩展只需要它。

设了 `type: 'spz'` 之后，SDK 在渲染前多做三件事：加载 LessJS 组件库、接受 `Promise<string>` 形式的 `component`，以及用 `localeMap` 替换 HTML 里的占位符：

```javascript
import { extend } from 'shoplazza-extension-ui';

extend({
  type: 'spz',
  extensionPoint: 'Checkout::Navigate::RenderBefore',
  component: Promise.resolve(
    "<div>${i18n('greeting')} - ${i18n('promo.line')}</div>"
  ),
  localeMap: {
    'en-US': { greeting: 'Hello', promo: { line: 'Free returns within 30 days' } },
    'zh-CN': { greeting: '你好', promo: { line: '30 天内免费退货' } },
  },
  id: __EXTENSION_ID__,
});
```

- `${i18n(...)}` 里的 key **必须带引号**：`${i18n('greeting')}` 会被替换，`${i18n(greeting)}` 渲染出来是 `undefined`。
- 嵌套 key 用点号打平，`promo: { line: … }` 要写成 `${i18n('promo.line')}`。
- 用哪一份文案取决于顾客的完整语言标记，也就是 `localeMap` 每一项的 key。
- HTML 写成普通引号字符串，不要写成模板字符串，否则 `${…}` 会被 JavaScript 先求值，轮不到 SDK 替换。
- 占位符 `{EXTENSION_POINT}` 会被替换成由扩展点名派生的 id，例如 `Checkout::Summary::RenderBefore` 对应 `Checkout-Summary-RenderBefore-ID`。

:::warning
**安全提示（XSS 风险）：** `component` 是**直接渲染的 HTML 字符串**，**不要把未转义的用户输入或外部 API 返回值拼接进去**，否则会导致 XSS 注入。任何动态内容先做 HTML 转义，或改用 `createElement` / `textContent` 等 DOM API 构造。

**多次调用同一扩展点：** 同一个 `extensionPoint` 可以被多次 `extend`，内容按调用顺序**追加**渲染，而不是覆盖。
:::

### HTML 模板

对于内容较复杂的扩展，推荐将 HTML 拆分到独立文件中管理。脚手架默认生成以下文件结构：

**`src/index.html`**（可选，组装入口）：

```html
<div>
  import './style.html'
  import './content.html'
  import './script.html'
</div>
```

:::note
**`import './xxx.html'` 不是 ES Module 语法。** 它是 `shoplazza-cli` 在构建时识别的**文本拼接指令**：在打包阶段，CLI 会把 `import './xxx.html'` 这一行原地替换为 `./xxx.html` 文件的内容（按出现顺序内联），而不是运行时动态加载。

- 该语法**只能写在 `src/index.html` 顶层**作为组装入口；写在 `<script>` 内部或 JS 文件里都不会被识别。
- 被 import 的 HTML 片段不应再写 `import`，避免递归。
- 内容简单或需要动态生成 HTML 时，可以跳过模板文件，直接在 `index.js` 中拼接字符串传给 `component`。
:::

**`src/style.html`**（可选，样式）：

```html
<style>
  .my-block {
    padding: 12px;
    background: #f5f5f5;
  }
</style>
```

**`src/content.html`**（可选，页面内容）：

```html
<div class="my-block">
  <h2>Hello, Shoplazza!</h2>
</div>
```

**`src/script.html`**（可选）：脚手架占位，若要在 HTML 模板片段内编写脚本，直接使用普通 `<script>` 标签即可：

```html
<script>
  document.querySelector('.my-block').addEventListener('click', function() {
    window.CheckoutAPI.step.goToHomePage();
  });
</script>
```

也可以不使用 HTML 模板，直接在 `index.js` 中构造 HTML 字符串传入 `component`，适合内容简单或需要动态生成的场景。

:::caution 结账页性能预算
结账页性能直接影响转化率，扩展 JS / CSS 应尽量精简：单个扩展 gzip 后建议 `< 30KB`，避免引入大型依赖；优先用 `setTimeout` / `IntersectionObserver` 等被动方式而不是高频轮询。
:::
