- 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.
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 路由类只做三件事:
- 解析请求——从
request中取出参数并做最基础的格式校验; - 调用一个 use case——把解析结果交给抽象出来的用例接口;
- 映射结果——把 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.
相关推荐
2025年Android Developer Roadmap完全指南:业务逻辑封装与实战应用
2025年Android Developer Roadmap完全指南:业务逻辑封装与实战应用 Android Developer Roadmap是一份全面的学习
移动开发教程低代码业务逻辑:JeecgBoot Online代码编辑器
低代码业务逻辑:JeecgBoot Online代码编辑器 你是否还在为企业级应用开发中的复杂业务逻辑编写而烦恼?是否希望有一种工具能让你无需深入编程细节就能快
低代码后端前端AI 应用大模型RAG工作流自动化如何快速掌握yidaRule:动态规则引擎的终极实践指南
如何快速掌握yidaRule:动态规则引擎的终极实践指南 yidaRule(益达规则仓库)是一款颠覆传统业务逻辑的动态规则引擎,帮助开发者轻松管理和应用各类规则
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考