Strapi @strapi/openapi 扩展指南:从零创建自定义 Routes Matcher Rule 过滤规则
在 Strapi 的 @strapi/openapi 包中,最终哪些路由会进入生成的 OpenAPI 文档,是由一条"路由收集 + 规则匹配"的管线决定的:RouteCollector 从多个 Provider 汇聚路由,再交由 RouteMatcher 依据一组可组合的 MatcherRule 逐条过滤。本文基于官方贡献指南《Routes Matcher Rule》,完整讲解如何新增一条匹配规则(以"只选择特定 HTTP 方法的路由"为例),并结合仓库源码剖析规则的类型定义、求值语义(every 全量通过 + 短路)及其在 generate 流程中的装配位置,帮助你在扩展 Strapi OpenAPI 生成器时准确控制被文档化的路由范围。
一、扩展定位:Matcher Rule 在生成管线中的角色
@strapi/openapi 的贡献者指南将扩展点按职责划分为六个方向,其中 Matcher Rule 专门用于"过滤哪些路由会被文档化":
| 扩展点 | 适用场景 |
|---|---|
| Routes provider | 从新的 Strapi 来源收集路由 |
| Routes matcher rule(本文主题) | 过滤哪些路由被写入文档 |
| Assemblers | 构建或扩展 OpenAPI 文档各部分 |
| Context factory | 增加新的装配层级及其上下文 |
| Processors | 在装配前后运行逻辑 |
| Testing | 编写或调试单元测试 |
要理解新增规则的落点,先看源码中匹配器的工作方式。RouteMatcher 的实现极其精简——它持有规则数组,匹配时任一路由必须通过全部规则:
// packages/core/openapi/src/routes/matcher.ts
export class RouteMatcher {
private readonly _rules: MatcherRule[];
constructor(rules: MatcherRule[] = []) {
this._rules = rules;
}
match(route: Core.Route): boolean {
return this._rules.every((rule) => rule(route));
}
}
对应文件见 matcher.ts。这里有两个直接影响扩展行为的语义,均可由测试用例印证:
- AND 组合语义:
every意味着多条规则之间是"与"关系,任一规则返回false,该路由即被过滤。route-matcher.test.ts 中should fail to match if any rule fails用例验证了这一点:path.startsWith('/search')通过但method === 'POST'失败时,整体匹配结果为false。 - 短路求值:源码注释明确写着 "Exits early if any rule fails"——前面的规则失败后,后续规则不再执行。因此新增规则时,把廉价判断(如方法比较)放在昂贵的判断之前,是合理的工程习惯。
而每条规则本身只是一个函数。其类型定义只有一行:
// packages/core/openapi/src/routes/types.ts
export type MatcherRule = (route: Core.Route) => boolean;
见 types.ts。这意味着一条 Matcher Rule 本质上是一个柯里化的谓词工厂:外层函数接收规则参数(如方法列表),返回一个内层函数接收 Core.Route 并返回布尔值。Core.Route 的关键字段包括 path、method、info(含 type)与 handler,测试中的构造示例如下:
const route: Core.Route = {
path: '/users/123',
method: 'GET',
info: { type: 'content-api' },
handler: '',
};
二、按指南新增 isMethodIn 规则:三步完整操作
以下三步完整继承自官方指南 02-routes-matcher-rule.md,假设目标是为 @strapi/openapi 增加一条"只选择特定 HTTP 方法路由"的匹配规则。
1. 在 src/routes/rules 下新建规则文件
添加文件 src/routes/rules/is-method-in.ts,粘贴如下片段:
import type { MatcherRule } from '../types';
export const isMethodIn = (methods: string[]): MatcherRule => {
return (route) => methods.includes(route.method);
};
注意两点细节:
- 类型导入路径是
'../types',即规则文件位于src/routes/rules/目录时,向上一级即可拿到MatcherRule类型定义; - 返回值的判断依据是
route.method,这决定了该规则与isOfType(基于route.info.type)职责互补,前者过滤"路由种类",后者过滤"请求方法"。
2. 从 src/routes/rules/index.ts 导出规则
在规则桶文件中追加导出:
// ... other exports
export { isMethodIn } from './is-method-in';
// ^ export the rule from here
对照现有实现,rules/index.ts 目前只导出一条内置规则:
export { isOfType } from './is-of-type';
而 is-of-type.ts 的写法与指南示例完全同构,可以直接作为新规则的参照模板:
import type { MatcherRule } from '../types';
export const isOfType = (type: string): MatcherRule => {
return (route) => route.info.type === type;
};
3. 将新规则传给 generate 流程中的 RouteMatcher
最后一步在包入口 src/exports.ts 中完成。generate 函数负责把 Provider、Matcher 与装配器工厂装配成一个完整的生成管线,新规则需要传入 RouteMatcher 的规则数组:
// ...
export const generate = (strapi: Core.Strapi, options?: GenerationOptions): GeneratorOutput => {
// ...
const routeCollector = new RouteCollector(
[
/* ... */
],
new RouteMatcher([
// ... other rules
rules.isMethodIn(['POST', 'PUT']),
// ^ pass the new rule to the matcher instance
])
);
// ...
};
这里的 rules. 前缀并非随意书写。从 routes/index.ts 的导出方式可以看出,规则被打包成了一个命名空间后统一再导出:
export * as rules from './rules';
因此调用方以 rules.isOfType(...) 的命名空间形式访问所有规则,新增规则只要完成第 2 步的桶文件导出,即可立即通过 rules.isMethodIn 访问,无需改动 routes/index.ts 本身。
现有 generate 流程的真实代码
对照 exports.ts 中的实际实现,可以看到当前默认只装配了一条类型过滤规则,按 options.type(默认 'content-api')区分 Admin 与内容 API 路由:
export const generate = (strapi: Core.Strapi, options?: GenerationOptions): GeneratorOutput => {
const { type = 'content-api' } = options ?? {};
const config = {
preProcessors: new PreProcessorFactory().createAll(),
assemblers: new DocumentAssemblerFactory().createAll(),
postProcessors: new PostProcessorsFactory().createAll(),
};
// Data sources for the Strapi routes
const routeCollector = new RouteCollector(
[
new AdminRoutesProvider(strapi),
new ApiRoutesProvider(strapi),
new PluginRoutesProvider(strapi),
],
new RouteMatcher([
// Only match content-api routes
rules.isOfType(type),
])
);
const contextFactory = new DocumentContextFactory();
const generator = new OpenAPIGenerator(config, strapi, routeCollector, contextFactory);
return generator.generate();
};
从源码结构看,若在 RouteMatcher 规则数组中追加 rules.isMethodIn(['POST', 'PUT']),效果是在现有"仅 content-api 路由"的基础上再叠加一层"仅 POST/PUT 路由"的过滤,最终进入文档的路径只保留写操作接口。
三、规则生效的完整调用链:从 Provider 到过滤结果
新增规则只是管线的"过滤条件",理解它如何被消费,有助于验证规则是否生效。整条链路由 RouteCollector 串联,核心逻辑见 collector.ts:
public collect(): Core.Route[] {
const routes = this._providers.flatMap((provider) => Array.from(provider));
const sanitizedRoutes = this.filter(routes);
debug(
'collected %o/%o routes from %o providers %o',
sanitizedRoutes.length,
routes.length,
this._providers.length,
this._providers.map((provider) => provider.constructor.name)
);
return sanitizedRoutes;
}
private filter(routes: Core.Route[]): Core.Route[] {
return routes.filter((route) => this._matcher.match(route));
}
调用链可以概括为:
- 汇聚:
collect()用flatMap从所有RoutesProvider(当前内置AdminRoutesProvider、ApiRoutesProvider、PluginRoutesProvider)拉取全量路由; - 过滤:
filter()对每条路由调用this._matcher.match(route),即前面剖析的"全部规则通过才保留"; - 观测:通过
debug('routes:collector')输出collected N/M routes from K providers格式的日志,过滤前后数量对比会直接反映新规则的命中效果——这是验证isMethodIn是否按预期收窄路由集合的最直接手段。
RouteCollector 的构造函数签名也值得注意:matcher 参数缺省为一个无规则的 RouteMatcher,而无规则的 RouteMatcher 因 every 在空数组上恒为 true,等于不做任何过滤。这说明"不传规则"与"传空规则数组"语义等价,均为全量放行。
四、验证与测试:如何确认新规则正确
RouteMatcher 的行为已有专门的单元测试覆盖,位于 route-matcher.test.ts。四个用例分别验证:
- 单条规则精确命中目标路由(
route.path === '/users/123'时match为真); - 不满足规则的路由被拒绝(
/products/123不匹配); - 多条规则同时满足时整体匹配成功(
startsWith('/search')+method === 'GET'); - 任一规则失败则整体失败(AND 语义)。
这些用例恰好也演示了"内联匿名规则"与"工厂函数规则"两种等价写法:测试中直接用 (route) => route.method === 'GET' 这样的箭头函数作为规则,而指南推荐的 isMethodIn(['POST', 'PUT']) 只是把它封装成可复用、可传参的工厂。为新增的 isMethodIn 编写测试时,可以完全套用上述 Arrange-Act-Assert 结构,仅将规则替换为工厂返回值:
const matcher = new RouteMatcher([isMethodIn(['POST', 'PUT'])]);
expect(matcher.match({ path: '/posts', method: 'POST', info: { type: 'content-api' }, handler: '' })).toBeTruthy();
expect(matcher.match({ path: '/posts', method: 'GET', info: { type: 'content-api' }, handler: '' })).toBeFalsy();
相关测试文件与夹具(__tests__/fixtures/routes.ts、__tests__/mocks/route-matcher.mock.ts 等)位于 packages/core/openapi/tests 目录,贡献指南的 Testing 文档 提供了更多调试与测试约定。
五、小结:扩展要点速查
| 步骤 | 文件 | 要点 |
|---|---|---|
| 定义规则 | src/routes/rules/is-method-in.ts |
规则是 (route: Core.Route) => boolean 的工厂,从 '../types' 导入 MatcherRule |
| 桶导出 | src/routes/rules/index.ts |
export { isMethodIn } from './is-method-in',之后自动进入 rules 命名空间 |
| 装配使用 | src/exports.ts |
在 generate 的 RouteMatcher([...]) 规则数组中调用 rules.isMethodIn([...]) |
需要牢记的约束:多条规则之间是 AND 关系且会短路,规则的求值顺序影响性能;规则只影响"哪些路由进入文档",不改变路由本身的内容,路由的产出仍由各 Provider 负责。掌握这条过滤规则后,再配合 Routes provider 指南 扩展路由来源,就能完整控制 Strapi OpenAPI 文档生成的输入边界。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00