> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aftersell.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 自定义脚本

> 使用 Initialization 和 On cart update 脚本插槽在 Aftersell Cart 中运行自定义 JavaScript。

自定义脚本让你可以使用 [Cart SDK](/zh/aftersell/cart/sdk-overview) 对购物车运行你自己的 JavaScript。在购物车编辑器的 **Cart settings → Custom script** 中添加脚本，那里的下拉菜单可在两个插槽之间切换：**Initialization** 和 **On cart update**。

在这些编辑器中编写纯 JavaScript，无需 `<script>` 标签。**On cart update** 有一个 **Reset to default** 操作，可恢复其初始模板；**Initialization** 没有，所以在清空之前请自行保留一份副本。

<Note>
  商家过去用脚本实现的很多功能现在已成为内置设置。请先查看[编写脚本之前](/zh/aftersell/cart/sdk-use-cases#before-you-write-a-script)：设置在购物车重新设计后仍能继续工作，而你的脚本可能不行。
</Note>

<div id="which-slot-to-use">
  ## 该使用哪个插槽
</div>

|            | Initialization                                                                                                                                          | On cart update                  |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| **运行时机**   | 一次，在购物车加载时。                                                                                                                                             | 首次加载之后的每次购物车变化。                 |
| **你编写的内容** | 整个脚本。                                                                                                                                                   | 仅处理函数主体。`cart_updated` 包装器是锁定的。 |
| **适用场景**   | 一次性注册行为：[`configure`](/zh/aftersell/cart/sdk-configure)、[`events.on`](/zh/aftersell/cart/sdk-events)、[`hooks.register*`](/zh/aftersell/cart/sdk-hooks)。 | 需要根据购物车当前内容重新评估的规则。             |
| **示例**     | 使用行转换隐藏免费赠品行。                                                                                                                                           | 让免费赠品与消费门槛保持同步。                 |

<div id="initialization">
  ## Initialization
</div>

**Initialization** 脚本在**购物车加载时运行一次**。它是你进行各项设置的入口：配置购物车行为、订阅事件以及注册 hooks。[SDK](/zh/aftersell/cart/sdk-overview) 以 `window.aftersell.cart` 的形式提供。

你在这里进行的设置调用（[`configure(...)`](/zh/aftersell/cart/sdk-configure)、[`events.on(...)`](/zh/aftersell/cart/sdk-events)、[`hooks.*`](/zh/aftersell/cart/sdk-hooks)）即使在购物车尚未完全启动之前，也可以安全地放在脚本顶部调用；它们会被缓冲，并在购物车启动后应用。读取或更改购物车的操作（如 [`addItem`](/zh/aftersell/cart/sdk-actions#additemvariantid-quantity) 或 [`getCart`](/zh/aftersell/cart/sdk-actions#getcart)）应在 [`ready()`](/zh/aftersell/cart/sdk-overview#ready) 或事件处理函数内部运行。

该插槽初始包含三个**已注释掉**的示例——每次添加商品时打开抽屉、响应 `cart_loaded`，以及隐藏免费赠品行——因此未经修改的 Initialization 脚本不会执行任何操作。取消注释其中一个来试用，或将它们替换掉。

这个插槽最自然的形态是**不涉及任何事件的一次性注册**：注册一次行为，然后让购物车从此开始应用它。在不改变总额的前提下从抽屉中隐藏免费赠品行，就是随附的示例：

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerLineTransform((line) => {
  if (line.finalLinePrice === 0) line.setHidden(true);
});
```

[`registerLineTransform`](/zh/aftersell/cart/sdk-hooks#registerlinetransform) 会在每一行渲染时运行，而 `setHidden` 仅影响显示，所以该行仍保留在购物车中并计入总额，只是不在抽屉中显示。参阅[隐藏和重新标记购物车行](/zh/aftersell/cart/sdk-use-case-hide-lines)以了解转换还能做什么。

读取购物车的操作要放在 `ready()` 内部：

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.ready().then(() => {
  const state = window.aftersell.cart.actions.getCart();
  console.log('Cart loaded with', state.itemCount, 'items');
});
```

访问购物车的 DOM 需要同样的等待，并且需要 [`shadowRoot`](/zh/aftersell/cart/sdk-overview#shadowroot)：购物车渲染在 shadow root 内部，所以 `document.querySelector` 看不到抽屉中的任何内容。

<Tip>
  需要在购物车加载**之前**根据市场、国家或货币进行分支判断？请改用 [`context`](/zh/aftersell/cart/sdk-overview#context)。它可以同步获取，无需 `ready()`，因此对于规则不适用的购物者，你可以完全跳过处理函数的注册。
</Tip>

<div id="on-cart-update">
  ## On cart update
</div>

**On cart update** 脚本在购物车每次变化时运行。它是围绕 `cart_updated` 订阅的锁定包装器，所以你只需编辑主体部分，你的代码会接收到更新后的 `cart`。

这个插槽适用于必须**在每次购物车变化时重新评估**的规则。免费赠品门槛是经典案例（消费满 \$75 送免费托特包），因为答案取决于当前内容，而且没有其他方式能在内容变化时通知你：

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('cart_updated', (cart) => {
  const GIFT_VARIANT_ID = 1234567890;
  const THRESHOLD = 7500;   // $75.00, in cents

  let giftLine = null;
  let subtotal = 0;
  (cart.items ?? []).forEach((line) => {
    if (line.variantId === GIFT_VARIANT_ID) giftLine = line;
    else subtotal += line.finalLinePrice;   // the gift itself never counts toward the threshold
  });

  const shouldHaveGift = subtotal >= THRESHOLD;
  const hasGift = Boolean(giftLine);

  // Bail when the cart already matches. This is the part that matters: adding or
  // removing an item fires cart_updated again, so without this check the handler
  // re-enters itself forever.
  if (shouldHaveGift === hasGift) return;

  if (shouldHaveGift) window.aftersell.cart.actions.addItem(GIFT_VARIANT_ID, 1);
  else window.aftersell.cart.actions.removeItem(giftLine.key);
});
```

<div id="keeping-the-cart-in-a-desired-state">
  ### 让购物车保持在期望状态
</div>

`if (shouldHaveGift === hasGift) return;` 这一行是这段代码安全的关键，并且它可推广到所有让购物车保持在期望状态的脚本。这个插槽既响应购物车变化又会引起变化，所以每次 `addItem` 或 `removeItem` 都会重新进入它。描述你想要的状态，将它与现有状态进行比较，并在两者已经一致时提前返回，这样处理函数一次执行就会收敛，而不会陷入循环。参阅[两条规则](/zh/aftersell/cart/sdk-events#the-two-rules)了解应避免的无防护版本，以及为什么载荷是只读的。

在较慢的商店中，还值得保留一个模块级的进行中标志，这样两次快速变化就不会在第一次添加完成之前同时发起添加。

<Note>
  `cart_updated` 仅在首次加载**之后**的变化时触发（[事件时序](/zh/aftersell/cart/sdk-events#cart_updated)），所以这个插槽中的脚本不会协调页面加载时已经满足条件的购物车。若要同时处理两种情况，请在 **Initialization** 插槽中用同一个函数订阅 `cart_loaded` 和 `cart_updated`。参阅[达到门槛时自动添加免费赠品](/zh/aftersell/cart/sdk-use-case-free-gift)。
</Note>

<div id="when-a-script-breaks">
  ## 脚本出错时
</div>

每个插槽都在自己的沙箱中运行，所以损坏的 **Initialization** 脚本不会阻止 **On cart update** 运行，两者也都无法破坏购物车本身。

不过在一个插槽内部，执行会**在第一个错误处停止**。该行以下的所有内容都会被跳过，这意味着后面的任何 `configure`、`events.on` 或 `hooks.register*` 都不会被注册。当代码看起来没问题却出现"我的处理函数从不触发"时，这通常就是原因。

购物车会在浏览器控制台中指出出错的行，并且每个插槽以自己的文件名运行（`aftersell-cart-init.js` 和 `aftersell-cart-cart-update.js`），所以你可以从 DevTools 的 Sources 面板打开任意一个并设置断点。参阅[调试](/zh/aftersell/cart/sdk-overview#debugging)了解确切的报错信息，以及捕获未输出到控制台的 hook 失败的调试通道。

由于 `cart_loaded` [会向迟来的订阅者重放](/zh/aftersell/cart/sdk-events#cart_loaded)，注册顺序从不重要。最安全的结构是先注册所有内容，然后在处理函数内部执行有风险的工作，这样抛出的错误只会被隔离在该处理函数中。

<div id="where-to-go-next">
  ## 后续阅读
</div>

* **[Cart SDK](/zh/aftersell/cart/sdk-overview)**：自定义脚本是运行 SDK 代码的方式。参阅 [configure](/zh/aftersell/cart/sdk-configure)、[events](/zh/aftersell/cart/sdk-events)、[actions](/zh/aftersell/cart/sdk-actions) 和 [hooks](/zh/aftersell/cart/sdk-hooks) 参考了解完整接口，参阅[购物车对象](/zh/aftersell/cart/sdk-cart-object)了解处理函数接收内容的结构，并参阅[使用案例](/zh/aftersell/cart/sdk-use-cases)获取现成的代码片段。
* **[自定义代码区块](/zh/aftersell/cart/custom-code-blocks)**：用于向购物车添加标记。请注意，自定义代码区块的 HTML 模式**不会**运行 JavaScript；逻辑请使用自定义脚本（或该区块的 React 模式）。
