ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Inversify-Express-Utils 源码剖析(二):HttpContext 与子容器的每请求依赖隔离设计

2026/8/27 17:03:03 拓冰建站 浏览量
Inversify-Express-Utils 源码剖析(二):HttpContext 与子容器的每请求依赖隔离设计 Inversify-Express-Utils 源码剖析二HttpContext 与子容器的每请求依赖隔离设计【免费下载链接】inversify-express-utilsSome utilities for the development of Express application with InversifyJS项目地址: https://gitcode.com/gh_mirrors/in/inversify-express-utilsinversify-express-utils是基于 InversifyJS 依赖注入框架构建 Express 应用的实用工具库。本文剖析它最核心的设计——HttpContext与**每请求创建一个子容器child container**的依赖隔离机制看懂它是如何用不到几十行代码实现每个 HTTP 请求一套独立依赖容器的。适合刚接触 TypeScript 依赖注入、想了解 Web 框架如何处理请求级状态的新手阅读。 为什么单例容器不够用在典型的 InversifyJS 应用中根容器是单例的一个 Service 被注入后所有请求共享同一个实例。对无状态的服务这没问题可一旦需要当前用户TraceId这类请求相关的值单例模型就开始失灵用户 A 的 token 不能被用户 B 的并发请求看到请求头里的 TraceId 必须只对本次请求可见。inversify-express-utils 的解法是根容器只当全局配置每个进入的 HTTP 请求都通过container.createChild()创建一个独立的子容器所有请求级依赖只绑定在这个子容器里请求结束即消失。 HttpContext 解剖请求的载体卡HttpContext接口定义在src/interfaces.ts只有 4 个属性export interface HttpContextT unknown { container: inversifyInterfaces.Container; // 本次请求的子容器 request: Request; // express 请求对象 response: Response; // express 响应对象 user: PrincipalT; // 当前用户身份 }一眼就能看出设计意图HttpContext 是每请求状态的唯一载体——容器是本次请求的req/res 是本次请求的user 也是为本次请求认证的。HttpContext 在哪里创建src/server.ts的InversifyExpressServer.build()会注册一个最先执行的全局中间件this._app.all(*, (req, res, next) { const httpContext await this._createHttpContext(req, res, next); Reflect.defineMetadata(METADATA_KEY.httpContext, httpContext, req); next(); });两个关键细节_createHttpContext做两件事调用AuthProvider.getUser()解析出当前用户Principal再调用this._container.createChild()生成本请求的子容器上下文对象通过Reflect.defineMetadata挂到 req 上。后续的中间件和处理器随时可以从 req 上取回全程无需手动传参。对应的元数据键常量inversify-express-utils:httpcontext定义在src/constants.ts的METADATA_KEY中。️ 子容器三行代码实现每请求依赖隔离核心就在那个源码头注释里private async _createHttpContext(req, res, next) { const principal await this._getCurrentUser(req, res, next); return { // We use a childContainer for each request so we can be // sure that the binding is unique for each HTTP request container: this._container.createChild(), request: req, response: res, user: principal, }; }子容器的三个特性让隔离天然成立特性效果继承子容器能解析根容器里注册的所有绑定常规单例服务照常工作隔离在子容器里bind()的依赖只对当前请求可见其他请求看不到短暂请求结束后子容器无人引用自动被 GC无需手动清理src/server.ts里还有两个精巧的小动作值得注意构建期的假上下文registerControllers()会先在根容器绑定一个空对象顶替HttpContext保证注册阶段从容器实例化控制器时不会解析失败请求期替换为真的handlerFactory()会先在子容器执行bind(TYPE.HttpContext).toConstantValue(httpContext)然后才用子容器解析控制器。所以控制器注入到的httpContext永远是当前请求的那一份。 控制器与中间件如何拿到 HttpContext控制器一行injectHttpContextsrc/decorators.ts导出的injectHttpContext其实就是inject(TYPE.HttpContext)的别名。BaseHttpController已经替你声明好了这个依赖injectable() export class BaseHttpController { injectHttpContext protected readonly httpContext!: HttpContext; ... }继承BaseHttpController的控制器可以直接用this.httpContext.request、this.httpContext.user等零配置。如果你写不继承基类的自定义控制器手动加一个injectHttpContext装饰器即可src/test/http_context.test.ts里有一个完整示例。中间件单例实例 每请求上下文的混合体src/base_middleware.ts的BaseMiddleware有一个httpContext属性它由src/server.ts的resolveMiddlewere()在每次调用时重新赋值if (middlewareInstance instanceof BaseMiddleware) { return (req, res, next) { const m this._container.get(middlewareItem); m.httpContext this._getHttpContext(req); // 注入本次请求的上下文 m.handler(req, res, next); }; }注意这里的巧思中间件实例本身来自根容器单例没问题可复用但它的httpContext每次都被换成当前请求的子容器——状态跟随请求走实例可以复用两者互不冲突。更重要的是BaseMiddleware提供了一个bind()快捷方法它直接绑定到this.httpContext.containerprotected bindT(serviceIdentifier: ServiceIdentifierT) { return this.httpContext.container.bind(serviceIdentifier); }这就是**请求级服务request-scope services**功能的入口。 测试如何验证100 个并发请求下的隔离性src/test/base_middleware.test.ts用两个测试把这个机制验证得很扎实并发不串号TracingMiddleware通过bind(TYPES.TraceIdValue)把请求头X-Trace-Id绑进子容器下游Service注入该值。测试并发发出 100 个请求每个带不同的 TraceId并断言每个请求拿到的正是自己头里的 TraceId——证明并发下无任何交叉污染不泄漏出请求边界TransactionMiddleware给子容器绑定一个事务串。走/1端点挂中间件时每次请求都拿到递增的新值走/2端点不挂中间件时该绑定根本解析不到——证明请求级绑定既不会跨请求也不会泄漏回根容器。这两个测试基本就是子容器隔离设计的规格说明阅读源码前建议先看一遍。 要点回顾HttpContextsrc/interfaces.ts承载四样东西请求级子容器、req/res、当前用户 Principal每请求一个子容器src/server.ts的_createHttpContext中createChild()生成并用Reflect.defineMetadata挂到 req 上供全链路取用控制器经injectHttpContext或继承BaseHttpController获得上下文中间件由resolveMiddlewere()每次调用注入上下文请求级服务通过BaseMiddleware.bind()写入子容器仅在本请求内生效整个设计的代价只是每请求一次createChild()调用换来的是业务代码不再需要手工管理请求级上下文——它们全部变成普通的依赖注入这正是 inversify-express-utils 把 InversifyJS 与 Express 结合得如此自然的关键。【免费下载链接】inversify-express-utilsSome utilities for the development of Express application with InversifyJS项目地址: https://gitcode.com/gh_mirrors/in/inversify-express-utils创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考