Skip to content

依赖注入(DI)

Zebra 的 DI 是框架的核心,不是可选功能。每个应用都围绕一个 Container 构建,路由与中间件声明自己的依赖,容器在启动时校验整张依赖图。

声明可注入类

@injectable() 标记类,构造函数依赖由 emitDecoratorMetadata 自动推断,或用 @inject() 显式指定:

ts
import { inject, injectable } from "@zebra-web/zebra";

@injectable()
class UserRepo {
  // 自动推断:构造参数类型来自 design:paramtypes
  constructor(private db: Database) {}
}

@injectable()
class AuthService {
  // 显式指定:适用于抽象类 / token / 接口场景
  constructor(@inject(UserRepo) private repo: UserRepo) {}
}

注册绑定

Zebra 实例上注册:

ts
const z = new Zebra();

z.injectSingleton(UserRepo);       // 类 → 自身(.toSelf()),singleton 作用域
z.injectSingleton(IRepo, MockRepo); // 抽象标识 → 具体实现
z.injectRequest(Service);           // 每个请求一个新实例
z.injectTransient(Service);         // 每次解析新实例
z.injectSession(Service);           // 每个会话一个实例
z.injectValue(TOKEN, value);        // 绑定一个已存在的值(单例)

// 工厂绑定:懒形式(接收容器)与声明依赖形式
z.injectFactorySingleton(TOKEN, (c) => new Service(c.resolve(Dep)));
z.injectFactorySingleton(TOKEN, { dep: Dep }, ({ dep }) => new Service(dep));

injectFactory* 也支持 Request / Transient / Session 三种变体。

标识符(Identifier)

路由与绑定里的 id 可以是:

类型说明
ClassConstructor<T>具体类(Greeter
AbstractConstructor<T>抽象类(IRepo
Token<T>语义化 token:const DB = token<Database>("DB")
ts
import { token } from "@zebra-web/zebra";

export const DB = token<Database>("db");

z.injectValue(DB, new Database());

z.get("/users", { db: DB }, async (req, { db }) => db.query(...));

Token 绑定到工厂或值,是「接口 + 实现」解耦(尤其跨包共享标识)的标准方式。isToken() 可做运行时判定。

四种作用域

作用域生命周期缓存位置
Singleton整个应用生命周期一个实例根容器
Session每个会话 id 一个实例,空闲 TTL 后回收会话子容器
Request每个请求一个实例,请求结束 dispose请求子容器
Transient每次解析都新建无缓存

依赖关系有作用域约束:singleton 不能依赖 request/session 作用域的依赖(它比它们活得更久)。canDependOn(consumer, dependency) 定义了规则:依赖的 rank 必须 ≤ 消费者的 rank(singleton(0) < session(1) < request(2) < transient(3));transient 无缓存,任何作用域都可安全依赖。违规会在启动时被 validateGraph 捕获并抛 ScopeMismatchError

Session 作用域

Session 作用域需要给 Zebra 配置一个会话解析器(如何从请求里拿到 session id):

ts
const z = new Zebra({
  session: {
    resolver: (req) => extractSessionId(req), // 返回 string | undefined
    ttl: 30 * 60 * 1000,                       // 空闲 TTL
  },
});
  • 解析器返回的 id 会在容器里开一个「会话子容器」,该 id 的所有 Session 作用域依赖缓存在其中。
  • 会话空闲超过 ttl 后自动回收(dispose);app.disposeSession(id) 可手动立即回收。
  • 解析器返回 undefined 时,请求使用一个临时会话容器(请求结束即释放)——匿名访问也有会话语义,但不会跨请求保留。

@zebra-web/session 一起用时,sessionMiddleware() 返回对象自带 resolver,把它接进 Zebra 构造选项即可让 cookie 会话和 session 作用域 DI 协同工作。见 会话章节

命名对象路由 DI

路由与中间件用「命名对象」声明依赖,handler 的第二个参数拿到解析结果,类型完全对应:

ts
z.get("/hi/:name", { g: Greeter, db: DB }, async (req, { g, db }) => {
  // g: Greeter, db: Database —— 与声明一一对应
});

自带 Container 的高级用法

ts
import { Container } from "@zebra-web/zebra";

const container = new Container();
container.bind(IRepo).to(MockRepo);
container.bind(DB).toFactory((c) => new Database(c.resolve(Config)));
container.bind(TOKEN).toValue(instance);

const z = new Zebra({ container });

BindingBuilder 提供的链式方法:

方法作用
.to(cls)绑定到实现类
.toSelf()绑定到自身(配合 injectSingleton(Cls) 的简写)
.toFactory(fn)懒工厂,接收容器
.toFactoryWithDeps(deps, fn)声明式工厂,接收已解析依赖
.toValue(v)绑定已存在值
.inSingletonScope() / .inSessionScope() / .inRequestScope() / .inTransientScope()设置作用域

容器还支持:

  • rebind(id) —— 解绑后重新绑定(测试替身)。
  • snapshot() / restore() —— 保存/恢复绑定与实例快照(测试隔离)。
  • createChildScope(kind) —— 手动创建子容器。
  • resolve(id) —— 手动解析(非路由路径使用,比如 ws 消息处理)。

启动时图校验

listen() 时(performPreparevalidateGraph 会检查:

  1. 所有路由 / 中间件声明的依赖都已绑定 → 否则抛 UnboundTokenError(带解析链)。
  2. 构造依赖是否存在循环 → CircularDependencyError(列出环)。
  3. 作用域是否违规 → ScopeMismatchError

之后容器 freeze()任何注册(路由、绑定、中间件、钩子)都会在 listen() 之后抛错

释放(Disposable)

实现 Disposabledispose(): Promise<void>)的实例会在以下时机被自动释放:

  • 请求结束(request 作用域);
  • 会话回收 / disposeSession(id)(session 作用域);
  • app.stop()(singleton 作用域)。

释放顺序是 LIFO(依赖先于被依赖者)。isDisposable() 是运行时判定。

错误一览

错误触发
UnboundTokenError解析到未绑定的标识(含解析链)
CircularDependencyError构造函数依赖成环
ScopeMismatchErrorsingleton 依赖低层作用域等违规
RangeErrorsession.ttl / gracePeriod / requestTimeout 配置非法

下一步

Built with VitePress · MIT Licensed