new Service() 是解耦的第一道坎,因其将实例创建和销毁逻辑耦合进组件,导致测试困难、维护成本高、内存泄漏及单例失效;真正解耦需将生命周期管理权交由外部工厂统一控制。

直接用 new Service() 创建依赖,业务逻辑就和框架生命周期绑死了;真解耦不是换 DI 库,而是把“谁创建、何时销毁”从组件里彻底移出去。
为什么 new Service() 是解耦的第一道坎
大型 SPA 里满屏 new ApiClient()、new AuthManager()、new Logger(),看着干净,实则每个 new 都在组件内部埋下耦合炸弹:
- 测试时无法替换为 mock 实例,只能 patch 全局构造函数或改源码
- 要加请求拦截、日志上下文、重试策略?得改所有 new 点,漏一个就出问题
- 组件卸载后实例还挂着(比如未取消的定时器、事件监听),引发内存泄漏
- Vue/React 组件重渲染不等于服务该重建——但
new每次都新建,违背单例语义
provide/inject 不是 DI,只是传递通道
Vue 的 provide/inject 常被误当 DI 工具,但它不管理实例生命周期,也不保证单例。常见错误写法:
-
provide('api', new ApiClient())→ 每次 setup 都新建,不同组件拿到的是不同实例 -
const api = inject('api')→ key 名模糊,类型丢失,TS 推导失效,重构时 rename 会静默失败
安全用法只有一条:先用工厂函数统一创建,再 provide 整个服务对象:
const services = createServices(); // 工厂返回稳定实例
provide('services', services); // ✅ 提供对象,非单个实例
消费端也必须解构自同一对象,确保引用一致:
const { api, logger } = inject('services'); // ✅ 同一引用
TS + InjectionKey 是类型安全的底线
不用 InjectionKey,Vue 的 inject 返回 any,IDE 补全失效,TS 编译器不校验字段名拼写。正确姿势:
- 为每个服务契约定义显式
InjectionKey:
import { InjectionKey } from 'vue';
export const ApiKey: InjectionKey<ApiClient> = Symbol('ApiClient');
- 提供时用 key 注册:
provide(ApiKey, services.api); // ✅ 类型受控
- 消费时用 key 获取,自动获得类型推导:
const api = inject(ApiKey); // ✅ api 类型为 ApiClient,字段补全可用
这一步不做,后续所有“解耦”都是裸奔——类型系统形同虚设,重构风险陡增。
跨框架复用的关键:依赖契约必须是接口,不是类
想让同一套业务逻辑跑在 Vue、React 甚至 SSR 环境,依赖不能绑定具体实现类。例如:
- ❌ 错误:依赖
class ConsoleLogger→ React 里没法直接用 - ✅ 正确:定义
interface ILogger,各框架提供对应实现(ConsoleLogger/ReactNativeLogger/SSRNoopLogger)
工厂函数按环境切换实现:
const createServices = () => ({
logger: import.meta.env.SSR ? new SSRNoopLogger() : new ConsoleLogger(),
api: new ApiClient({ baseUrl: import.meta.env.VITE_API_URL })
});
业务代码只依赖 ILogger 接口,完全感知不到底层是哪个类——这才是跨框架可复用的起点。
真正难的从来不是“怎么注入”,而是谁来决定“这个实例该活多久”“它该响应哪些生命周期事件”“它的依赖是否已就绪”。这些决策一旦散落在组件内部,解耦就只剩名词游戏。

















