字段钩子

字段钩子(Field Hooks)是基于钩子概念,在文档的单个字段级别运行的函数。它们允许你在文档生命周期的特定事件中执行自定义逻辑。字段钩子提供了强大的能力,可以将你的逻辑与集合钩子和全局钩子隔离开来。

要为字段添加钩子,可以在字段配置中使用 hooks 属性:

import type { Field } from 'payload'

export const FieldWithHooks: Field = {
  // ...
  hooks: {
    // highlight-line
    // ...
  },
}

配置选项

所有 Field Hooks 都接受同步或异步函数数组。这些函数可以选择性地在操作继续前修改字段的返回值。所有 Field Hooks 都遵循相同的参数格式,不过某些参数可能根据特定 hook 类型而 undefined。

重要提示: 由于 GraphQL 的类型特性,从字段返回的数据类型变更会导致 GraphQL API 报错。如果需要改变数据的形状或类型,请考虑使用 Collection Hooks 或 Global Hooks。

要为字段添加 hooks,请在 Field Config 中使用 hooks 属性:

import type { Field } from 'payload';

const FieldWithHooks: Field = {
  name: 'name',
  type: 'text',
  // highlight-start
  hooks: {
    beforeValidate: [(args) => {...}],
    beforeChange: [(args) => {...}],
    beforeDuplicate: [(args) => {...}],
    afterChange: [(args) => {...}],
    afterRead: [(args) => {...}],
  }
  // highlight-end
}

所有 Field Hooks 都提供以下参数:

选项描述
collection当前 Hook 运行的 Collection。如果字段属于 Global,则为 null。
context在 Hooks 间传递的自定义上下文。详情。
data在 afterRead hook 中是完整文档。在 create 和 update 操作中,这是传入的操作数据。
field当前 Hook 运行的 Field。
findMany布尔值,表示在 afterRead hook 中是查找单个还是多个文档。
global当前 Hook 运行的 Global。如果字段属于 Collection,则为 null。
operation当前 hook 运行的操作名称。在 beforeValidate、beforeChange 和 afterChange hooks 中可用于区分 create 和 update 操作。
originalDoc在 update 操作中,这是应用更改前的文档。在 afterChange hook 中,这是结果文档。
overrideAccess布尔值,表示当前操作是否覆盖了 访问控制。
path字段在 schema 中的路径。
previousDoc在 afterChange hook 中,这是应用更改前的文档。
previousSiblingDoc仅在 beforeChange 和 afterChange hook 中,这是应用更改前的兄弟文档数据。
previousValue仅在 beforeChange 和 afterChange hooks 中,这是字段更改前的值。
reqWeb 请求 对象。Local API 操作中会被模拟。
schemaPath字段在 schema 中的路径。
siblingData与当前 hook 运行字段相邻的兄弟字段数据。
siblingDocWithLocales包含所有 本地化 的兄弟文档数据。
siblingFields当前 hook 运行字段的兄弟字段。
value当前 Field 的值。

提示: 根据执行的操作有条件地限定逻辑范围是个好做法。例如,在编写 beforeChange hook 时,你可能需要根据当前 operation 是 create 还是 update 来执行不同的逻辑。

beforeValidate

在 create 和 update 操作期间运行。该钩子允许你在服务器端验证传入数据之前添加或格式化数据。

请注意,这不会在客户端验证之前运行。如果你在前端渲染自定义字段组件并为其提供 validate 函数,验证的运行顺序将是:

  1. validate 在客户端运行
  2. 如果成功,beforeValidate 在服务器端运行
  3. validate 在服务器端运行
import type { Field } from 'payload'

const usernameField: Field = {
  name: 'username',
  type: 'text',
  hooks: {
    beforeValidate: [
      ({ value }) => {
        // 去除空格并转换为小写
        return value.trim().toLowerCase()
      },
    ],
  },
}

在这个例子中,beforeValidate 钩子用于处理 username 字段。该钩子接收字段的传入值,通过去除空格并转换为小写来转换它。这确保了用户名在数据库中以一致的格式存储。

beforeChange

在验证之后立即运行,beforeChange 钩子会在 create 和 update 操作中执行。在这个阶段,你可以确信将要保存到文档中的字段数据是符合你的字段验证要求的有效数据。

import type { Field } from 'payload'

const emailField: Field = {
  name: 'email',
  type: 'email',
  hooks: {
    beforeChange: [
      ({ value, operation }) => {
        if (operation === 'create') {
          // 为 'create' 操作执行额外的验证或转换
        }
        return value
      },
    ],
  },
}

在 emailField 中,beforeChange 钩子检查 operation 类型。如果操作是 create,则对 email 字段值执行额外的验证或转换。这允许针对特定操作应用逻辑到字段上。

afterChange

afterChange 钩子会在字段值被修改并保存到数据库后执行。这个钩子适用于基于字段新值进行后处理或触发副作用的情况。

import type { Field } from 'payload'

const membershipStatusField: Field = {
  name: 'membershipStatus',
  type: 'select',
  options: [
    { label: 'Standard', value: 'standard' },
    { label: 'Premium', value: 'premium' },
    { label: 'VIP', value: 'vip' },
  ],
  hooks: {
    afterChange: [
      ({ value, previousValue, req }) => {
        if (value !== previousValue) {
          // 当会员状态变更时记录日志或执行操作
          console.log(
            `用户 ID ${req.user.id} 将会员状态从 ${previousValue} 更改为 ${value}。`,
          )
          // 在这里可以实现跟踪不同等级间转换的操作
        }
      },
    ],
  },
}

在这个示例中,afterChange 钩子被用于 membershipStatusField 字段,该字段允许用户选择会员等级(Standard、Premium、VIP)。钩子会监控会员状态的变化。当变更发生时,它会记录更新日志,并可用于触发进一步操作,例如跟踪不同等级间的转换或通知用户会员权益的变化。

afterRead

afterRead 钩子在字段值从数据库读取后被调用。这个钩子非常适合对字段数据进行格式化或转换以便输出。

import type { Field } from 'payload'

const dateField: Field = {
  name: 'createdAt',
  type: 'date',
  hooks: {
    afterRead: [
      ({ value }) => {
        // 格式化日期以便显示
        return new Date(value).toLocaleDateString()
      },
    ],
  },
}

在这个例子中,dateField 的 afterRead 钩子使用 toLocaleDateString() 将日期格式化为更易读的形式。这个钩子改变了日期呈现给用户的方式,使其更加用户友好。

beforeDuplicate

beforeDuplicate 字段钩子在复制文档时针对每个语言环境(当使用本地化功能时)被调用。当文档具有完全相同的属性可能导致问题时,可以使用此钩子。它提供了一种方法来避免在 unique、required 字段上出现重复名称,或者当外部系统期望文档值不重复时使用。

这个钩子在 beforeValidate 和 beforeChange 钩子之前被调用。

默认情况下,对于唯一且必需的文本字段,Payload 会在原始文档值后追加 "- Copy"。如果你的字段有自己的处理逻辑,则不会添加默认后缀。你必须从 beforeDuplicate 钩子返回非唯一值以避免错误,或者在 collection 上启用 disableDuplicate 选项。

以下是一个数字字段的示例,该钩子通过递增数字来避免复制文档时出现唯一约束错误:

import type { Field } from 'payload'

const numberField: Field = {
  name: 'number',
  type: 'number',
  hooks: {
    // 将现有值加1
    beforeDuplicate: [
      ({ value }) => {
        return (value ?? 0) + 1
      },
    ],
  },
}

TypeScript

Payload 导出了用于字段钩子的类型,可以按如下方式访问和使用:

import type { FieldHook } from 'payload'

// 字段钩子类型是一个泛型,接受三个参数:
// 1: 文档类型
// 2: 值类型
// 3: 同级数据类型

type ExampleFieldHook = FieldHook<ExampleDocumentType, string, SiblingDataType>

const exampleFieldHook: ExampleFieldHook = (args) => {
  const {
    value, // 类型为上面指定的 `string`
    data, // 类型为你的 ExampleDocumentType 的 Partial
    siblingData, // 类型为 SiblingDataType 的 Partial
    originalDoc, // 类型为 ExampleDocumentType
    operation,
    req,
  } = args

  // 在这里执行某些操作...

  return value // 应该返回上面指定的 string 类型,或 undefined,或 null
}