字段钩子
字段钩子(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 中,这是字段更改前的值。 |
req | Web 请求 对象。Local API 操作中会被模拟。 |
schemaPath | 字段在 schema 中的路径。 |
siblingData | 与当前 hook 运行字段相邻的兄弟字段数据。 |
siblingDocWithLocales | 包含所有 本地化 的兄弟文档数据。 |
siblingFields | 当前 hook 运行字段的兄弟字段。 |
value | 当前 Field 的值。 |
提示: 根据执行的操作有条件地限定逻辑范围是个好做法。例如,在编写 beforeChange
hook 时,你可能需要根据当前 operation
是 create
还是 update
来执行不同的逻辑。
beforeValidate
在 create
和 update
操作期间运行。该钩子允许你在服务器端验证传入数据之前添加或格式化数据。
请注意,这不会在客户端验证之前运行。如果你在前端渲染自定义字段组件并为其提供 validate
函数,验证的运行顺序将是:
validate
在客户端运行- 如果成功,
beforeValidate
在服务器端运行 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
}