尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Webiny 代码风格指南:禁止把 Container 当作 Service Locator(依赖注入最佳实践)

发布时间:2026/9/28 20:17:21

资讯中心
01
ARTICLE

Webiny 代码风格指南:禁止把 Container 当作 Service Locator(依赖注入最佳实践)

Webiny 代码风格指南:禁止把 Container 当作 Service Locator(依赖注入最佳实践)
CMS后端前端【免费下载链接】webiny-jsOpen-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 是一个基于 TypeScript、运行在 AWS ServerlessLambda、DynamoDB、S3之上的开源自托管 CMS 平台其全栈代码大量采用依赖注入DI与实现声明createImplementation模式。本文是 Webiny 仓库中 no-container-as-service-locator.md 代码风格指南的完整解读它回答了一个核心问题——实现类implementation class应当如何正确获取依赖读完本文你将掌握把依赖声明在dependencies中、通过构造函数注入这一规则理解container.resolve(...)在哪些场景是合理的、在哪些场景是反模式并看到HttpRouter、AssetDeliveryRoute、WebsiteBuilderRedirectsRoute等仓库真实代码如何体现或暂时违反这一规则。一、规则核心声明依赖而不是在方法体内现取Webiny 中每一个可被容器实例化的对象都通过类似Xxx.createImplementation({ implementation, dependencies })的方式注册。dependencies数组声明了这个实现需要哪些依赖容器据此构建依赖图并注入。规则原文在类的dependencies中声明它需要什么并通过构造函数接收它。不要在类的方法内部注入RequestContainer或Container后调用container.resolve(...)—— 这样做会把真实的依赖从类签名、DI 依赖图以及任何阅读该类代码的人面前隐藏起来。反模式示例Bad// Bad class MyRouteImpl implements HttpRouteHandler.Interface { constructor(private container: Container) {} async handle(request: IHttpRequest) { const prepare this.container.resolve(PrepareUseCase); const ai this.container.resolve(Ai); // ... } } export const MyRoute HttpRouteHandler.createImplementation({ implementation: MyRouteImpl, dependencies: [RequestContainer] });问题在哪里签名说谎MyRouteImpl的构造函数只接受一个Container读者无法从签名得知它真正依赖PrepareUseCase和AiDI 图失真dependencies: [RequestContainer]只记录了一个依赖容器在实例化时并不会主动构建PrepareUseCase和Ai的依赖树潜在的错误被推迟到handle()运行时才暴露可测试性差单测中必须注入一个完整可用的容器再通过resolve获取具体实现无法直接传入 mock 依赖。正确做法Good// Good class MyRouteImpl implements HttpRouteHandler.Interface { constructor( private prepare: PrepareUseCase.Interface, private ai: Ai.Interface ) {} async handle(request: IHttpRequest) { // ... } } export const MyRoute HttpRouteHandler.createImplementation({ implementation: MyRouteImpl, dependencies: [PrepareUseCase, Ai] });改进点一目了然签名即真相构造函数参数就是全部依赖DI 图完整容器在构建MyRoute时就会递归构建PrepareUseCase与Ai任何缺失的注册会在构建期失败而不是在请求处理时面向接口注入的是PrepareUseCase.Interface与Ai.Interface测试中可轻松替换为 mock 实现。从源码结构看这一约定与 Webiny 的createImplementation机制深度绑定实现类的dependencies元数据正是容器如packages/di中的Container.resolveImplementation解析该实现时读取的依赖清单见下文HttpRouter源码注释HttpRouter.ts中关于resolveImplementation的说明——它asks for exactly the matched routes class: it reads that routes dependencies from its own metadata读取路由自身元数据中声明的依赖来解析。二、唯一的合法例外createFeature的resolve()钩子规则并不是一刀切地禁止所有container.resolve(...)调用。文档明确指出container.resolve(...)IS correct in acreateFeatureresolve()hook — that hook exists to hand resolved instances to callers. This rule is about implementation classes.即在createFeature的resolve()钩子中使用container.resolve(...)是正确且必要的——该钩子的职责就是把解析好的实例交付出给调用方例如向其他 feature 暴露公开 API 对象。这条规则约束的对象是实现类implementation class而不是 feature 组装层。理解这一例外很重要createFeature是 Webiny 中把一组实现注册进容器的入口参见packages/api-website-builder/src/WebsiteBuilderFeature.ts等 feature 文件它本身承担容器装配职责属于依赖图构建的一环而实现类路由、用例、处理器是消费者应当只接收成品依赖。区分这两类代码是落地本规则的第一步。三、为什么HttpRouter可以持有 Container设计上有意的例外规则的另一半是HttpRouter本身接收容器是刻意为之deliberate。文档给出了三点理由全部可以在 HttpRouter.ts 中得到印证1. 只构建匹配到的那个路由HttpRouterImplClass的构造函数签名为constructor(private container: Container) {}HttpRouter.ts其route()方法核心逻辑是async route(request: IHttpRequest): PromiseIHttpResponse { for (const definition of this.container.resolveAll(HttpRouteDefinition)) { const params this.match(definition, request); if (params null) { continue; } const route this.container.resolveImplementation(definition.handler); // ... 调用 route.handle(...) } throw new RouteNotFoundError(request.method, request.path); }HttpRouter.ts路由器先通过match()用method和path匹配所有HttpRouteDefinition只有命中后才调用resolveImplementation(definition.handler)构建该路由的实现类。换句话说HttpRouter持容器是为了完成按需实例化这一动态行为——这正是它在整个 DI 体系中的特殊职责。2. 路由定义Definition本身零依赖匹配过程遍历的是HttpRouteDefinition实现。如AssetDeliveryRouteDefinition所示见下文路由定义类只声明name、method、path、handler四个只读字段dependencies: []为空class AssetDeliveryRouteDefinitionImpl implements HttpRouteDefinition.Interface { readonly name asset-delivery; readonly method GET; readonly path /files/*; readonly handler AssetDeliveryRoute; } export const AssetDeliveryRouteDefinition HttpRouteDefinition.createImplementation({ implementation: AssetDeliveryRouteDefinitionImpl, dependencies: [] });AssetDeliveryRoute.ts源码注释明确写道What the router matches on. Zero dependencies, so building it costs nothing.——匹配一个路由不会构建任何东西只是几个字段的赋值。matchPathHttpRouter.ts支持/*前缀通配与:param路径参数解析纯字符串操作、零容器参与。3. 一次关键的历史重构路由不再每请求全量构建HttpRouter的源码注释HttpRouter.ts记录了一次重要的性能修复Routes used to be resolved as instances just to read theirpath, which built every one of their dependency graphs on every request: a static-asset request constructed the whole GraphQL engine, every contextual schema and the AI provider before discovering it wanted none of them.即旧实现为了读取path就把所有路由都实例化导致一个静态资源请求也会把整个 GraphQL 引擎、每个上下文 schema 和 AI provider 的依赖树全部构建一遍。现在的实现改为只构建匹配的路由这就是文档所说a route is built only once its definition matches路由仅在定义匹配时被构建一次的出处。四、遗留的反模式两个仍在使用 Service Locator 的路由不要照抄文档特别点名了两个路由——AssetDeliveryRoute与WebsiteBuilderRedirectsRoute——仍然在handle()内部懒解析依赖并解释了它们当初为何这样做、如今为何已无必要That was once necessary twice over: route construction ran before the request-context initializers, so reaching a request-time token threw, and every route was built on every request whether or not it matched. Neither is true now — those tokens are providers, the initializers are deleted, and a route is built only once its definition matches. Declare dependencies instead; dont copy them.历史原因有两条且两条都已被推翻旧事实路由构建发生在 request-context 初始化器之前——在路由构建阶段去触碰 request-time token 会抛异常新现实这些 token 现在都是 provider按需惰性解析且初始化器已被删除。旧事实每个请求都会构建全部路由——无论是否匹配新现实路由只在定义匹配后才构建。AssetDeliveryRoute 的实际代码AssetDeliveryRoute.ts 中AssetDeliveryRouteImpl的构造函数只接收Container在handle()内通过this.container.resolve(...)取四个协作者class AssetDeliveryRouteImpl implements HttpRouteHandler.Interface { constructor(private container: Container) {} async handle(request: HttpRouteHandler.Request, response: HttpRouteHandler.Response) { // Resolve asset-delivery collaborators lazily (request time), not as constructor deps. const requestResolver this.container.resolve(AssetRequestResolver); const assetResolver this.container.resolve(AssetResolver); const assetProcessor this.container.resolve(AssetProcessor); const outputStrategy this.container.resolve(AssetOutputStrategy); // ... } } export const AssetDeliveryRoute HttpRouteHandler.createImplementation({ implementation: AssetDeliveryRouteImpl, dependencies: [RequestContainer] });源码注释解释了当初懒解析的动机AssetProcessor的PrivateFilesAssetProcessor装饰器会拉入GetFileUseCase → CMS entry repositories而后者依赖仅在 CMS GraphQL 请求进行中才注册的EntryFromStorageTransform——在路由构建阶段解析会导致非资源请求如/graphql触发这条尚未注册的依赖链。值得注意的是这份注释描述的场景与文档给出的历史原因一致构建时序与注册时机问题而dependencies: [RequestContainer]依然是对规则的违反。WebsiteBuilderRedirectsRoute 的实际代码WebsiteBuilderRedirectsRoute.ts 更明确地留下了整改 TODOclass WebsiteBuilderRedirectsRouteImpl implements HttpRouteHandler.Interface { constructor(private container: Container) {} async handle(_request: HttpRouteHandler.Request, response: HttpRouteHandler.Response) { // TODO: declare these as constructor dependencies. They were resolved lazily because the // router used to construct every route on every request to path-match, which is no longer // true — a route is built only once its definition matches. const identityCtx this.container.resolve(IdentityContext); const getActiveRedirects this.container.resolve(GetActiveRedirectsUseCase); // ... } }WebsiteBuilderRedirectsRoute.ts源码中的 TODO 注释与文档结论完全呼应路由每个请求全量构建以做路径匹配的旧机制已经不存在懒解析的前提已消失应当改为把IdentityContext与GetActiveRedirectsUseCase声明为构造函数依赖。对读者的实操建议以AssetDeliveryRoute和WebsiteBuilderRedirectsRoute为反例不要复制它们的写法编写新路由时一律采用Good模式构造函数接收UseCase.Interface/ providerdependencies声明对应 token若你正在维护这两个文件应按源码中的 TODO 逐步迁移把dependencies: [RequestContainer]替换为真实的依赖清单并把handle()内的resolve调用改为构造函数注入。五、落地清单如何自查你的实现类是否符合规则将本规则固化为一条可执行的检查清单检查点合规写法违规信号构造函数参数只接收具体依赖的接口如PrepareUseCase.Interface、Ai.Interface构造函数只接收Container/RequestContainerdependencies声明列出每一个真实依赖 tokendependencies: [PrepareUseCase, Ai]dependencies: [RequestContainer]与类内resolve的 token 对不上方法体内只使用构造函数注入的依赖方法体内出现this.container.resolve(X)解析时机依赖在构建期由容器完成解析与注入依赖延迟到运行时才被发现、解析测试方式直接传入 mock 依赖实例即可实例化测试必须先构造一个完整容器才能resolve结合本文第四节可以补充两条边界判定你在写实现类路由、用例、处理器等被容器实例化、执行具体业务逻辑的类→禁止Service Locator你在写装配层createFeature的resolve()钩子、HttpRouter这类按需构建的动态路由器→container.resolve(...)是合理且必要的。六、总结Webiny 的这条代码风格规则本质上是一条可读性、可测试性与构建期安全的约定把依赖明说在构造函数签名和dependencies元数据中让 DI 图在构建期就完整、让阅读者一眼看清类的所有协作方同时把动态解析的职责收拢到createFeature.resolve()与HttpRouter这类特殊装配点。仓库中HttpRouter的注释与实现展示了一个有意持有容器的边界案例而AssetDeliveryRoute与WebsiteBuilderRedirectsRoute则作为历史遗留反例时刻提醒后来者环境已经改变路由按需构建、初始化器已删除、token 已是 provider旧的懒解析理由不再成立——请声明依赖不要照抄旧代码。相关参考文件规则原文no-container-as-service-locator.md路由器实现HttpRouter.ts遗留反例一AssetDeliveryRoute.ts遗留反例二WebsiteBuilderRedirectsRoute.ts赞分享CMS后端前端【免费下载链接】webiny-jsOpen-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点击查看免费下载相关推荐Vapor依赖注入Service容器和依赖管理最佳实践Vapor依赖注入Service容器和依赖管理最佳实践 在现代Web应用开发中依赖注入Dependency Injection是构建可测试、可维护和松耦后端Web框架ExplorerPatcher5分钟让Windows 11拥有经典界面告别不适应ExplorerPatcher5分钟让Windows 11拥有经典界面告别不适应 你是否怀念Windows 10的经典开始菜单和任务栏是否对Window桌面应用系统编程gh_mirrors/sh1/sh的依赖注入最佳实践最佳实践指南gh_mirrors/sh1/sh的依赖注入最佳实践最佳实践指南 在现代软件开发中依赖注入Dependency InjectionDI是一种重要的设计开发工具CLI创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。