Strapi OpenAPI 文档组装器开发指南:从叶子 Assembler 到复合 Assembler
2026/9/13 15:04:58 网站建设 项目流程

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/strapi

Strapi 的 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.DocumentDocumentInfoAssemblerDocumentAssemblerFactory
PathAssembler.PathPathItemAssemblerPathAssemblerFactory
Path itemAssembler.PathItemOperationAssemblerPathItemAssemblerFactory
OperationAssembler.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 层的叶子 Assembler(parameters、body、responses 等)。只有当需要引入新的嵌套文档结构、需要编排多个子 Assembler 时,才需要添加复合 Assembler(composite assembler)

整条流水线由 OpenAPIGenerator 驱动:generate()依次执行_initContext → _bootstrap → _preProcess → _assemble → _postProcess → _finalize,其中_assemble按注册顺序依次调用 Document 层的各个 Assembler,Document 层再递归触发 Path、PathItem、Operation 层的组装。你新增的 Assembler 最终都会汇入这条调用链。

新增一个叶子 Assembler

以贡献指南中的示例为例:假设要添加一个OperationSummaryAssembler,从路由元数据中设置 Operation 的summary字段。

第一步:创建 Assembler 类

src/assemblers/document/path/path-item/operation目录下创建summary.ts

import 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 的两个关键约定:

  1. 签名约定assemble(context, route)接收本层 context 加上接口声明的额外参数(Operation 层额外接收route)。
  2. 输出约定:把结果写入context.output.data。这个对象会被调用你的那个复合 Assembler 合并进父级输出——叶子 Assembler 不需要关心自己处于文档的哪个位置,只需负责"填充自己那一段"。

现有的叶子实现都可以作为参照,例如 OperationIDAssembler 通过origin/method/path三段式拼装生成唯一的operationId,并同样以context.output.data.operationId = operationId收尾;OperationParametersAssembler 则展示了一个稍复杂的叶子实现——它从context.strapi.contentAPISchemaRegistrycontext.registries.extractedComponentSchemas读取共享状态,把 Zod 模式转换为 OpenAPI Schema 后写入context.output.data.parameters

另外注意createDebugger的使用:每个 Assembler 都会创建一个带命名空间的调试器(如assembler:summaryassembler: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。该工厂当前注册了DocumentMetadataAssemblerDocumentInfoAssemblerDocumentServerAssemblerDocumentSecurityAssemblerDocumentPathsAssembler(后者是复合 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 的四个要点,且当前仓库中的真实版本比指南示例更完整:

  1. 解构出共享属性const { output, ...sharedProps } = context把父 context 中的strapiroutestimerregistries摘出来,用于创建子 context;output被排除,避免子 Assembler 误写父级输出。
  2. 为每条路由创建独立的子 contextthis._contextFactory.create(sharedProps)为每个route生成一个OperationContext,多个子 Assembler 在同一子 context 的output.data上协作。
  3. 合并回父级输出Object.assign(output.data, { [method]: operationContext.output.data })把组装好的 Operation Object 按 HTTP 方法(小写)写入父 PathItem 的data
  4. 内置校验:真实实现(operation.ts)还包含两个校验方法——_validateHTTPIndex确保方法名属于 OpenAPI 允许的 HTTP 方法集合,_validateOperationObject确保最终 Operation 对象包含responses属性(OpenAPI 规范要求),否则抛出明确错误。新增复合 Assembler 时建议保留这种"组装后校验"的习惯,让结构性缺陷尽早暴露。

通过工厂接线

创建或扩展对应层级的工厂(例如PathItemAssemblerFactory),让它用子 Assembler 列表和 context factory 实例化你的复合 Assembler,再将该工厂注册到父层级。这条"工厂链"在源码中层层嵌套:

  • DocumentAssemblerFactory 创建DocumentPathsAssembler,注入 PathAssemblerFactory;
  • PathAssemblerFactory 创建 PathItemAssembler,注入 PathItemAssemblerFactory;
  • PathItemAssemblerFactory 创建 OperationAssembler,注入 OperationAssemblerFactory。

每个_createXxxAssembler都接受"工厂 + context factory"两个可注入参数,方便在测试中替换依赖——这也是你在__tests__下编写单元测试时应遵循的注入方式。

上下文工厂与共享状态的正确传递

每个 Assembler 层级操作的都是由对应工厂创建的强类型 context(DocumentContextPathContextPathItemContextOperationContext)。只有当你引入一个全新的组装层级(拥有自己的输出形状)时,才需要新增一个 context factory;仅在现有层级添加叶子 Assembler 时,直接复用现有工厂(如OperationContextFactory)即可。

上下文工厂的基类 AbstractContextFactory 决定了 context 的构成:

public create(context: PartialContext<T>, defaultValue: T): Context<T> { 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 }; }

这里有两个设计细节值得注意:

  • timerregistries优先复用父 context 传入的实例context.timer ?? ...)。这正是文档中提示"创建子 context 时复用父级的timerregistries"的原因——复合 Assembler 通过PartialContext传递这些共享属性后,整棵组装树共用同一个计时器和注册表,保证耗时统计(output.stats.time)与跨 Assembler 的共享状态(如去重后的 Schema 缓存extractedComponentSchemas)保持一致。若不复用,子层级会各自新建 timer/registries,共享缓存就会失效。
  • output.datadefaultValue初始化,各叶子 Assembler 在此基础上渐进填充,最终由复合 Assembler 合并上抛。

RegistriesFactory.createAll()目前返回空对象、ContextRegistries是空接口,属于为未来共享组装状态(去重 Schema、跨 Assembler 缓存等)预留的扩展点,当前无需单独配置。如需了解完整的 context factory 编写步骤(在src/types.ts定义 context 数据类型、继承AbstractContextFactory等),可继续阅读 Context factory 贡献指南。

验证你的改动

新增或修改 Assembler 后,仓库中有两类既有用例可作为验证模板:

  • operation-assemblers.test.ts:覆盖 Operation 层各个叶子 Assembler(operationId、parameters、responses、tags、body)的输出断言;
  • document-assemblers.test.ts:覆盖 Document 层组装结果;
  • 测试通过 fixtures 与 mocks 提供模拟的 routes 与 Strapi 实例,新 Assembler 的测试可以沿用同一套夹具与工厂注入模式。

运行方式与包内其他测试一致:在 packages/core/openapi 目录下执行该包的测试脚本即可。

小结

为 Strapi 的 OpenAPI 文档扩展组装能力时,可以按以下决策路径操作:

  1. 判断层级:只改现有层级的某个字段输出 → 写叶子 Assembler;引入新的嵌套文档结构 → 写复合 Assembler(必要时配套新的 context factory)。
  2. 三步落地叶子 Assembler:实现Assembler.Xxx接口并写入context.output.data→ 从该层index.ts导出 → 注册到对应层级的AssemblerFactory.createAll(),注意数组顺序即执行顺序。
  3. 复合 Assembler 记住三件事:子 context 必须复用父级timer/registries;子 Assembler 的结果通过Object.assign合并回父级output.data;组装后做结构校验。
  4. 以测试收尾:参照__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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询