☰
Webiny 代码规范实践:Routes 只做传输,Use Cases 承载业务逻辑
2026/9/29 2:06:52 网站建设 项目流程
  • CMS
  • 后端
  • 前端

【免费下载链接】webiny-js

Open-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 的开源代码规范文档 routes-delegate-to-use-cases.md 确立了一条清晰的分层原则:HttpRoute是传输层而非功能代码,它的职责仅限于解析请求、调用一个 use case、并把结果映射为状态码;认证、编排、调用其他服务等业务逻辑必须收敛到 use case 及其抽象之后,以便脱离 HTTP 请求被解析、装饰与测试。本文以该规范为骨架,结合仓库中event-handler-core的路由实现与ai-powerups的真实用例,说明这条规则背后的架构动机与落地方式,读完你可以直接按此模式写出可复用、可测试、可装饰的 Webiny HTTP 路由。

核心原则:Route 是传输层,不是功能层

规范的第一句话定义了路由的边界:AnHttpRouteis transport, not feature code。一个合格的 Webiny 路由类只做三件事:

  1. 解析请求——从request中取出参数并做最基础的格式校验;
  2. 调用一个 use case——把解析结果交给抽象出来的用例接口;
  3. 映射结果——把 use case 的返回值或错误翻译成 HTTP 状态码与响应体。

业务逻辑——身份校验、跨服务编排、事件发布、仓库访问——一律不属于路由。它必须住进一个 use case,并且是"behind its own abstraction"(处在自己的抽象之后),即路由只依赖用例的Interface,而不是具体实现类。这样做的好处是规范原文点名的三件事:

  • 可以被解析(resolved):抽象与实现的绑定关系由依赖容器管理,路由构造时注入的是接口,具体实现何时替换、如何组合由 DI 决定;
  • 可以被装饰(decorated):装饰器可以包住用例接口,在真正执行前后注入审计、限流、重试等横切逻辑;
  • 可以在没有 HTTP 请求的情况下被测试(tested):用例不感知IHttpRequest,单测直接构造参数调用execute()即可。

仓库中这套抽象就定义在 packages/event-handler-core/src/features/http/abstractions.ts:路由处理逻辑实现HttpRouteHandler.Interface(即IHttpRoute,其handle(request, response)接收IHttpRequest与可链式调用的IHttpResponseBuilder),路由定义实现HttpRouteDefinition.Interface(name/method/path/handler四个纯数据字段)。定义与处理分离的设计,使得路由的匹配阶段零依赖、零开销,具体见下文。

好示例:薄路由 + 厚用例

规范给出的"好"示例是CreateThingRouteImpl,它把整条链路拆成了两层:

// Good class CreateThingRouteImpl implements HttpRoute.Interface { public readonly method = "POST"; public readonly path = "/things"; public constructor(private readonly createThing: CreateThingUseCase.Interface) {} public async handle(request: IHttpRequest): Promise<IHttpResponse> { const params = parseBody(request.body); if (!params) { return json(400, { error: "Invalid body." }); } return json(200, await this.createThing.execute(params)); } }

逐行拆解这条规范示例:

  • method与path是路由定义,不是行为。HttpRouter匹配请求时只读这两个字段(以及name),连用例都不需要被构建,见 HttpRouter.ts 中的match();
  • 构造参数注入的是CreateThingUseCase.Interface。这就是"behind its own abstraction"的字面实现——路由与具体实现零耦合;
  • parseBody是唯一被允许存在的"业务前处理",且它的产物只是传输格式(请求体)到领域输入(params)的转换,失败直接映射为 400;
  • 成功路径只有一行json(200, await this.createThing.execute(params)):用例负责返回领域结果,路由负责把它翻译成 HTTP 语义。

值得注意的是,示例中路由类把method/path声明为实例字段。Webiny 实际项目中通常更进一步,将定义拆成独立类并挂到HttpRouteDefinition抽象上(见下文AdminAssistant的真实实现),这样路由匹配阶段连处理类都不实例化——这正是 HttpRouter.ts 注释 中强调的优化动机:曾经为了读一个path就要解析整个依赖图,静态资源请求也会白白构建 GraphQL 引擎、上下文 schema 与 AI provider。

坏示例:业务逻辑泄漏进路由的代价

规范给出的"坏"示例把整个特性塞进了handle():

// Bad — the feature lives in the route, so nothing else can reuse or test it class CreateThingRouteImpl implements HttpRoute.Interface { public async handle(request: IHttpRequest): Promise<IHttpResponse> { const identity = this.identityContext.getIdentity(); if (identity.isAnonymous()) { return json(401, { error: "Authentication required." }); } const validated = validate(request.body); const created = await this.repository.create(validated); await this.eventPublisher.publish(new ThingCreatedEvent(created)); return json(200, created); } }

这段代码集中暴露了四类反模式,每一条都能在仓库中找到对应的"正解":

坏示例中的操作问题正确归属
identityContext.getIdentity()+ 匿名判定身份/授权是横切关注点,每个入口都要重复用例内部(见AdminAssistantUseCase.prepare()的NotAuthorizedError抛出)或装饰器
validate(request.body)校验逻辑无法脱离 HTTP 复用用例入口或领域层校验器
this.repository.create(validated)直接触碰仓库,绕过了用例的编排与事务边界用例内部,遵循 use cases go through repositories
eventPublisher.publish(...)事件发布是副作用,路由无法独立测试用例内部,随业务步骤一同编排

更重要的是注释点出的本质:特性住在路由里,其他入口就无法复用,也无法测试。同一份"创建 Thing"的流程,如果还要被后台任务、GraphQL resolver、定时器或另一个内部入口调用,就必须把逻辑从路由里抠出来重新包一层——而如果一开始就放在用例里,所有入口共享同一抽象即可。此外,HttpRoute的handle()直接依赖identityContext、repository、eventPublisher三个具体依赖,意味着测试必须构造真实的 HTTP 请求与全套基础设施;拆成用例后,单测只需要new CreateThingUseCase(fakeRepository)一把梭。

为什么"抽象之后"如此重要:解析、装饰、测试三件事

规范要求用例"behind its own abstraction",仓库中的机制让这三件事成为可操作的事实:

1. 可解析(resolved)。Webiny 的用例通过createAbstraction/createImplementation声明,抽象与实现的绑定携带依赖元数据。以 packages/ai-powerups/src/api/features/AdminAssistant/abstractions.ts 为例,用例抽象AdminAssistantUseCase只暴露一个stream()方法;其实现类在 AdminAssistantUseCase.ts 中通过createImplementation登记六个依赖:Ai、AiSdkTools、全部AiSdkToolDefinition({ multiple: true })、IdentityContext、AdminAssistantConfig、ResolveAiCapabilityUseCase。路由只 import 抽象,容器负责装配具体实现。

2. 可装饰(decorated)。装饰器的钩子点在于抽象本身:createAbstraction允许为同一抽象注册多个实现/装饰器,HttpRouter在构建匹配路由时调用resolveImplementation,该调用会应用为HttpRouteHandler注册的装饰器(HttpRouter.ts 的注释明确说明 "applies decorators registered forHttpRouteHandler, so a route stays decoratable")。用例层同理——AdminAssistant的 Wb 翻译功能就有WbTranslatePageDecorator,以装饰器形式包裹用例抽象。

3. 可测试(tested without an HTTP request)。路由与用例分离后,测试有两个层次:用例单测直接构造领域参数,完全不需要IHttpRequest;路由测试则通过测试专用的 HTTP 事件处理器。仓库的 packages/event-handler-core/src/features/testing/HttpRouterHandler.ts 提供了TestHttpEventHandler实现:它把ctx.event交给router.route(),把RouteNotFoundError映射为 404、带code的结构化错误映射为 500 并保留code/data字段供断言,其余兜底 500。也就是说,你可以构造一个普通对象形式的请求直接await router.route(...),无需启动任何 HTTP 服务器。

仓库中的真实范例:AdminAssistantStreamRoute 与 AdminAssistantUseCase

ai-powerups包的 Admin Assistant(AI 管理助手)是这条规范的完整落地样本,两个文件对照阅读即可看清边界:

路由侧——AdminAssistantStreamRoute.ts 把"定义"与"处理"拆成两个类:

class AdminAssistantStreamRouteImpl implements HttpRouteHandler.Interface { public constructor(private readonly assistant: AdminAssistantUseCase.Interface) {} public async handle( request: IHttpRequest, response: IHttpResponseBuilder ): Promise<IHttpResponseBuilder> { const parsed = parseChatBody(request.body); if (!parsed) { return response.status(400).json({ error: BAD_REQUEST_MESSAGE }); } const events = this.assistant.stream(parsed); const frames = toSseFrames(events); return response.sse(frames); } } export const AdminAssistantStreamRoute = HttpRouteHandler.createImplementation({ implementation: AdminAssistantStreamRouteImpl, dependencies: [AdminAssistantUseCase] }); class AdminAssistantStreamRouteDefinitionImpl implements HttpRouteDefinition.Interface { readonly name = "admin-assistant-stream"; readonly method = "POST"; readonly path = "/stream/ai/admin-assistant"; readonly handler = AdminAssistantStreamRoute; }

对照规范逐条验证:路由只做请求体解析(parseChatBody,失败映射 400)、调用用例的stream()、把异步事件帧映射为 SSE 响应——没有任何身份判断、没有调用任何 AI SDK、没有触碰任何仓库。连注释都贯彻了这一哲学:"what events exist, and what they carry, belongs to the feature, so the mapping lives here"——SSE 帧的线格式来自event-handler-core,而事件本身属于特性,因此"帧映射"这一小段传输适配逻辑才被允许留在路由里。文件注释还解释了为何额外开一条流式路由而非给缓冲路由加开关:两种响应的契约(JSON 对象 vs 事件流)本质不同,客户端按 URL 选择,同时缓冲路由保留给无法读流的调用方。

用例侧——AdminAssistantUseCase.ts 承载了全部业务逻辑,这正是规范要求的"auth checks, orchestration, calls to other services":

  • 身份校验:prepare()第一步if (this.identityContext.getIdentity().isAnonymous()) throw new NotAuthorizedError();
  • 能力解析:resolveCapability.execute(ADMIN_ASSISTANT_CAPABILITY)决定模型与连接,失败时抛出携带"去 Settings → AI Power-Ups → Model roles 选择模型"信息的错误;
  • 跨服务编排:调用aiSdkTools.getToolSet()、按readOnlyHint过滤只读工具、通过toolApproval回调把写操作暂停为人工审批;
  • 工具调用与事件产出:stream()是一个 async generator,把 AI SDK 的流式 part 翻译成text/tool-call/tool-result/tool-error/approval/error/done事件——路由只负责把这些事件帧化。

用例的注释还记录了一个典型教训:必须用responseMessages(累计历史)而非response.messages(仅最后一步),否则审批恢复时approvalId对不上、模型只会重复提出改动——这类业务细节天然属于用例层,路由永远不需要关心。

边界判断清单:写 Route 前先过一遍

综合规范文档与仓库实现,可以提炼出下面这张自查清单,用于判断一段代码是否应该放进路由:

  • 这段代码是否只与"传输"有关(解析请求体、映射状态码、选择响应类型)?
  • 它是否只调用一个用例接口,且以构造注入而非静态引用的方式获得?
  • 身份/权限校验是否出现在路由里?——是,则移到用例(如AdminAssistantUseCase.prepare())或装饰器;
  • 是否直接 new 仓库、发布事件、调用第三方 SDK?——是,则这些都应发生在用例内部;
  • 业务逻辑离开 HTTP 后是否仍可被另一个入口(GraphQL resolver、后台任务、定时器)复用?
  • 用例能否在完全不构造IHttpRequest的情况下被单元测试?

如果答案有任何一项不满足,规范的建议是:把逻辑下沉到用例抽象之后,路由保持"薄"。这样做的收益在 Webiny 的架构里是被源码实证的——HttpRouter.ts 通过"定义与处理分离 + 惰性构建匹配路由"避免了每次请求解析全部依赖图,而薄路由 + 用例抽象的组合,恰恰是让这套惰性机制成为可能的先决条件:匹配只需纯数据,处理才需要依赖图。反过来说,把业务塞进路由,既破坏了可测试性,也让每一次请求都背上整棵依赖图。

小结

routes-delegate-to-use-cases.md用一页篇幅定义了一条可执行的架构纪律:路由只做翻译,用例只做业务,抽象隔开两者。在 Webiny 中,这条纪律由HttpRouteHandler/HttpRouteDefinition/ 用例createAbstraction三件套从机制上支撑——定义与处理分离让匹配零成本,接口注入让用例可解析可装饰,测试用事件处理器让路由与用例都能脱离 HTTP 独立验证。照着 AdminAssistantStreamRoute.ts 与 AdminAssistantUseCase.ts 这对样本写新特性,就是对该规范最直接的实践。

  • CMS
  • 后端
  • 前端

【免费下载链接】webiny-js

Open-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
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询