Skip to content

全栈框架

全栈示例为 loader、action、路由处理函数以及 server action 使用作用域。开发构建会将根容器缓存在 globalThis 上,以避免在 HMR 期间产生重复的客户端实例。

这两个示例共用 examples/_shared/container.ts。请对比每个框架所等待的操作边界。

示例展示内容
next-app-router.tsNext.js App Router 的请求与 Server Action 作用域边界
remix.tsRemix 的 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()] })
}

仓库文件:examples/fullstack/remix.ts