前端框架
前端示例在页面、路由、屏幕或功能边界处创建作用域。它们各自保留小型的构建器,而不是导入服务端的共享模块。
请对比每个框架在何处创建作用域、如何将作用域提供给子组件,以及在卸载时如何执行清理。
| 示例 | 展示内容 |
|---|---|
react.tsx | React 功能作用域,配合惰性 useState 与清理 |
react-native.tsx | React Native 屏幕作用域 |
vue.ts | Vue 3 provide/inject 作用域边界 |
svelte.ts | Svelte context 作用域边界 |
React
tsx
import { Container } from '@inferdi/inferdi'
import {
createContext,
type PropsWithChildren,
useContext,
useEffect,
useState,
} from 'react'
// Frontend examples keep their own minimal builder because the browser does
// not have the `Database` / `process.env` shape that `_shared/container.ts`
// uses on the server. The patterns are the same: scoped `FeatureContext`,
// singleton services on the root, page-level scope on mount.
class FeatureContext {
featureName = ''
}
class ApiClient {
async listProjects() {
return [{ id: 'project_1', name: 'Docs' }]
}
}
class ProjectsViewModel {
constructor(
private readonly feature: FeatureContext,
private readonly api: ApiClient,
) {}
async load() {
return {
feature: this.feature.featureName,
projects: await this.api.listProjects(),
}
}
}
const root = new Container()
.registerClass('feature', FeatureContext, [], 'scoped')
.registerClass('api', ApiClient, [])
.registerClass('projectsVm', ProjectsViewModel, ['feature', 'api'], 'scoped')
type AppContainer = typeof root
type PageContainer = ReturnType<typeof createProjectsPageScope>
function createProjectsPageScope(parent: AppContainer) {
const scope = parent.createScope()
try {
scope.get('feature').featureName = 'projects'
return scope
} catch (error) {
scope.dispose().catch(console.error)
throw error
}
}
const RootDIContext = createContext<AppContainer | null>(null)
const PageDIContext = createContext<PageContainer | null>(null)
export function AppDIProvider({ children }: PropsWithChildren) {
return <RootDIContext.Provider value={root}>{children}</RootDIContext.Provider>
}
export function ProjectsPage({ children }: PropsWithChildren) {
const parent = useContext(RootDIContext)
if (parent === null) throw new Error('DI provider is missing')
// useState with a lazy initializer is the correct primitive for "create
// exactly once per mounted instance". `useMemo` is documented as an
// optimization, not a guarantee — under concurrent rendering React may
// re-run the factory at any time and the discarded scope would leak
// because `useEffect` cleanup only runs on the kept render.
const [scope] = useState(() => createProjectsPageScope(parent))
useEffect(() => {
// React cleanup is synchronous. Use async dispose explicitly so scopes
// with async factories/disposers do not go through sync [Symbol.dispose]().
return () => {
scope.dispose().catch(console.error)
}
}, [scope])
return <PageDIContext.Provider value={scope}>{children}</PageDIContext.Provider>
}
export function useProjectsViewModel() {
const container = useContext(PageDIContext)
if (container === null) throw new Error('Projects page scope is missing')
return container.get('projectsVm')
}仓库文件:examples/frontend/react.tsx
React Native
tsx
import { Container } from '@inferdi/inferdi'
import {
createContext,
type PropsWithChildren,
useContext,
useEffect,
useState,
} from 'react'
class ScreenContext {
screenName = ''
}
class DeviceStorage {
async getItem(key: string) {
return `value:${key}`
}
}
class SettingsViewModel {
constructor(
private readonly screen: ScreenContext,
private readonly storage: DeviceStorage,
) {}
async loadTheme() {
return {
screen: this.screen.screenName,
theme: await this.storage.getItem('theme'),
}
}
}
const root = new Container()
.registerClass('screen', ScreenContext, [], 'scoped')
.registerClass('storage', DeviceStorage, [])
.registerClass('settingsVm', SettingsViewModel, ['screen', 'storage'], 'scoped')
type AppContainer = typeof root
type ScreenContainer = ReturnType<typeof createSettingsScreenScope>
function createSettingsScreenScope(parent: AppContainer) {
const scope = parent.createScope()
try {
scope.get('screen').screenName = 'settings'
return scope
} catch (error) {
scope.dispose().catch(console.error)
throw error
}
}
const RootDIContext = createContext<AppContainer | null>(null)
const ScreenDIContext = createContext<ScreenContainer | null>(null)
export function AppDIProvider({ children }: PropsWithChildren) {
return <RootDIContext.Provider value={root}>{children}</RootDIContext.Provider>
}
export function SettingsScreenScope({ children }: PropsWithChildren) {
const parent = useContext(RootDIContext)
if (parent === null) throw new Error('DI provider is missing')
// useState with a lazy initializer (not useMemo) — see the React example
// for the rationale: useMemo is not a guarantee, lazy useState is.
const [scope] = useState(() => createSettingsScreenScope(parent))
useEffect(() => {
return () => {
scope.dispose().catch(console.error)
}
}, [scope])
return <ScreenDIContext.Provider value={scope}>{children}</ScreenDIContext.Provider>
}
export function useSettingsViewModel() {
const container = useContext(ScreenDIContext)
if (container === null) throw new Error('Settings screen scope is missing')
return container.get('settingsVm')
}仓库文件:examples/frontend/react-native.tsx
Vue
ts
import { Container } from '@inferdi/inferdi'
import {
defineComponent,
inject,
onUnmounted,
provide,
type InjectionKey,
} from 'vue'
class RouteContext {
routeName = ''
}
class ApiClient {
async dashboard() {
return { total: 42 }
}
}
class DashboardViewModel {
constructor(
private readonly route: RouteContext,
private readonly api: ApiClient,
) {}
async load() {
return {
route: this.route.routeName,
dashboard: await this.api.dashboard(),
}
}
}
const root = new Container()
.registerClass('route', RouteContext, [], 'scoped')
.registerClass('api', ApiClient, [])
.registerClass('dashboardVm', DashboardViewModel, ['route', 'api'], 'scoped')
type AppContainer = typeof root
type RouteContainer = ReturnType<typeof createDashboardRouteScope>
function createDashboardRouteScope(parent: AppContainer) {
const scope = parent.createScope()
try {
scope.get('route').routeName = 'dashboard'
return scope
} catch (error) {
scope.dispose().catch(console.error)
throw error
}
}
const RootDIKey: InjectionKey<AppContainer> = Symbol('InferDI.root')
const RouteDIKey: InjectionKey<RouteContainer> = Symbol('InferDI.route')
export const AppDIProvider = defineComponent({
setup(_props, { slots }) {
provide(RootDIKey, root)
return () => slots.default?.()
},
})
export const DashboardRouteProvider = defineComponent({
setup(_props, { slots }) {
const parent = inject(RootDIKey)
if (parent === undefined) throw new Error('DI provider is missing')
const scope = createDashboardRouteScope(parent)
provide(RouteDIKey, scope)
// Vue unmount hooks are synchronous; call async dispose explicitly.
onUnmounted(() => {
scope.dispose().catch(console.error)
})
return () => slots.default?.()
},
})
export function useDashboardViewModel() {
const container = inject(RouteDIKey)
if (container === undefined) throw new Error('Dashboard route scope is missing')
return container.get('dashboardVm')
}Svelte
ts
import { Container } from '@inferdi/inferdi'
import { getContext, onDestroy, setContext } from 'svelte'
class RouteContext {
routeName = ''
}
class ApiClient {
async loadInbox() {
return [{ id: 'msg_1', subject: 'Hello' }]
}
}
class InboxViewModel {
constructor(
private readonly route: RouteContext,
private readonly api: ApiClient,
) {}
async load() {
return {
route: this.route.routeName,
messages: await this.api.loadInbox(),
}
}
}
const root = new Container()
.registerClass('route', RouteContext, [], 'scoped')
.registerClass('api', ApiClient, [])
.registerClass('inboxVm', InboxViewModel, ['route', 'api'], 'scoped')
type AppContainer = typeof root
type RouteContainer = ReturnType<typeof createInboxRouteScope>
const RootDIKey = Symbol('InferDI.root')
const RouteDIKey = Symbol('InferDI.route')
function createInboxRouteScope(parent: AppContainer) {
const scope = parent.createScope()
try {
scope.get('route').routeName = 'inbox'
return scope
} catch (error) {
scope.dispose().catch(console.error)
throw error
}
}
export function provideRootContainer() {
setContext(RootDIKey, root)
}
export function provideInboxRouteScope() {
const parent = getContext<AppContainer | undefined>(RootDIKey)
if (parent === undefined) {
throw new Error('InferDI root context is missing — call provideRootContainer() in a parent layout')
}
const scope = createInboxRouteScope(parent)
setContext(RouteDIKey, scope)
// Svelte onDestroy is synchronous; start async cleanup and report failures.
onDestroy(() => {
scope.dispose().catch(console.error)
})
}
export function getInboxViewModel() {
const scope = getContext<RouteContainer | undefined>(RouteDIKey)
if (scope === undefined) {
throw new Error('Inbox route scope is missing — call provideInboxRouteScope() in this route')
}
return scope.get('inboxVm')
}