ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Strapi 自定义字段(Custom Fields)机制深度解析:从 RFC 设计到源码实现

2026/9/7 17:23:57 拓冰建站 浏览量
Strapi 自定义字段(Custom Fields)机制深度解析:从 RFC 设计到源码实现 Strapi 自定义字段Custom Fields机制深度解析从 RFC 设计到源码实现【免费下载链接】strapi Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapi自定义字段是 Strapi 中替换现有字段输入以改善内容编辑体验的扩展机制。本文基于 Strapi 仓库中的 RFC 设计文档docs/docs/rfcs/01-custom-fields.md结合当前仓库源码完整讲解自定义字段的注册流程、启动时的类型转换原理、管理端Admin与服务端Server的双端设计、打包与共享方式以及 RFC 中明确列出的设计权衡与替代方案。读完本文你能够理解type: customField这一“逻辑类型”是如何在应用启动阶段被替换为真实数据类型的并能参照仓库内置的 color-picker 插件完成一个自定义字段插件的注册。一、自定义字段是什么按照 RFC 文档的定义Summary 一节Custom fields provide a way to replace the inputs of existing Strapi types to improve the content editing experience.即自定义字段提供替换 Strapi 现有字段类型输入控件的方式以改善内容编辑体验。它不是一种全新的存储类型而是对既有基础类型string、number、boolean 等的“输入层”包装在数据库中它仍然以底层 Strapi 数据类型持久化但在 Content-type BuilderCTB和管理面板中呈现为自定义的输入组件。要成为可用的自定义字段它必须在admin管理端和server服务端两端同时完成注册——这是整个机制的核心设计约束下文分端展开。二、总体设计双端注册 启动期类型转换RFC 的 Detailed design 一节给出三条主线Server 端自定义字段在服务端的 register 生命周期插件 API 参考中的 register 阶段被加载应用启动时内容类型与组件 schema 中保存的type: customField会被convertCustomFieldType函数转换为底层 Strapi 数据类型。Admin 端自定义字段注册到管理面板后内容类型或组件保存前其属性会从底层数据类型被反向写回为type: customFieldCTB 的cleanData工具中的formatAttributes函数完成。打包自定义字段可以注册在 Strapi 应用或 Strapi 插件中但只有通过发布到 npm 的插件才能共享。这条“保存为 customField → 启动时展开为底层类型”的双向转换设计使得开发者在 schema.json 中只维护一份抽象类型而运行时数据库映射、校验、序列化始终面对的是真实的 Strapi 数据类型。三、服务端注册customFields 服务与注册表3.1 注册表registry的初始化自定义字段依赖一个名为custom-fields的注册表。在 packages/core/core/src/providers/registries.ts 中可以看到核心容器初始化时会添加该注册表.add(custom-fields, () registries.customFields(strapi))对应的服务工厂位于 packages/core/core/src/services/custom-fields.ts全文只有 9 行职责极其单一——把插件传入的自定义字段对象追加进注册表const createCustomFields (strapi: Core.Strapi): Modules.CustomFields.CustomFields { return { register(customField) { strapi.get(custom-fields).add(customField); }, }; };该服务在核心容器中被注册为customFields见 packages/core/core/src/Strapi.ts.add(customFields, createCustomFields(this))并在 Strapi 实例上暴露为 getterpackages/core/core/src/Strapi.tsget customFields(): Modules.CustomFields.CustomFields { return this.get(customFields); }因此插件在服务端注册自定义字段的标准写法是strapi.customFields.register({...})这正是 packages/core/core/src/registries/tests/custom-fields.test.ts 所验证的路径。3.2 注册时机register 生命周期之后立即转换类型RFC 文档明确指出schema.json中保存的type: customField会在应用启动、register生命周期中所有自定义字段加载完成之后由convertCustomFieldType函数转换为底层 Strapi 数据类型。当前仓库中的实际调用链在 packages/core/core/src/Strapi.ts 的register()方法末尾await this.runPluginsLifecycles(utils.LIFECYCLES.REGISTER); await this.runUserLifecycles(utils.LIFECYCLES.REGISTER); // NOTE: Swap type customField for underlying data type utils.convertCustomFieldType(this); return this;时序上可以确认为先执行所有插件与用户层的 register 生命周期此时各插件通过strapi.customFields.register把自定义字段塞进注册表最后才统一执行一次convertCustomFieldType。这个顺序保证了转换时注册表中一定已经存在全部自定义字段定义若顺序颠倒strapi.get(custom-fields).get(...)就会取不到目标字段。四、核心源码解析convertCustomFieldType 的类型替换转换函数本体在 packages/core/core/src/utils/convert-custom-field-type.ts完整实现如下export const convertCustomFieldType (strapi: Core.Strapi) { const allContentTypeSchemaAttributes Object.values(strapi.contentTypes).map( (schema) schema.attributes ); const allComponentSchemaAttributes Object.values(strapi.components).map( (schema) schema.attributes ); const allSchemasAttributes: InputAttributes[] [ ...allContentTypeSchemaAttributes, ...allComponentSchemaAttributes, ]; for (const schemaAttrbutes of allSchemasAttributes) { for (const attribute of Object.values(schemaAttrbutes)) { if (attribute.type customField) { const customField strapi.get(custom-fields).get(attribute.customField); attribute.type customField.type; } } } };逐行拆解其工作原理遍历范围收集strapi.contentTypes所有内容类型与strapi.components所有组件的全部属性合并为统一的属性集——即内容类型和组件一视同仁地接受自定义字段。匹配条件只处理attribute.type customField的属性。注意判断的是attribute.customField注册时的name如color去注册表中取值而不是字段在 schema 里的键名。替换动作attribute.type customField.type直接把逻辑类型原地改写为该自定义字段声明的底层类型例如string。这是一个就地in-place修改改写完成后后续所有依赖attribute.type的子系统数据库映射、实体校验、序列化看到的都是原生类型完全无感知。该函数带有专门的单元测试packages/core/core/src/utils/tests/convert-custom-field-type.vitest.test.ts用于验证上述替换行为。五、管理端Admin注册与保存前的反向转换5.1 反向转换保存前还原为 customFieldRFC 的 Admin 一节说明当内容类型或组件被保存到磁盘时属性的底层数据类型会在保存前被转换为type: customField由 CTB 的cleanData工具中的formatAttributes函数完成RFC 中引用的cleanData.js#L97-L100是旧版路径当前仓库已经历重构该formatAttributes逻辑现在见于 packages/core/content-type-builder/server/src/utils/attributes.ts 以及 packages/core/content-manager/server/src/services/data-mapper.tsCTB 服务端在处理 schema 时负责属性的双向格式化。与服务端的启动期转换方向相反这条链路保证 schema.json 文件中落盘的一直是type: customFieldcustomField: color这样的抽象表示从而与具体自定义字段实现解耦——换掉插件后只需在 CTB 中重新选择字段类型即可。5.2 管理端注册需要提供的字段从仓库内置的 color-picker 插件管理端注册代码packages/plugins/color-picker/admin/src/index.ts可以看到Admin 端注册的完整形态export default { register(app: any) { app.customFields.register({ name: color, pluginId: color-picker, type: string, icon: ColorPickerIcon, intlLabel: { id: getTrad(color-picker.label), defaultMessage: Color, }, intlDescription: { id: getTrad(color-picker.description), defaultMessage: Select any color, }, components: { Input: async () import(./components/ColorPickerInput).then((module) ({ default: module.ColorPickerInput, })), }, options: { advanced: [ { intlLabel: { id: getTrad(color-picker.options.advanced.regex), defaultMessage: RegExp pattern, }, name: regex, type: text, // ... }, ], }, }); }, };对照服务端的最小注册形态packages/plugins/color-picker/server/src/register.tsexport const register ({ strapi }: any) { strapi.customFields.register({ name: color, plugin: color-picker, type: string, }); };两端的差异一目了然维度Server 端strapi.customFields.registerAdmin 端app.customFields.register必填字段name、plugin、type底层数据类型name、pluginId、type附加字段无icon、intlLabel、intlDescription、components.Input懒加载输入组件、options.advancedCTB 高级表单选项作用供启动期类型转换、数据层使用供管理面板渲染输入控件与 CTB 表单使用其中components.Input采用async () import(...)动态导入方式输入组件会被按需加载不影响管理面板首屏体积。options.advanced则用于在 Content-type Builder 的高级设置区注入额外配置项color-picker 用其提供了一个正则校验 pattern 输入框。六、打包与共享应用可用共享必须走插件RFC 的 Packaging 一节给出的结论是自定义字段既可以注册在Strapi 应用内也可以注册在Strapi 插件中但只有发布到 npm 的插件才能跨项目共享自定义字段。从源码结构看这与 Strapi 的加载机制一致应用内的字段注册只对本项目生效而插件拥有独立的register生命周期入口如 packages/plugins/color-picker/server/src/index.ts 导出的register随插件一起被安装与加载因此天然适合分发。七、TradeoffsRFC 明确列出的局限RFC 的 Tradeoffs 一节如实记录了三项尚未解决的限制这也是使用该特性前必须了解的边界尚不支持创建自定义数据库类型——自定义字段只能包装现有 Strapi 类型不能引入新的持久化类型在 Content-type Builder 中扩展自定义字段的 base / advanced 表单时尚不能导入自定义输入组件从仓库内置示例看CTB 高级表单当前支持的是text等内置控件形态见上文 color-picker 的options.advanced自定义字段不允许使用 relation、component、dynamic zone 和 media 类型作为底层类型——这四类字段强依赖 Strapi 实体关系体系与自定义字段的“输入包装”定位不兼容。八、Alternatives为什么选择插件体系而非独立包类型RFC 的 Alternatives 一节解释了一个关键设计决策为什么自定义字段不单独成为一种新的包类型而是直接寄生在插件 API 上能力上限独立包将导致自定义字段无法访问插件 API 的其他能力。虽然并非总是必需但插件 API 让需要更复杂行为的自定义字段例如使用 injection zones 注入区成为可能加载与生态成本新包类型意味着 Strapi 需要新增一个 loadermarketplace 需要新增一个分区与审核流程整体特性复杂度显著上升“插件 API 对简单自定义字段过重”的问题有折中方案可以新增一个只生成自定义字段所需文件的插件生成器plugin generator从脚手架层面降低开发成本而不必改变包模型。这一权衡的结果就是你在仓库中看到的形式自定义字段 一个极简插件server admin 各一段注册代码例如仓库内置的 packages/plugins/color-picker/另有一个社区侧的完整案例是 Shopify 字段插件strapi-plugin-shopify-fields可作为生产级参考。九、要点回顾与验证路径注册入口服务端strapi.customFields.register服务工厂 packages/core/core/src/services/custom-fields.ts管理端app.customFields.register参考 packages/plugins/color-picker/admin/src/index.ts启动期转换packages/core/core/src/Strapi.ts 在register生命周期全部完成后调用 convertCustomFieldType把type: customField原地替换为底层类型保存期反向转换CTB 数据格式化层formatAttributes见 packages/core/content-type-builder/server/src/utils/attributes.ts在落盘前把属性还原为type: customField边界不能新建数据库类型、CTB 表单不能自定义输入组件、relation/component/dynamic zone/media 四类类型被禁用共享仅通过 npm 插件分发。掌握以上链路后你可以完整解释一个自定义字段从 CTB 保存、schema 落盘、应用启动、类型展开到管理面板渲染输入组件的全生命周期也能评估自定义字段是否满足你的场景需求。【免费下载链接】strapi Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考