依赖注入(DI)
Zebra 的 DI 是框架的核心,不是可选功能。每个应用都围绕一个 Container 构建,路由与中间件声明自己的依赖,容器在启动时校验整张依赖图。
声明可注入类
用 @injectable() 标记类,构造函数依赖由 emitDecoratorMetadata 自动推断,或用 @inject() 显式指定:
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 实例上注册:
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") |
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):
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 的第二个参数拿到解析结果,类型完全对应:
z.get("/hi/:name", { g: Greeter, db: DB }, async (req, { g, db }) => {
// g: Greeter, db: Database —— 与声明一一对应
});自带 Container 的高级用法
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() 时(performPrepare)validateGraph 会检查:
- 所有路由 / 中间件声明的依赖都已绑定 → 否则抛
UnboundTokenError(带解析链)。 - 构造依赖是否存在循环 →
CircularDependencyError(列出环)。 - 作用域是否违规 →
ScopeMismatchError。
之后容器 freeze():任何注册(路由、绑定、中间件、钩子)都会在 listen() 之后抛错。
释放(Disposable)
实现 Disposable(dispose(): Promise<void>)的实例会在以下时机被自动释放:
- 请求结束(request 作用域);
- 会话回收 /
disposeSession(id)(session 作用域); app.stop()(singleton 作用域)。
释放顺序是 LIFO(依赖先于被依赖者)。isDisposable() 是运行时判定。
错误一览
| 错误 | 触发 |
|---|---|
UnboundTokenError | 解析到未绑定的标识(含解析链) |
CircularDependencyError | 构造函数依赖成环 |
ScopeMismatchError | singleton 依赖低层作用域等违规 |
RangeError | session.ttl / gracePeriod / requestTimeout 配置非法 |