Skip to content

推断DI

🌐 InferDI

InferDI 是一个零依赖、无装饰器、强类型的 TypeScript 依赖注入容器。@inferdi/hono 中间件将其接入 Hono 的请求管道:它为每个请求创建一个 DI 范围,在上下文中以 c.var.di 暴露,并在响应完成后销毁——无需装饰器、反射或路由扫描。

该图是这种类型:一个错误排序的依赖、缺失的键,或者请求作用域的值泄露到单例中,都是编译错误,而不是运行时意外。

🌐 The graph is the type: a misordered dependency, a missing key, or a request-scoped value leaking into a singleton are all compile errors, not runtime surprises.

🛠️ 安装

🌐 🛠️ Installation

bash
npm install @inferdi/inferdi @inferdi/hono

NOTE

InferDI 可在 npm 和 JSR 上发布。在 Deno 上使用 deno add jsr:@inferdi/inferdi jsr:@inferdi/hono npm:hono 安装。

🚀 入门

🌐 🚀 Getting Started

1. 建立一个容器

🌐 1. Build a container

在根 Container 上注册你的服务。依赖作为键的元组传递,并根据构造函数按位置进行类型检查——顺序或类型错误将导致编译错误。每个注册都会声明一个生命周期:singleton(默认,每个容器一个实例)、scoped(每个请求一个实例)或 transient(每次解析时创建新实例)。

🌐 Register your services on a root Container. Dependencies are passed as a tuple of keys and type-checked positionally against the constructor — a wrong order or type is a compile error. Each registration declares a lifetime: singleton (default, one instance per container), scoped (one per request), or transient (new on every resolve).

ts
// container.ts
import { Container } from '@inferdi/inferdi'

export function buildRootContainer() {
  return (
    new Container()
      .registerClass('logger', Logger, [])
      // `request` is scoped: a fresh instance per request scope.
      .registerClass('request', RequestContext, [], 'scoped')
      // `users` is scoped too — it depends on the scoped `request`.
      .registerClass(
        'users',
        UserService,
        ['logger', 'request'],
        'scoped'
      )
  )
}

NOTE

一个 singleton 不能依赖于 scopedtransient 服务——那会把一个短期存在的值泄露到长期存在的对象中,而 InferDI 会在编译时拒绝它。请保持请求绑定的服务 scoped

2. 添加中间件

🌐 2. Add the middleware

inferdiHono 在你的处理程序运行之前创建一个请求范围,并在之后处理它。InferdiHonoEnv<typeof root>c.var.di 类型化为你的具体范围,因此 .get(key) 保持完全类型化。

ts
import { Hono } from 'hono'
import { inferdiHono, type InferdiHonoEnv } from '@inferdi/hono'
import { buildRootContainer } from './container'

const root = buildRootContainer()
const app = new Hono<InferdiHonoEnv<typeof root>>()

app.use('*', inferdiHono({ container: root }))

export default app

3. 填充请求范围

🌐 3. Hydrate the request scope

在任何处理程序看到作用域之前,使用 setupScope 用每次请求的数据(请求 ID、已认证用户等)填充请求范围的服务。它每个请求运行一次,并且可以是异步的。

🌐 Use setupScope to fill request-scoped services with per-request data (request id, authenticated user, …) before any handler sees the scope. It runs once per request and may be async.

ts
app.use(
  '*',
  inferdiHono({
    container: root,
    setupScope: (scope, c) => {
      const request = scope.get('request')
      request.requestId = crypto.randomUUID()
      request.userId = c.req.header('x-user-id')
    },
  })
)

4. 在处理程序中解析服务

🌐 4. Resolve services in handlers

使用 c.var.di.get(key) 从请求范围解析任何已注册的服务。返回的值是完全类型化的,并且作用域服务在整个请求中共享一个实例。

🌐 Resolve any registered service from the request scope with c.var.di.get(key). The returned value is fully typed, and scoped services share one instance for the whole request.

ts
app.get('/users/:id', async (c) => {
  const user = await c.var.di.get('users').profile(c.req.param('id'))
  return c.json(user)
})

c.get('di') 等同于 c.var.di。要使用不同的上下文键,请传递 key 并在环境类型中反映它:

ts
type AppEnv = InferdiHonoEnv<typeof root, 'container'>

const app = new Hono<AppEnv>()
app.use('*', inferdiHono({ container: root, key: 'container' }))

app.get('/users/:id', (c) =>
  c.json(c.var.container.get('users').profile(c.req.param('id')))
)

⚙️ 选项

🌐 ⚙️ Options

inferdiHono 接受以下选项:

选项默认值描述
container必填。 根容器。中间件从不处理根的释放。
key'di'用于 c.var[key] / c.get(key) 的上下文变量键。
createScoperoot.createScope()覆盖请求作用域的创建方式。可能是异步的。
setupScope在处理程序运行前初始化作用域。可能是异步的。
disposeScopescope.dispose()覆盖请求作用域的释放方式。可能是异步的。
autoDisposetrue当应用代码负责释放时,设置为 false(或返回 false)。
onDisposeErrorconsole.error响应后释放失败的收集入口。

🌊 流媒体

🌐 🌊 Streaming

流式响应会在流回调完成之前返回,因此请使用 skipInferdiDispose(c) 禁用自动释放,并在流结束时自行释放作用域。

🌐 A streaming response returns before the stream callback finishes, so disable auto-disposal with skipInferdiDispose(c) and dispose the scope yourself when the stream ends.

ts
import { stream } from 'hono/streaming'
import { skipInferdiDispose } from '@inferdi/hono'

app.get('/events', (c) => {
  skipInferdiDispose(c)
  const scope = c.var.di
  const events = scope.get('events')

  return stream(c, async (s) => {
    try {
      for await (const event of events.subscribe()) {
        await s.write(`data: ${JSON.stringify(event)}\n\n`)
      }
    } finally {
      await scope.dispose()
    }
  })
})

另请参阅

🌐 See also

Hono 中文网 - 粤ICP备13048890号