
Strapi OpenAPI 文档组装器开发指南从叶子 Assembler 到复合 Assembler【免费下载链接】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/strapiStrapi 的 OpenAPI 文档strapi/openapi包通过一套分层的 Assembler 流水线逐级构建从顶层 Document 到 Path、Path Item再到每个具体的 Operation。本文基于 Strapi 仓库中的贡献指南与 openapi 包源码完整讲解 Assembler 的四个层级、新增叶子 Assembler 的三步流程、新增复合 Assembler 的参考实现以及上下文Context在层级之间如何传递与合并帮助你在为 Strapi 添加新的 OpenAPI 字段或嵌套文档结构时能准确地定位修改点并保证与现有流水线兼容。Assembler 的四个层级Assembler 按 OpenAPI 文档的结构分层构建文档每一层都对应一个 TypeScript 接口和一个上下文context类型由对应的 Factory 负责实例化。这一层级关系定义在 Assembler 类型文件 中层级接口典型实现示例工厂DocumentAssembler.DocumentDocumentInfoAssemblerDocumentAssemblerFactoryPathAssembler.PathPathItemAssemblerPathAssemblerFactoryPath itemAssembler.PathItemOperationAssemblerPathItemAssemblerFactoryOperationAssembler.OperationOperationParametersAssemblerOperationAssemblerFactory各层接口的assemble方法签名体现了层级的差异可以直接从 types.ts 确认export interface Document extends Assembler { assemble(context: DocumentContext): void; } export interface Path extends Assembler { assemble(context: PathContext): void; } export interface PathItem extends Assembler { assemble(context: PathItemContext, path: string, routes: Core.Route[]): void; } export interface Operation extends Assembler { assemble(context: OperationContext, route: Core.Route): void; }可以看到叶子层Operation除了拿到本层的 context还会额外接收一条Core.Route而PathItem层则接收path和一组routes。日常开发中绝大多数改动都是Operation 层的叶子 Assemblerparameters、body、responses 等。只有当需要引入新的嵌套文档结构、需要编排多个子 Assembler 时才需要添加复合 Assemblercomposite assembler。整条流水线由 OpenAPIGenerator 驱动generate()依次执行_initContext → _bootstrap → _preProcess → _assemble → _postProcess → _finalize其中_assemble按注册顺序依次调用 Document 层的各个 AssemblerDocument 层再递归触发 Path、PathItem、Operation 层的组装。你新增的 Assembler 最终都会汇入这条调用链。新增一个叶子 Assembler以贡献指南中的示例为例假设要添加一个OperationSummaryAssembler从路由元数据中设置 Operation 的summary字段。第一步创建 Assembler 类在src/assemblers/document/path/path-item/operation目录下创建summary.tsimport type { Core } from strapi/types; import type { OperationContext } from ../../../../../types; import { createDebugger } from ../../../../../utils; import type { Assembler } from ../../../..; const debug createDebugger(assembler:summary); export class OperationSummaryAssembler implements Assembler.Operation { assemble(context: OperationContext, route: Core.Route): void { const summary route.info.apiName ?? route.handler; debug(assembling summary for %o %o: %o, route.method, route.path, summary); context.output.data.summary summary; } }这个例子包含叶子 Assembler 的两个关键约定签名约定assemble(context, route)接收本层 context 加上接口声明的额外参数Operation 层额外接收route。输出约定把结果写入context.output.data。这个对象会被调用你的那个复合 Assembler 合并进父级输出——叶子 Assembler 不需要关心自己处于文档的哪个位置只需负责填充自己那一段。现有的叶子实现都可以作为参照例如 OperationIDAssembler 通过origin/method/path三段式拼装生成唯一的operationId并同样以context.output.data.operationId operationId收尾OperationParametersAssembler 则展示了一个稍复杂的叶子实现——它从context.strapi.contentAPISchemaRegistry与context.registries.extractedComponentSchemas读取共享状态把 Zod 模式转换为 OpenAPI Schema 后写入context.output.data.parameters。另外注意createDebugger的使用每个 Assembler 都会创建一个带命名空间的调试器如assembler:summary、assembler:operation-id在开启 debug 环境变量时可以追踪每个 Assembler 的执行过程。第二步从 index.ts 导出在src/assemblers/document/path/path-item/operation/index.ts中追加导出export { OperationSummaryAssembler } from ./summary;第三步注册到 OperationAssemblerFactory在 OperationAssemblerFactory 的createAll()中注册新 Assembler。当前工厂注册的五个 Operation 层 Assembler 是export class OperationAssemblerFactory { createAll(): Assembler.Operation[] { return [ this._createOperationIDAssembler(), // OperationIDAssembler this._createParametersAssembler(), // OperationParametersAssembler this._createResponsesAssembler(), // OperationResponsesAssembler this._createTagsAssembler(), // OperationTagsAssembler this._createBodyAssembler(), // BodyAssembler new OperationSummaryAssembler(), // 新增 ]; } }工厂的实际写法是一个私有_createXxxAssembler()方法对应一个 Assembler见 factory.ts新增时保持同样的私有方法风格即可。由于createAll()返回的数组按顺序执行新增 Assembler 的插入位置决定了它对output.data的写入时机——若多个 Assembler 写同一个字段靠后者会覆盖前者。对于Document 层的叶子 Assembler例如新增一个 OpenAPI 顶层字段遵循完全相同的模式在src/assemblers/document/下实现并导出然后注册到 DocumentAssemblerFactory。该工厂当前注册了DocumentMetadataAssembler、DocumentInfoAssembler、DocumentServerAssembler、DocumentSecurityAssembler和DocumentPathsAssembler后者是复合 Assembler。新增一个复合 Assembler复合 Assembler 的职责是创建子 context → 运行子 Assemblers → 把结果合并回父级输出。仓库中的参考实现是 OperationAssembler其核心逻辑如下export class OperationAssembler implements Assembler.PathItem { constructor( private readonly _assemblers: Assembler.Operation[], private readonly _contextFactory: OperationContextFactory new OperationContextFactory() ) {} assemble(context: PathItemContext, path: string, routes: Core.Route[]): void { const { output, ...sharedProps } context; for (const route of routes) { const operationContext this._contextFactory.create(sharedProps); for (const assembler of this._assemblers) { assembler.assemble(operationContext, route); } Object.assign(output.data, { [route.method.toLowerCase()]: operationContext.output.data }); } } }这段实现展示了复合 Assembler 的四个要点且当前仓库中的真实版本比指南示例更完整解构出共享属性const { output, ...sharedProps } context把父 context 中的strapi、routes、timer、registries摘出来用于创建子 contextoutput被排除避免子 Assembler 误写父级输出。为每条路由创建独立的子 contextthis._contextFactory.create(sharedProps)为每个route生成一个OperationContext多个子 Assembler 在同一子 context 的output.data上协作。合并回父级输出Object.assign(output.data, { [method]: operationContext.output.data })把组装好的 Operation Object 按 HTTP 方法小写写入父 PathItem 的data。内置校验真实实现operation.ts还包含两个校验方法——_validateHTTPIndex确保方法名属于 OpenAPI 允许的 HTTP 方法集合_validateOperationObject确保最终 Operation 对象包含responses属性OpenAPI 规范要求否则抛出明确错误。新增复合 Assembler 时建议保留这种组装后校验的习惯让结构性缺陷尽早暴露。通过工厂接线创建或扩展对应层级的工厂例如PathItemAssemblerFactory让它用子 Assembler 列表和 context factory 实例化你的复合 Assembler再将该工厂注册到父层级。这条工厂链在源码中层层嵌套DocumentAssemblerFactory 创建DocumentPathsAssembler注入 PathAssemblerFactoryPathAssemblerFactory 创建 PathItemAssembler注入 PathItemAssemblerFactoryPathItemAssemblerFactory 创建 OperationAssembler注入 OperationAssemblerFactory。每个_createXxxAssembler都接受工厂 context factory两个可注入参数方便在测试中替换依赖——这也是你在__tests__下编写单元测试时应遵循的注入方式。上下文工厂与共享状态的正确传递每个 Assembler 层级操作的都是由对应工厂创建的强类型 contextDocumentContext、PathContext、PathItemContext、OperationContext。只有当你引入一个全新的组装层级拥有自己的输出形状时才需要新增一个 context factory仅在现有层级添加叶子 Assembler 时直接复用现有工厂如OperationContextFactory即可。上下文工厂的基类 AbstractContextFactory 决定了 context 的构成public create(context: PartialContextT, defaultValue: T): ContextT { const { strapi, routes } context; // 允许覆盖以在子 Assembler 中共享 registries 和 timer const timer context.timer ?? this._timerFactory.create(); const registries context.registries ?? this._registriesFactory.createAll(); // 默认输出由 defaultValue 初始化 const output this.createDefaultOutput(defaultValue); return { strapi, routes, timer, registries, output }; }这里有两个设计细节值得注意timer与registries优先复用父 context 传入的实例context.timer ?? ...。这正是文档中提示创建子 context 时复用父级的timer和registries的原因——复合 Assembler 通过PartialContext传递这些共享属性后整棵组装树共用同一个计时器和注册表保证耗时统计output.stats.time与跨 Assembler 的共享状态如去重后的 Schema 缓存extractedComponentSchemas保持一致。若不复用子层级会各自新建 timer/registries共享缓存就会失效。output.data用defaultValue初始化各叶子 Assembler 在此基础上渐进填充最终由复合 Assembler 合并上抛。RegistriesFactory.createAll()目前返回空对象、ContextRegistries是空接口属于为未来共享组装状态去重 Schema、跨 Assembler 缓存等预留的扩展点当前无需单独配置。如需了解完整的 context factory 编写步骤在src/types.ts定义 context 数据类型、继承AbstractContextFactory等可继续阅读 Context factory 贡献指南。验证你的改动新增或修改 Assembler 后仓库中有两类既有用例可作为验证模板operation-assemblers.test.ts覆盖 Operation 层各个叶子 AssembleroperationId、parameters、responses、tags、body的输出断言document-assemblers.test.ts覆盖 Document 层组装结果测试通过 fixtures 与 mocks 提供模拟的 routes 与 Strapi 实例新 Assembler 的测试可以沿用同一套夹具与工厂注入模式。运行方式与包内其他测试一致在 packages/core/openapi 目录下执行该包的测试脚本即可。小结为 Strapi 的 OpenAPI 文档扩展组装能力时可以按以下决策路径操作判断层级只改现有层级的某个字段输出 → 写叶子 Assembler引入新的嵌套文档结构 → 写复合 Assembler必要时配套新的 context factory。三步落地叶子 Assembler实现Assembler.Xxx接口并写入context.output.data→ 从该层index.ts导出 → 注册到对应层级的AssemblerFactory.createAll()注意数组顺序即执行顺序。复合 Assembler 记住三件事子 context 必须复用父级timer/registries子 Assembler 的结果通过Object.assign合并回父级output.data组装后做结构校验。以测试收尾参照__tests__/operation-assemblers.test.ts等既有用例用 fixtures 与工厂注入验证输出。遵循这套模式你的改动就能与 Strapi OpenAPI 生成器现有的分层流水线无缝衔接并通过调试器与单元测试获得可验证的行为保障。【免费下载链接】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),仅供参考