上下文

context 对象用于在不同 Hook 之间共享数据。它在整个请求生命周期中持续存在,并且可以在每个 Hook 中访问。通过向 req.context 设置属性,你可以有效地在多个 Hook 之间共享逻辑。

何时使用 Context

Context 为解决以下难题提供了方案:

  1. 在 Hook 之间传递数据:当多个 Hook 需要来自第三方 API 的数据时,可以在 beforeChange 中获取并使用,然后在 afterChange 钩子中再次使用,而无需重复获取。
  2. 防止无限循环:在触发 afterChange 钩子的同一文档上调用 payload.update() 会导致无限循环,可以通过在 context 中设置无操作条件来控制流程。
  3. 向 Local API 传递数据:在 req.context 上设置值并传递给 payload.create(),你可以在不添加额外字段的情况下向钩子提供额外数据。
  4. 在钩子与中间件或自定义端点之间传递数据:钩子可以在多个集合之间设置上下文,然后在最终的 postMiddleware 中使用。

如何使用 Context

让我们看看如何在前述两种场景中使用 context 的示例:

在钩子间传递数据

要在钩子之间传递数据,你可以在请求生命周期的早期钩子中将值赋给上下文(context),然后在后续钩子的上下文中获取这些值。

例如:

import type { CollectionConfig } from 'payload'

const Customer: CollectionConfig = {
  slug: 'customers',
  hooks: {
    beforeChange: [
      async ({ context, data }) => {
        // 将customerData赋值给上下文供后续使用
        context.customerData = await fetchCustomerData(data.customerID)
        return {
          ...data,
          // 这里使用的一些数据
          name: context.customerData.name,
        }
      },
    ],
    afterChange: [
      async ({ context, doc, req }) => {
        // 直接使用context.customerData而无需再次获取
        if (context.customerData.contacted === false) {
          createTodo('Call Customer', context.customerData)
        }
      },
    ],
  },
  fields: [
    /* ... */
  ],
}

防止无限循环

假设你有一个 afterChange 钩子,你想在其中执行计算(因为计算所需的文档 ID 在 afterChange 钩子中可用,但在 beforeChange 钩子中不可用)。计算完成后,你想用计算结果更新文档。

错误示例:

import type { CollectionConfig } from 'payload'

const Customer: CollectionConfig = {
  slug: 'customers',
  hooks: {
    afterChange: [
      async ({ doc, req }) => {
        await req.payload.update({
          // 危险:在 afterChange 中更新相同 slug 的 collection 会导致无限循环!
          collection: 'customers',
          id: doc.id,
          data: {
            ...(await fetchCustomerData(data.customerID)),
          },
        })
      },
    ],
  },
  fields: [
    /* ... */
  ],
}

为了避免上述情况,我们需要告诉 afterChange 钩子在执行更新后不再运行(从而避免再次更新自身)。我们可以通过 context 来解决这个问题。

修正后的示例:

import type { CollectionConfig } from 'payload'

const MyCollection: CollectionConfig = {
  slug: 'slug',
  hooks: {
    afterChange: [
      async ({ context, doc, req }) => {
        // 如果之前设置了标志则返回
        if (context.triggerAfterChange === false) {
          return
        }
        await req.payload.update({
          collection: contextHooksSlug,
          id: doc.id,
          data: {
            ...(await fetchCustomerData(data.customerID)),
          },
          context: {
            // 设置标志以防止再次运行
            triggerAfterChange: false,
          },
        })
      },
    ],
  },
  fields: [
    /* ... */
  ],
}

TypeScript

context 的默认 TypeScript 接口是 { [key: string]: unknown }。如果你希望在项目中或为他人开发插件时使用更严格的类型定义,可以通过 declare 语法来覆盖它。

这被称为"类型增强"(type augmentation),是 TypeScript 的一项功能,允许我们为现有类型添加新的类型定义。只需在任何 .ts.d.ts 文件中添加如下代码:

import { RequestContext as OriginalRequestContext } from 'payload'

declare module 'payload' {
  // 创建一个新接口,将你的额外字段与原始接口合并
  export interface RequestContext extends OriginalRequestContext {
    myObject?: string
    // ...
  }
}

这将会为每个 context 对象添加类型为 string 的 myObject 属性。请确保正确遵循此示例,因为如果操作不当,类型增强可能会破坏你的类型定义。