全栈框架
全栈示例为 loader、action、路由处理函数以及 server action 使用作用域。开发构建会将根容器缓存在 globalThis 上,以避免在 HMR 期间产生重复的客户端实例。
这两个示例共用 examples/_shared/container.ts。请对比每个框架所等待的操作边界。
| 示例 | 展示内容 |
|---|---|
next-app-router.ts | Next.js App Router 的请求与 Server Action 作用域边界 |
remix.ts | Remix 的 loader 与 action 作用域边界 |
Next.js App Router
ts
'use server'
import { revalidatePath } from 'next/cache'
import { buildRootContainer, type RootContainer } from '../_shared/container.js'
// In development, HMR can reload this module without resetting globalThis.
// Reusing the root avoids leaking DB/cache clients on every file save and
// guarantees that across reloads `buildRootContainer()` runs ONCE, so the
// async `Database` factory in `_shared/container.ts` is initialized once.
const globalForInferDI = globalThis as typeof globalThis & {
__inferdiNextRoot?: RootContainer
}
export const root: RootContainer =
process.env.NODE_ENV === 'production'
? buildRootContainer()
: (globalForInferDI.__inferdiNextRoot ??= buildRootContainer())
// Server Actions run as a single bounded async unit and Next awaits the
// function before returning to the client. `await using` is the right tool
// here: the scope is asyncDispose'd exactly when the action finishes,
// including on thrown errors.
export async function getProfileAction(formData: FormData) {
await using scope = root.createScope()
const ctx = scope.get('request')
ctx.requestId = crypto.randomUUID()
ctx.userId = String(formData.get('userId') ?? '') || undefined
const profile = await scope.get('users').profile(ctx.userId ?? 'me')
revalidatePath('/profile')
return profile
}仓库文件:examples/fullstack/next-app-router.ts
Remix
ts
import type {
ActionFunctionArgs,
LoaderFunctionArgs,
} from '@remix-run/node'
import { json } from '@remix-run/node'
import {
buildRootContainer,
createRequestScope,
type RootContainer,
} from '../_shared/container.js'
// Remix dev can re-evaluate modules during HMR while globalThis survives.
// Cache the root so the async Database factory is not re-run on every save.
const globalForInferDI = globalThis as typeof globalThis & {
__inferdiRemixRoot?: RootContainer
}
export const root: RootContainer =
process.env.NODE_ENV === 'production'
? buildRootContainer()
: (globalForInferDI.__inferdiRemixRoot ??= buildRootContainer())
async function scopeFor(request: Request) {
return createRequestScope(root, {
requestId: request.headers.get('x-request-id') ?? crypto.randomUUID(),
userId: request.headers.get('x-user-id') ?? undefined,
})
}
export async function loader({ request }: LoaderFunctionArgs) {
// Loader / action functions are bounded async operations that Remix awaits
// before serializing the response. `await using` ties scope disposal to
// the same boundary — Remix never streams data out of a loader after it
// returns, so the scope is safe to tear down here.
await using scope = await scopeFor(request)
return json(await scope.get('users').profile('me'))
}
export async function action({ request }: ActionFunctionArgs) {
await using scope = await scopeFor(request)
const form = await request.formData()
return json({ ok: true, fields: [...form.keys()] })
}