跳转到正文

InferDI ​

InferDI 是 TypeScript 的依赖注入容器,不使用装饰器、反射或运行时依赖。@inferdi/hono 中间件会在每次调用时创建请求作用域,通过 c.var.di 暴露该作用域,并在路由处理链结束后释放它。

容器的类型包含其依赖图。TypeScript 会报告缺失或顺序错误的依赖,以及无效的生命周期关系。

安装 ​

bash
npm install @inferdi/inferdi @inferdi/hono

NOTE

InferDI 也发布在 JSR 上。使用 Deno 时,运行 deno add jsr:@inferdi/inferdi jsr:@inferdi/hono npm:hono。

入门 ​

1. 构建容器 ​

请求数据应位于 HTTP 边界。定义一个小型的应用类型,而不是将 Hono 的 Context 放入依赖图。

ts
// 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. 创建请求作用域 ​

使用函数创建应用所需的具体作用域:

ts
const openRequestScope = (request: RequestContext) =>
  root.createScope({ request })

type RequestScope = ReturnType<typeof openRequestScope>

3. 添加中间件 ​

InferdiHonoScopeEnv 通过 Hono 上下文变量暴露具体的、已就绪的 RequestScope。

ts
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,因此是异步的。

ts
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 app

c.get('di') 等同于 c.var.di,具有相同的类型。

异步依赖 ​

registerAsyncFactory() 将最终的 Database 类型而非 Promise<Database> 存入依赖图。其异步状态会传播到 UserService,因此这两个服务都通过 getAsync() 解析。虽然 getAsync() 也接受已就绪的同步键,但普通同步服务应使用 get()。

返回 Promise 的 registerFactory() 回调具有不同语义:Promise 本身就是服务值,在依赖图中仍为同步条目,并通过 get() 解析。

自定义上下文键 ​

传入 key,即可通过另一个 Hono 上下文变量暴露同一作用域。自定义作用域工厂仍须提供已声明的请求输入。

ts
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) 使用的上下文变量。
createScoperoot.createScope()创建请求作用域。可用于从 Hono 传入带类型的输入,可以是异步函数。
setupScope无在创建作用域之后、路由处理程序之前运行,可以是异步函数。
disposeScopescope.dispose()覆盖请求作用域的释放行为,可以是异步函数。
autoDisposetrue当应用代码负责释放时,设为 false 或返回 false。
onDisposeErrorconsole.error处理请求作用域清理失败的情况。

流式传输 ​

Hono 的 stream()、streamText() 和 streamSSE() 可以在回调结束前返回 Response。请在返回响应前调用 skipInferdiDispose(),并在流结束时释放作用域。

ts
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() 仅在响应成功时禁用自动清理。如果路由在返回响应前失败,中间件仍会释放作用域。

另请参阅 ​

基于 MIT 许可证发布。