InferDI
InferDI 是 TypeScript 的依赖注入容器,不使用装饰器、反射或运行时依赖。@inferdi/hono 中间件会在每次调用时创建请求作用域,通过 c.var.di 暴露该作用域,并在路由处理链结束后释放它。
容器的类型包含其依赖图。TypeScript 会报告缺失或顺序错误的依赖,以及无效的生命周期关系。
安装
npm install @inferdi/inferdi @inferdi/honoNOTE
InferDI 也发布在 JSR 上。使用 Deno 时,运行 deno add jsr:@inferdi/inferdi jsr:@inferdi/hono npm:hono。
入门
1. 构建容器
请求数据应位于 HTTP 边界。定义一个小型的应用类型,而不是将 Hono 的 Context 放入依赖图。
// container.ts
import { Container } from '@inferdi/inferdi'
import { connectDatabase, UserService } from './services'
export interface RequestContext {
readonly requestId: string
readonly userId: string | undefined
}
const config = {
dsn: 'postgres://localhost/app',
} satisfies { readonly dsn: string }
export const root = new Container()
.registerValue('config', config)
.declareScopeInputs<{ request: RequestContext }>()
.registerAsyncFactory(
'db',
(config: typeof config) => connectDatabase(config.dsn),
['config']
)
.registerClass('users', UserService, ['db', 'request'], 'scoped')declareScopeInputs() 只向依赖图添加类型占位,不会注册值。由于 users 依赖 request,InferDI 不允许在作用域提供该输入之前解析此服务。users 使用作用域生命周期,因为单例不能依赖请求作用域的数据。
2. 创建请求作用域
使用函数创建应用所需的具体作用域:
const openRequestScope = (request: RequestContext) =>
root.createScope({ request })
type RequestScope = ReturnType<typeof openRequestScope>3. 添加中间件
InferdiHonoScopeEnv 通过 Hono 上下文变量暴露具体的、已就绪的 RequestScope。
import { Hono } from 'hono'
import { inferdiHono, type InferdiHonoScopeEnv } from '@inferdi/hono'
type AppEnv = InferdiHonoScopeEnv<RequestScope>
const app = new Hono<AppEnv>()
const createRequestScope = (userId: string | undefined) =>
openRequestScope({
requestId: crypto.randomUUID(),
userId,
})
app.use(
'*',
inferdiHono({
container: root,
createScope: (_root, c) =>
createRequestScope(c.req.header('x-user-id')),
})
)当中间件使用根容器的无参数 createScope() 返回的类型时,InferdiHonoEnv<typeof root> 仍然适用。自定义作用域工厂可以像本例一样返回更具体的类型,因此本示例使用 InferdiHonoScopeEnv<RequestScope>。
4. 解析服务
对于已就绪的同步键,使用 get();对于声明式异步键,使用 getAsync()。request 输入是同步的,而 users 依赖 db,因此是异步的。
app.get('/users/:id', async (c) => {
const request = c.var.di.get('request') // RequestContext
const users = await c.var.di.getAsync('users') // UserService
const user = await users.profile(c.req.param('id'))
return c.json({ requestId: request.requestId, user })
})
export default appc.get('di') 等同于 c.var.di,具有相同的类型。
异步依赖
registerAsyncFactory() 将最终的 Database 类型而非 Promise<Database> 存入依赖图。其异步状态会传播到 UserService,因此这两个服务都通过 getAsync() 解析。虽然 getAsync() 也接受已就绪的同步键,但普通同步服务应使用 get()。
返回 Promise 的 registerFactory() 回调具有不同语义:Promise 本身就是服务值,在依赖图中仍为同步条目,并通过 get() 解析。
自定义上下文键
传入 key,即可通过另一个 Hono 上下文变量暴露同一作用域。自定义作用域工厂仍须提供已声明的请求输入。
type CustomEnv = InferdiHonoScopeEnv<RequestScope, 'container'>
const customKeyApp = new Hono<CustomEnv>()
customKeyApp.use(
'*',
inferdiHono({
container: root,
key: 'container',
createScope: (_root, c) =>
createRequestScope(c.req.header('x-user-id')),
})
)
customKeyApp.get('/users/:id', async (c) => {
const users = await c.var.container.getAsync('users')
return c.json(await users.profile(c.req.param('id')))
})选项
inferdiHono 接受以下选项:
| 选项 | 默认值 | 说明 |
|---|---|---|
container | 必填 | 根容器。中间件不会释放它。 |
key | 'di' | c.var[key] 和 c.get(key) 使用的上下文变量。 |
createScope | root.createScope() | 创建请求作用域。可用于从 Hono 传入带类型的输入,可以是异步函数。 |
setupScope | 无 | 在创建作用域之后、路由处理程序之前运行,可以是异步函数。 |
disposeScope | scope.dispose() | 覆盖请求作用域的释放行为,可以是异步函数。 |
autoDispose | true | 当应用代码负责释放时,设为 false 或返回 false。 |
onDisposeError | console.error | 处理请求作用域清理失败的情况。 |
流式传输
Hono 的 stream()、streamText() 和 streamSSE() 可以在回调结束前返回 Response。请在返回响应前调用 skipInferdiDispose(),并在流结束时释放作用域。
import { streamText } from 'hono/streaming'
import { skipInferdiDispose } from '@inferdi/hono'
app.get('/users/:id/export', (c) => {
skipInferdiDispose(c)
const scope = c.var.di
const id = c.req.param('id')
return streamText(c, async (stream) => {
try {
const users = await scope.getAsync('users')
const user = await users.profile(id)
await stream.write(JSON.stringify(user) ?? 'null')
} finally {
await scope.dispose()
}
})
})skipInferdiDispose() 仅在响应成功时禁用自动清理。如果路由在返回响应前失败,中间件仍会释放作用域。