依赖注入真正起效的第一步是消灭new Service(),所有业务类必须由统一入口创建并注入,否则解耦只是幻觉;否则会导致实例混乱、测试困难、配置僵化及类型推导断裂等问题。

直接结论:在大型 SPA 工程中,依赖注入(DI)不是靠引入一个框架就自动生效的,真正起效的第一步是消灭 new Service() —— 所有业务类必须由统一入口创建并注入,否则所谓“解耦”只是幻觉。
为什么 new Service() 是解耦失败的起点
你在 Vue 组件里写 const api = new ApiClient(),或在 React 的 useEffect 里调用 new AuthManager(),看似无害,实则把服务生命周期、实例状态、环境配置全部钉死在 UI 层。一旦要切 mock、加请求拦截、做灰度路由、甚至只是改个 baseUrl,就得 grep 全项目、逐个替换、反复验证。
这类写法还会导致:
- 同一服务在不同组件中产生多个实例,
localStorage同步失效、token 状态不一致 - 单元测试时无法替换依赖,只能 patch 全局构造函数,极易污染其他测试用例
- TS 类型推导断裂——
ApiClient的泛型参数、拦截器链、重试策略等上下文信息全丢失
解决办法不是立刻上 Inversify 或 tsyringe,而是先用一个纯函数收口:
const createServices = (env: Env) => ({
api: new ApiClient({ baseUrl: env.API_URL, timeout: 10000 }),
auth: new AuthManager({ storage: env.STORAGE }),
logger: env.IS_DEV ? new ConsoleLogger() : new SentryLogger()
});
provide/inject 在 Vue 中的正确用法边界
Vue 的 provide/inject 不是 DI 容器,它只是响应式数据传递通道。常见错误是每次 setup 都 provide('api', new ApiClient()),结果每个组件实例都拿到新实例,单例契约彻底崩坏。
安全做法只有两条:
- 只
provide已经由工厂函数创建好的稳定对象,例如provide('services', createServices(env)) - 消费时用解构获取引用:
const { api } = inject('services'),而不是const api = inject('api')—— 后者语义模糊,且无法保证跨组件复用同一实例
额外提醒:InjectionKey 必须显式声明类型,否则 TS 无法推导 api 的方法签名,IDE 补全失效,重构 rename 会漏掉调用点。
React 中用 useContext + 工厂函数模拟 DI 容器
React 没有原生 provide/inject,但不需要第三方库也能实现同等效果。关键是把工厂函数返回的对象作为 Context value,且确保该对象在应用生命周期内不变(避免每次渲染重建)。
典型陷阱:
- 在组件内部定义
createServices()并传给MyContext.Provider—— 导致每次父组件 rerender,子组件收到全新 services 对象,所有依赖它的 hook 都会重新初始化 - Context value 使用
useMemo但未正确依赖 env 变量,造成环境切换后服务仍用旧配置
推荐写法:
const ServicesContext = createContext<ReturnType<typeof createServices>>(null!);
export function ServicesProvider({ children }: { children: ReactNode }) {
const services = useMemo(() => createServices(getEnv()), []); // ✅ 空依赖数组 + 纯函数
return <ServicesContext.Provider value={services}>{children}</ServicesContext.Provider>;
}
何时该引入 Inversify 或 tsyringe
当项目出现以下信号时,手动维护工厂函数的成本开始超过引入轻量 DI 库的收益:
- 服务之间存在循环依赖(A 依赖 B,B 依赖 C,C 又依赖 A),靠手写工厂难以理清初始化顺序
- 需要按模块动态注册服务(如插件系统),而非启动时一次性创建全部
- 不同环境(dev/staging/prod)需注入不同实现,且实现类本身也依赖其他服务(比如 mock api 依赖 mock storage)
此时 Inversify 的价值不是“更高级”,而是把“谁创建谁”这个决策从代码里抽离成配置:
container.bind<ApiClient>('ApiClient').to(ApiClient).inSingletonScope();
container.bind<AuthManager>('AuthManager').to(AuthManager).inSingletonScope();
// 切换环境只需改这一行
container.bind<Logger>('Logger').to(ConsoleLogger); // dev
// container.bind<Logger>('Logger').to(SentryLogger); // prod
注意:Inversify 不接管组件生命周期,也不侵入 React/Vue 渲染逻辑,它只管“实例怎么来”。只要你不把它和组件状态混用,就不会增加调试复杂度。
真正容易被忽略的是:DI 容器本身不能解决“接口抽象不清晰”的问题。如果 ApiClient 方法签名频繁变动、缺乏版本隔离、没有明确的 error 分类,再好的注入机制也救不了调用方的脆弱性。

















