ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Webiny 内容条目数据工厂可注入化重构:将 Entry Data Factory 重构为一等公民 Feature

Webiny 内容条目数据工厂可注入化重构:将 Entry Data Factory 重构为一等公民 Feature CMS后端前端【免费下载链接】webiny-jsOpen-source, self-hosted CMS platform on AWS serverless (Lambda, DynamoDB, S3). TypeScript framework with multi-tenancy, lifecycle hooks, GraphQL API, and AI-assisted development via MCP server. Built for developers at large organizations.项目地址https://gitcode.com/gh_mirrors/we/webiny-js点击查看免费下载本文以webiny/api-headless-cms包中 Entry Data Factory 的可注入 Feature重构设计文档为核心结合当前仓库源码讲解 Webiny 如何把六个原本被 use case 直接导入的纯async函数重构为可 DI 注入、可替换、可测试的一等公民 Feature。读完你将掌握createAbstraction/createImplementation/createFeature三件套的落地写法、每个工厂的依赖矩阵与单例注册方式以及 use case 如何通过构造器注入调用工厂。一、问题背景六个隐形工厂函数在重构之前packages/api-headless-cms的 CRUD 层中内容条目的数据装配逻辑由六个纯async函数承担原路径为crud/contentEntry/entryDataFactories/对应创建、更新、创建修订、发布、取消发布、重新发布六个场景CreateEntryDataFactory创建条目UpdateEntryDataFactory更新条目CreateEntryRevisionFromDataFactory从现有条目创建修订CreatePublishEntryDataFactory发布条目CreateUnpublishEntryDataFactory取消发布条目CreateRepublishEntryDataFactory重新发布条目这六个函数的共同问题在设计文档中写得很直白它们只是被 use case 直接导入的普通函数因此——测试无法替换单测中无法用 stub/mock 替换工厂实现插件作者无法覆盖第三方无法通过插件机制替换或增强默认行为无法注入DI没有注册进 IoC 容器use case 只能靠 import 硬编码引用。从当前源码看这个问题已经被修复如今的 use case 全部通过构造器注入工厂详见下文第五节说明设计文档描述的是重构动机重构已经落地。二、重构决策每个工厂成为一个可注入 Feature设计文档给出的核心决策是把每个工厂迁移到features/contentEntry/entryDataFactories/FactoryName/目录升级为一等公民的可注入 Feature。具体做法有三条类封装函数每个工厂用一个类包装原有函数逻辑类通过构造器声明自己的 DI 依赖以单例注册工厂本身是无状态的stateless因此全部按inSingletonScope()注册旧函数保留为内部实现原crud/contentEntry/entryDataFactories/下的函数仍然存在新实现委托delegate给它们但它们从此成为内部细节不再被 features 树之外的地方导入未来可被内联inline或删除。这正是 Webiny reach abstractions through the namespace 与prefer provider over resolved value等代码风格规范见 ai-context/code-style在 Headless CMS 中的具体体现。三、目录结构与四文件约定设计文档定义的目标目录结构如下当前仓库已经完全落地packages/api-headless-cms/src/features/contentEntry/entryDataFactories/ ├── EntryDataFactoriesFeature.ts ← 聚合注册全部 6 个被 ContentEntriesFeature 引入 ├── CreateEntryDataFactory/ ├── UpdateEntryDataFactory/ ├── CreateEntryRevisionFromDataFactory/ ├── CreatePublishEntryDataFactory/ ├── CreateUnpublishEntryDataFactory/ └── CreateRepublishEntryDataFactory/实际仓库中该目录还额外包含expiresAt.ts、mapAndCleanUpdatedInputData.ts、statuses.ts、system.ts等被工厂共享的小工具模块如状态常量STATUS_DRAFT/STATUS_PUBLISHED/STATUS_UNPUBLISHED与过期时间计算getExpiresAt。设计文档明确规定每个工厂文件夹固定包含四个文件命名与职责如下文件职责abstractions.ts定义 scoped token、接口与 namespaceFactoryName.ts用createImplementation把实现类绑定到抽象feature.ts用createFeature声明注册与生命周期作用域index.ts对外 re-export抽象与 feature下面逐一以CreateEntryDataFactory为例展开文件路径见 entryDataFactories 目录。3.1 abstractions.tsscoped token 接口 namespaceCreateEntryDataFactory/abstractions.ts 定义了三样东西import { createAbstraction } from webiny/feature/api; // ... 类型导入省略 export interface ICreateEntryDataResponseTValues extends CmsEntryValues CmsEntryValues { entry: CmsEntryTValues; input: CreateCmsEntryInputTValues; } export interface ICreateEntryDataFactory { createTValues extends CmsEntryValues CmsEntryValues( model: CmsModel, rawInput: CreateCmsEntryInputTValues, options?: CreateCmsEntryOptionsInput ): PromiseICreateEntryDataResponseTValues; } export const CreateEntryDataFactory createAbstractionICreateEntryDataFactory( Cms/Entry/CreateEntryDataFactory ); export namespace CreateEntryDataFactory { export type Interface ICreateEntryDataFactory; export type ResponseTValues extends CmsEntryValues CmsEntryValues ICreateEntryDataResponseTValues; }要点解读Token 即抽象createAbstractionICreateEntryDataFactory(Cms/Entry/CreateEntryDataFactory)生成一个带唯一字符串 token 的抽象该 token 是容器中查找到该依赖的键同名 namespace 聚合类型通过namespace与const同名合并调用方可以写CreateEntryDataFactory.Interface、CreateEntryDataFactory.ResponseTValues实现抽象 类型双引用接口约定create(model, rawInput, options)返回{ entry, input }其中input是清洗/映射后的输入values 已被structuredClone复制。3.2 FactoryName.ts类实现 createImplementationCreateEntryDataFactory/CreateEntryDataFactory.ts 中实现是一个类构造函数直接声明 DI 依赖class CreateEntryDataFactoryImpl implements ICreateEntryDataFactory { public constructor( private readonly cmsContext: CmsContext.Interface, private readonly identityContext: IdentityContext.Interface, private readonly tenantContext: TenantContext.Interface, private readonly accessControl: AccessControl.Interface, private readonly modelToAstConverter: ModelToAstConverter.Interface ) {} public async createTValues extends CmsEntryValues CmsEntryValues( model: CmsModel, rawInput: CreateCmsEntryInputTValues, options?: CreateCmsEntryOptionsInput ): PromiseICreateEntryDataResponseTValues { // ... 完整装配逻辑见第六节 } } export const CreateEntryDataFactory FactoryAbstraction.createImplementation({ implementation: CreateEntryDataFactoryImpl, dependencies: [CmsContext, IdentityContext, TenantContext, AccessControl, ModelToAstConverter] });模式总结与设计文档示例完全一致class XxxImpl implements Xxx.Interface构造函数参数即依赖清单全部private readonlycreateImplementation({ abstraction, implementation, dependencies })完成绑定dependencies数组列出注入的抽象 token。3.3 feature.ts单例作用域注册CreateEntryDataFactory/feature.ts 是标准的 feature 声明import { createFeature } from webiny/feature/api; import { CreateEntryDataFactory } from ./CreateEntryDataFactory.js; export const CreateEntryDataFactoryFeature createFeature({ name: CreateEntryDataFactory, register(container) { container.register(CreateEntryDataFactory).inSingletonScope(); } });createFeature返回一个可独立注册的 feature 对象container.register(...).inSingletonScope()显式声明单例作用域——因为工厂是无状态的每次请求复用同一个实例即可避免重复构建开销name用于特征聚合、日志与调试定位。3.4 index.ts统一出口index.ts 只做两件事export { CreateEntryDataFactory } from ./abstractions.js; export { CreateEntryDataFactoryFeature } from ./feature.js;即对外暴露抽象供注入方引用类型与 token和feature供注册方装配实现层CreateEntryDataFactoryImpl被完全封装在内部不泄漏。四、Token 作用域统一Cms/Entry/前缀设计文档要求所有工厂 token 使用Cms/Entry/前缀从源码确认全部遵守工厂TokenCreateEntryDataFactoryCms/Entry/CreateEntryDataFactoryUpdateEntryDataFactoryCms/Entry/UpdateEntryDataFactoryCreateEntryRevisionFromDataFactoryCms/Entry/CreateEntryRevisionFromDataFactoryCreatePublishEntryDataFactoryCms/Entry/CreatePublishEntryDataFactoryCreateUnpublishEntryDataFactoryCms/Entry/CreateUnpublishEntryDataFactoryCreateRepublishEntryDataFactoryCms/Entry/CreateRepublishEntryDataFactory设计文档特别注明同一包内已有的 use case token 目前尚未按此规范加前缀属于独立的清理任务separate cleanup task不属于本次重构范围。这意味着 reader 在查阅旧 use case 时会看到两类 token 命名风格并存这是有意为之的渐进式迁移。五、依赖矩阵设计表 vs 实际实现设计文档给出了一张每个工厂的依赖表格原文如下FactoryDependenciesCreateEntryDataFactoryCmsContext, IdentityContext, TenantContext, AccessControlUpdateEntryDataFactoryCmsContext, IdentityContext, TenantContextCreateEntryRevisionFromDataFactoryCmsContext, IdentityContext, TenantContext, AccessControlCreatePublishEntryDataFactoryCmsContext, IdentityContextCreateUnpublishEntryDataFactoryIdentityContextCreateRepublishEntryDataFactoryCmsContext, IdentityContext对照当前仓库源码中六个createImplementation的dependencies数组可通过在 entryDataFactories 目录下搜索dependencies: [逐一核对实际落地情况如下工厂实际注入依赖源码确认CreateEntryDataFactoryCmsContext, IdentityContext, TenantContext, AccessControl,ModelToAstConverterUpdateEntryDataFactoryCmsContext, IdentityContext,ModelToAstConverterCreateEntryRevisionFromDataFactoryCmsContext, IdentityContext, AccessControl,ModelToAstConverterCreatePublishEntryDataFactoryCmsContext, IdentityContextCreateUnpublishEntryDataFactoryIdentityContextCreateRepublishEntryDataFactoryCmsContext, IdentityContext两处差异值得注意均为实现阶段演进属正常设计演化ModelToAstConverter成为实际依赖Create/Update/CreateEntryRevisionFrom 三个工厂在实现时额外注入了ModelToAstConverter.Interface模型转 AST 转换器用于在装配数据前对模型执行toAst并补全条目字段 IDensureItemIds。这印证了设计文档中每个工厂 owns 自己的 DI 依赖的原则——实现细节被封装在类构造器中调用方无感知。部分工厂实际未使用 TenantContext例如UpdateEntryDataFactory、CreateEntryRevisionFromDataFactory在最终实现中移除了 TenantContext 依赖体现按需声明、最小依赖的收敛。六、源码级纵深CreateEntryDataFactory 的装配流水线为了理解这些工厂到底在做什么以 CreateEntryDataFactory.ts 的create()实现为例梳理完整的数据装配流水线这是设计文档未展开、由源码补充的核心内容清洗输入并填充默认值cleanInputValues遍历model.fields为每个字段补上默认值——优先取field.settings.defaultValue按字段类型做Boolean/Number转换否则取预定义值predefinedValues中selected的项list字段会收集所有选中的预定义值。模型转 AST 并补全 item IDsmodelToAstConverter.toAstensureItemIds确保嵌套结构如表单/富文本内部对象都有稳定 ID。数据校验validateModelEntryDataOrThrow位于 entryDataValidation.ts支持options.skipValidation跳过。引用字段映射referenceFieldsMapping把引用类型的值解析为实际关联内容validateEntries: true强制校验被引用条目存在。生成条目 IDcreateEntryId默认mdbid()生成若rawInput.id存在则校验格式必须匹配A-Za-z0-9与-且不能以-开头/结尾非法则抛INVALID_ID首个修订version 1并通过createIdentifier合成id。状态与权限默认状态STATUS_DRAFT若传入STATUS_PUBLISHED/STATUS_UNPUBLISHED分别调用accessControl.canAccessEntry({ model, pw: p/u })做条目前与条目后两次鉴权失败抛NotAuthorizedError非草稿状态条目locked true。填充发布元数据revisionFirstPublishedOn/revisionLastPublishedOn/firstPublishedOn/lastPublishedOn及对应的By字段在发布状态下用getDate/getIdentity回退到当前时间与当前身份。组装CmsEntrytenant来自tenantContext.getTenant().id、修订级与条目级时间/身份元数据、location.folderId回退wbyAco_location.folderId与ROOT_FOLDER、systemgetSystem、live发布时指向当前 version、expiresAtgetExpiresAt等。返回{ entry, input }input保留原始输入但把values替换为structuredClone(values)映射后的干净值供后续事件如EntryBeforeCreateEvent/EntryAfterCreateEvent携带。这条流水线同时解释了为什么 Create 工厂需要 CmsContext校验、引用映射都依赖上下文、AccessControl发布/取消发布鉴权、ModelToAstConverterAST 转换——每个依赖都有明确的职责来源。七、聚合注册EntryDataFactoriesFeature → ContentEntriesFeature六个工厂 feature 由聚合 feature 统一装配。EntryDataFactoriesFeature.ts 的register(container)依次调用六个子 feature 的registerexport const EntryDataFactoriesFeature createFeature({ name: EntryDataFactories, register(container) { CreateEntryDataFactoryFeature.register(container); UpdateEntryDataFactoryFeature.register(container); CreateEntryRevisionFromDataFactoryFeature.register(container); CreatePublishEntryDataFactoryFeature.register(container); CreateUnpublishEntryDataFactoryFeature.register(container); CreateRepublishEntryDataFactoryFeature.register(container); } });而 ContentEntriesFeature.ts 在注册各类 Query/Command feature 的同时明确引入了EntryDataFactoriesFeature.register(container)并在注释中按Query features / Provider features / Command features对子 feature 分组。也就是说只要装配了ContentEntriesFeature六个工厂就自动全部就位无需调用方逐个注册。八、调用点落地use case 通过构造器注入设计文档的Use Case Call Site (next step)章节描述了目标形态——use case 不再 import 函数而是注入工厂后只传业务参数。这一点在当前源码中已经完成以 CreateEntryUseCase.ts 为例import { CreateEntryDataFactory } from ~/features/contentEntry/entryDataFactories/CreateEntryDataFactory/index.js; class CreateEntryUseCaseImpl implements UseCaseAbstraction.Interface { public constructor( private eventPublisher: EventPublisher.Interface, private repository: CreateEntryRepository.Interface, private accessControl: AccessControl.Interface, private createEntryDataFactory: CreateEntryDataFactory.Interface // ← 注入工厂 ) {} public async executeT extends CmsEntryValues CmsEntryValues( model: CmsModel, rawInput: CreateCmsEntryInputT, options?: CreateCmsEntryOptionsInput ): PromiseResultCmsEntryT, UseCaseAbstraction.Error { // 写权限校验rwd: w... const { entry, input } await this.createEntryDataFactory.createT( model, rawInput, options ); // 条目级权限、EntryBeforeCreateEvent → repository.execute → EntryAfterCreateEvent } } export const CreateEntryUseCase UseCaseAbstraction.createImplementation({ implementation: CreateEntryUseCaseImpl, dependencies: [EventPublisher, CreateEntryRepository, AccessControl, CreateEntryDataFactory] });设计文档预言的简化调用从约 6 个参数收敛为create(model, rawInput, options)在此处精确兑现entry/input之外的所有上下文cmsContext、identity、tenant、accessControl都从构造函数依赖中解析execute只关心模型与输入。类似的注入模式同样出现在其余 use case 中PublishEntryUseCase注入CreatePublishEntryDataFactory见 PublishEntryUseCase.ts发布时先getRevisionById取原条目、getLatestRevision取最新修订再交给工厂生成发布数据UpdateEntryUseCase、UnpublishEntryUseCase、RepublishEntryUseCase、CreateEntryRevisionFromUseCase以及ValidateEntryUseCase也都通过~/features/contentEntry/entryDataFactories/FactoryName/index.js引用对应工厂抽象。这意味着测试现在可以在构造 use case 时注入 mock 工厂插件作者也可以通过覆盖dependencies中的 token 替换默认实现——设计文档提出的三大痛点测试替换、插件覆盖、DI 注入均已解决。九、迁移路径与后续任务设计文档的Whats Next部分规划了两步结合源码可确认进度接线 use case已完成把六个 use caseCreateEntry、UpdateEntry、CreateEntryRevisionFrom、PublishEntry、UnpublishEntry、RepublishEntry改为注入各自工厂替代直接 import 函数——当前源码中的六个 use case 文件均已按此模式改造。清理旧函数后续收尾crud/contentEntry/entryDataFactories/下的旧工厂函数保留作为实现细节新实现委托给它们不再被 features 树外部导入此后可按需内联或删除。需要留意的是当前仓库中旧crud/contentEntry/entryDataFactories/目录本身已不单独存在相关工具函数已并入 features 树或迁移至crud/contentEntry/下其他模块如entryDataValidation.ts、referenceFieldsMapping.ts这正说明重构最终走向了删除旧目录、实现就地化的收尾形态——设计文档描述的迁移终点已经实现。十、总结Entry Data Factory 的可注入化重构是webiny/api-headless-cms从函数式 CRUD 内部实现走向DI 化 Feature 架构的一个典型切片。它带来的收益可归纳为可测试use case 单元测试可以注入 stub 工厂隔离数据装配与存储/事件逻辑可扩展插件作者可注册同名 token 覆盖默认工厂实现自定义装配逻辑依赖显式化每个工厂通过构造器声明依赖配合createAbstraction/createImplementation/createFeature三件套与inSingletonScope()单例注册构成清晰、可追踪的装配图渐进迁移旧函数保留为委托目标token 命名Cms/Entry/前缀统一新代码未完成部分use case token 加前缀留作独立任务不阻塞主线。如需深入阅读实现细节可依次查看 设计文档、工厂目录实现、聚合注册 与 CreateEntryUseCase对照本文即可获得从设计到源码的完整链路。赞分享CMS后端前端【免费下载链接】webiny-jsOpen-source, self-hosted CMS platform on AWS serverless (Lambda, DynamoDB, S3). TypeScript framework with multi-tenancy, lifecycle hooks, GraphQL API, and AI-assisted development via MCP server. Built for developers at large organizations.项目地址https://gitcode.com/gh_mirrors/we/webiny-js点击查看免费下载相关推荐Webiny api-headless-cms 内容条目数据工厂Entry Data FactoriesDI 重构内联逻辑与工厂注入实战指南Webiny api headless cms 内容条目数据工厂Entry Data FactoriesDI 重构内联逻辑与工厂注入实战指南 本文基于仓库CMS后端前端Webiny Website Builder 页面特性 DI 容器化重构指南从 new 工厂到 webiny/feature/admin 依赖注入架构Webiny Website Builder 页面特性 DI 容器化重构指南从 new 工厂到 webiny/feature/admin 依赖注入架构 导读CMS后端前端webiny-js Headless CMS 条目数据工厂重构设计解析逻辑内联与 DI 工厂收编webiny js Headless CMS 条目数据工厂重构设计解析逻辑内联与 DI 工厂收编 导读 本文以 webiny js 仓库中一份已批准的架构设计CMS后端前端上一篇Bthread Tagged Task Groupbrpc 的 bthread 线程池按 tag 分组隔离实战指南下一篇Tooltip.js高级技巧掌握position属性与主题定制的实用指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表