> ## 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.

# Hooks

> 改变 Aftersell Cart 的行为方式：转换行、用 Storefront 数据增强行、塑造订阅选项，以及控制加入购物车。

[事件](/zh/aftersell/cart/sdk-events)让你*响应*购物车，[操作](/zh/aftersell/cart/sdk-actions)让你*更改*购物车，而 **hooks** 则改变购物车本身的行为方式：行如何渲染、携带什么数据，以及加入购物车时会发生什么。

Hooks 位于 `window.aftersell.cart.hooks` 下。

<Note>
  Hook 改变的是购物者**看到的内容**；操作改变的是**购物车里的内容**。用转换隐藏免费赠品行，它仍留在购物车中并计入总额。用 [`removeItem`](/zh/aftersell/cart/sdk-actions#removeitemkey) 移除它才是真正拿出来。
</Note>

<Note>
  Hooks 是设置类调用，所以可以安全地在脚本最顶部注册，无需等待 `ready()`。在你购物车的 **Initialization** 脚本中注册它们（参阅[自定义脚本](/zh/aftersell/cart/custom-scripts)）。
</Note>

<div id="how-registration-works">
  ## 注册的工作方式
</div>

每个 hook 都是一个 `register*` 方法。你用自己的函数调用它；它返回一个**注销函数**，调用即可移除你的注册。

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
const off = window.aftersell.cart.hooks.registerSkipAddToCartRule((form) =>
  form.hasAttribute('data-skip-aftersell')
);

// later: off();
```

注册是**累加式的**，所以你的函数会与其他所有函数一起运行。这很重要，因为你的脚本很少是页面上唯一的脚本：订阅应用、套装应用和主题本身可能都针对同一个 hook 注册。它们谁都不能替换你的，而在你之后加载的任何东西也不能静默丢弃你注册的内容。

| Hook                                                                                      | 作用                                                                   | 多个注册时                  |
| ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------- |
| [`registerLineTransform`](#registerlinetransform)                                         | 隐藏或重新标记单个行。                                                          | 全部运行，按注册顺序。            |
| [`registerLineComparator`](#registerlinecomparator)                                       | 重新排序渲染的行。                                                            | 作为决胜规则组合。              |
| [`registerCartEnricher`](#registercartenricher)                                           | 向每行附加额外的 Storefront 数据。                                              | 全部运行；每个 `id` 是自己的命名空间。 |
| [`registerSubscriptionOptionsTransform`](#registersubscriptionoptionstransform)           | 隐藏或重命名行的销售计划。                                                        | 全部运行；补丁按计划、按字段合并。      |
| [`registerDefaultSubscriptionOptionSelector`](#registerdefaultsubscriptionoptionselector) | 选择预选哪个计划。                                                            | 第一个非 `null` 的答案胜出。     |
| [`registerSkipAddToCartRule`](#registerskipaddtocartrule)                                 | 让特定表单绕过购物车。参阅[加入购物车拦截](/zh/aftersell/cart/add-to-cart-interception)。 | 任何返回 `true` 的规则即跳过。    |

抛出错误的 hook 或不是函数的 hook 会被跳过；其余照常运行，购物车继续工作。一个损坏的集成无法搞垮加入购物车、订阅选择器或排序。

反过来说，你自己损坏的 hook 会**静默**失败：什么都不会到达浏览器控制台。参阅[调试](/zh/aftersell/cart/sdk-overview#debugging)了解这些失败在哪里显现。

***

<div id="registerlinetransform">
  ## registerLineTransform
</div>

`registerLineTransform(fn)` 在每个购物车行渲染之前运行。用它来隐藏某行或改变其显示方式，而不触及购物者购物车中的实际内容。

该函数接收一个只读的行加上一组 setter。它返回一个注销函数。

| Setter                            | 效果                                                                              |
| --------------------------------- | ------------------------------------------------------------------------------- |
| `setHidden(bool)`                 | 从抽屉中隐藏该行。它仍留在购物车中并计入总额。                                                         |
| `setTitle(string)`                | 更改显示的标题。                                                                        |
| `setVariantTitle(string \| null)` | 更改显示的变体标签。                                                                      |
| `setInternalProperties(obj)`      | 合并仅用于渲染的属性。永远不会持久化到 Shopify。用于[组合套装行](/zh/aftersell/cart/sdk-use-case-bundles)。 |

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
// Hide free gift lines from the drawer. The cart total is unaffected.
const off = window.aftersell.cart.hooks.registerLineTransform((line) => {
  if (line.finalLinePrice === 0) {
    line.setHidden(true);
  }
  if (line.sellingPlan) {
    line.setVariantTitle(`Delivered ${line.sellingPlan.name.toLowerCase()}`);
  }
});

// later: off();
```

<Warning>
  转换只改变渲染的内容。它不能更改价格、数量或行的身份。这些请使用[操作](/zh/aftersell/cart/sdk-actions)。
</Warning>

\*\*适用场景：\*\*隐藏随购赠品或应用注入的行、重新标记订阅行、标记折扣商品、隐藏购物者不应单独管理的套装组件。

`setInternalProperties` 是套装组合背后的 setter：将规范的套装属性盖章到每一行上，就是让第三方应用的多个独立购物车行渲染为一个商品的方式。参阅[组合来自其他应用的套装行](/zh/aftersell/cart/sdk-use-case-bundles)。

<div id="registerlinecomparator">
  ## registerLineComparator
</div>

一个与 `Array.prototype.sort` 期望的形式相同的比较器。它在隐藏和重命名之后运行，所以看到的是转换后的行。

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
// Subscriptions first, then everything else.
window.aftersell.cart.hooks.registerLineComparator((lineA, lineB) => {
  return (lineB.sellingPlan ? 1 : 0) - (lineA.sellingPlan ? 1 : 0);
});
```

比较器**作为决胜规则组合**：第一个返回非零值的比较器决定这一对的顺序，其余的只在平局时被咨询。对你没有意见的对返回 `0`。这样就把决定权交给下一个比较器，而不是强加一个顺序。

\*\*适用场景：\*\*将订阅或高价值商品浮到顶部、将免费赠品和附加产品沉到底部、让赞助产品保持第一位。

<div id="registercartenricher">
  ## registerCartEnricher
</div>

`registerCartEnricher(registration)` 从 Shopify Storefront API 获取额外的产品或变体数据，并将其附加到每个匹配的购物车行的 `line.metadata[id]` 上。用它来展示元字段、标签或 Storefront API 暴露的任何其他内容，无需 Aftersell 做任何代码改动。

| 字段         | 类型                               | 描述                                                         |
| ---------- | -------------------------------- | ---------------------------------------------------------- |
| `id`       | `string`                         | 结果的命名空间；它落在 `line.metadata[id]`。必须唯一；使用相同 `id` 的第二次注册会被忽略。 |
| `onType`   | `'Product'` 或 `'ProductVariant'` | 片段针对哪个节点。也是连接键（产品 ID 与变体 ID）。                              |
| `fragment` | `string`                         | 拼接进 Storefront 查询的 GraphQL 字段选择（不带外层花括号）。花括号必须配对。          |

返回一个**注销函数**。

每当购物车加载或变化时，Aftersell 会为购物车上的每个产品或变体获取你的片段并附加结果。获取是非阻塞的：购物车立即渲染，数据到达后重新发出 `cart_updated`。缓慢或失败的片段永远不会延迟或破坏购物车。

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerCartEnricher({
  id: 'pricing',
  onType: 'ProductVariant',
  fragment: `
    anchorPrice: metafield(namespace: "custom", key: "anchor_price") { value }
    subscriberPrice: metafield(namespace: "custom", key: "subscriber_price") { value }
  `,
});

// Read it once the data arrives.
window.aftersell.cart.events.on('cart_updated', (state) => {
  state.items.forEach((line) => {
    const anchor = line.metadata.pricing?.anchorPrice;
    if (anchor) console.log(line.title, 'anchor price', anchor.value);
  });
});
```

由于增强是异步的，读取时务必加防护，因为在首次获取完成之前 `line.metadata.pricing` 是 `undefined`，而 `metadata` 本身默认为 `{}`。

\*\*适用场景：\*\*把元字段拉到每一行上（配送预估、成分列表、"单独发货"标志、忠诚度倍数），并通过[自定义代码区块](/zh/aftersell/cart/custom-code-blocks)渲染。参阅[在购物车行上显示元字段数据](/zh/aftersell/cart/sdk-use-case-metafields)。

<Note>
  多个增强器可以愉快共存，因为每个 `id` 是自己的命名空间，它们的数据永远不会冲突。
</Note>

<Warning>
  增强的值从 Storefront API 原样返回，**未经**消毒。请将它们渲染为文本，而不是原始 HTML。
</Warning>

<div id="registersubscriptionoptionstransform">
  ## registerSubscriptionOptionsTransform
</div>

隐藏或重命名某行提供的销售计划。你的函数接收只读的选项加上 setter，不返回任何内容。

| Setter            | 效果          |
| ----------------- | ----------- |
| `setHidden(bool)` | 从选择器中隐藏该计划。 |
| `setName(string)` | 更改显示的计划名称。  |

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerSubscriptionOptionsTransform((options, context) => {
  // context: { productId, variantId }
  options.forEach((option) => {
    if (option.discountPercent === 0) option.setHidden(true);
    option.setName(option.name.replace('Every ', ''));
  });
});
```

\*\*使用 setter 而不是返回列表，是为了让多个脚本能够共存。\*\*如果这个 hook 返回一个数组，一个只关心某个计划的转换会自然地写 `options.filter(...)`，从而在不经意间静默删除其他所有应用的计划。使用 setter，你只能描述你自己的修改：补丁按计划、按字段合并，同一计划同一字段上的真正冲突由最后写入者胜出。抛出错误的转换不贡献任何内容，其他转换仍然生效。

每个转换看到的都是*原始*选项，而不是打了一半补丁的视图，所以注册顺序不会改变你读到的内容。

<Note>
  计划顺序保持 Shopify 返回的样子，所以转换无法重新排序。要控制首先提供哪个计划（以及一次性购买升级按钮订阅哪个计划），请使用 [`registerDefaultSubscriptionOptionSelector`](#registerdefaultsubscriptionoptionselector)，它会将其选择提升到最前面。
</Note>

你也不能*添加*计划或更改价格：`discountPercent` 没有 setter，因为 Shopify 在结账时不会兑现的计划，在选择器中只会是一个空头承诺。

<div id="registerdefaultsubscriptionoptionselector">
  ## registerDefaultSubscriptionOptionSelector
</div>

选择在某行上预选哪个计划。返回一个计划 `id`，或返回 `null` 表示放弃选择。

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerDefaultSubscriptionOptionSelector((options) => {
  const best = options
    .slice()
    .sort((optionA, optionB) => optionB.discountPercent - optionA.discountPercent)[0];
  return best ? best.id : null;
});
```

**第一个返回可用计划 id 的选择器胜出**，所以对你不关心的行返回 `null`，而不要猜测。这样就把决定权交给下一个选择器，而不是覆盖它。与该行上任何计划都不匹配的 id 会被视同 `null` 并同样让位，所以过期的 id 不会把选择器清空。

你的函数接收 `(options, context)`，与选项转换获得的 `context` 相同。

<div id="registerskipaddtocartrule">
  ## registerSkipAddToCartRule
</div>

返回 `true` 可以让特定的产品表单正常加入购物车，完全绕过 Aftersell。这对需要自己的重定向或处理的表单很有用。

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerSkipAddToCartRule((form) =>
  form.hasAttribute('data-skip-aftersell')
);
```

**任何 `true` 都会跳过**，所以让你的规则保持窄范围，只匹配你拥有的特定表单，对其他一切返回 `false`。规则按注册顺序求值并在第一个 `true` 处停止，所以不要在规则中放副作用：你的规则是否运行，取决于在它之前注册了什么。

<Tip>
  如果你能控制表单的标记，你根本不需要 hook：给 `<form>` 添加类 **`aftersell-cart-skip-atc`**，Aftersell 就不会碰它。当你无法编辑标记，或决定取决于只有你的代码才知道的东西时，才使用这个 hook。
</Tip>

\*\*适用场景：\*\*需要自己重定向的预购或询价表单、订阅应用的自定义流程、应直接进入结账的"立即购买"按钮。要为整个页面关闭拦截，请改用 [`skip_add_to_cart_interceptor`](/zh/aftersell/cart/sdk-configure#skip_add_to_cart_interceptor)，但优先使用这个 hook，因为它的作用范围限定在你指定的表单。

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

* **[购物车对象](/zh/aftersell/cart/sdk-cart-object)**：转换接收的行的结构。
* **[事件](/zh/aftersell/cart/sdk-events)**：你可以订阅的一切。
* **[操作](/zh/aftersell/cart/sdk-actions)**：读取和更改购物车。
* **[使用案例](/zh/aftersell/cart/sdk-use-cases)**：常见需求的完整解决方案。
