# @kaokei/use-vue-service — 完整 LLM 参考文档 > Lightweight Vue 3 state management with dependency injection, inspired by Angular services. 本库基于 [@kaokei/di](https://github.com/kaokei/di) 开发,通过依赖注入实现面向服务编程和领域驱动开发,可以代替 vuex/pinia。通过类来声明服务,对 TypeScript 支持非常好。 - 版本:4.0.5 - 许可证:MIT - GitHub:https://github.com/kaokei/use-vue-service - npm:https://www.npmjs.com/package/@kaokei/use-vue-service - 文档站点:https://use-vue-service.kaokei.com - 依赖:@kaokei/di ^5.0.9(peerDependency) --- ## 安装 ```sh npm install @kaokei/di @kaokei/use-vue-service ``` 不依赖 reflect-metadata。使用 TC39 Stage 3 标准装饰器语法,TypeScript 5.0+ 默认支持,无需在 tsconfig.json 中配置 experimentalDecorators。 --- ## 核心概念 本库将 Angular 的"服务 + 依赖注入"模式引入 Vue 3: 1. 用 class 定义服务,服务中可以通过 `@Inject` 装饰器声明对其他服务的依赖 2. 在 Vue 组件的 setup 中调用 `declareProviders` 将服务绑定到当前组件的 DI 容器 3. 调用 `useService` 获取服务实例,实例会自动被 `reactive()` 包裹,天然具备 Vue 响应式能力 4. 子组件可以自动继承父组件声明的服务,也可以覆盖或新增服务绑定 5. 组件卸载时,容器自动销毁,清理资源 本库采用 **opt-out** 响应式策略:服务实例的所有属性默认都是响应式的,只有需要排除的属性才使用 `@Raw` 标记。这与 Pinia 等需要显式声明响应式字段的 opt-in 策略相反。 --- ## 导出总览 ```ts // 从 @kaokei/di 重新导出的所有 API export * from '@kaokei/di'; // 核心 API export { useService, declareProviders } from './core'; export { useRootService, declareRootProviders } from './core'; export { useAppService, declareAppProviders, declareAppProvidersPlugin } from './core'; // Token 常量 export { FIND_CHILD_SERVICE, FIND_CHILDREN_SERVICES } from './constants'; // 类型 export type { FindChildService, FindChildrenServices } from './interface'; // 装饰器 export { Computed } from './computed'; export { Raw } from './raw'; export { RunInScope } from './effect-scope'; export { autobind } from './autobind'; ``` --- ## 类型定义 ```ts import type { Container, Newable, CommonToken } from '@kaokei/di'; // 服务提供者类型 type NewableProvider = Newable[]; type FunctionProvider = (container: Container) => void; type Provider = NewableProvider | FunctionProvider; // 子组件服务查找函数类型 type FindChildService = (token: CommonToken) => T | undefined; type FindChildrenServices = (token: CommonToken) => T[]; ``` - `NewableProvider`:类数组形式。将每个类以 `toSelf()` 的方式绑定到容器,即类本身既是 token 也是实现。 - `FunctionProvider`:函数形式。接收容器实例作为参数,允许自由调用容器的绑定 API(如 `container.bind(token).toConstantValue()`、`container.bind(token).toDynamicValue()` 等)。 - `Provider`:`NewableProvider | FunctionProvider` 的联合类型。 --- ## 三组核心 API 三组 API 背后的容器存在继承关系:**根容器 → App 容器 → 组件容器**。容器查找遵循就近原则,并沿层级向上回退: - `useService` — 从当前组件容器开始,沿组件树向上查找,最终回退到 App 容器,再回退到根容器。能读取三组 API 声明的所有服务。 - `useAppService` — 从 App 容器开始,可回退到根容器。能读取 `declareAppProviders` 和 `declareRootProviders` 声明的服务,无法读取组件内 `declareProviders` 声明的服务。 - `useRootService` — 直接操作根容器,只能读取 `declareRootProviders` 声明的服务。 **组件内应统一使用 `useService`**:`useService` 的查找链覆盖全部三层容器,组件内无需区分服务是通过哪组 `declare*` 声明的,不要因为服务是通过 `declareRootProviders` 声明的就在组件内改用 `useRootService`。`useRootService` 和 `useAppService` 主要用于组件树之外(如 `main.ts`、工具函数等)。 ### 第一组:组件级作用域 — useService / declareProviders 容器通过 Vue 的 provide/inject 机制在组件树中传递。这是最常用的方式。 #### declareProviders ```ts function declareProviders(providers: NewableProvider): void; function declareProviders(providers: FunctionProvider): void; ``` 在当前组件中声明服务提供者。必须在 setup 中调用。 行为逻辑: - 如果当前组件已经声明过容器(重复调用),则直接在已有容器上追加绑定 - 如果当前组件尚未声明容器,则: 1. 获取父级容器作为 parent 2. 创建子容器(继承父级容器的所有绑定) 3. 在子容器上绑定新的服务 4. 通过 Vue 的 provide 将子容器注入组件树 5. 组件卸载时(onUnmounted)自动销毁子容器 伪代码: ```ts const container = new Container(); provide(CONTAINER_TOKEN, container); container.bind(ClassName).toSelf(); ``` #### useService ```ts function useService(token: CommonToken): T; ``` 从当前组件或最近的祖先组件的 DI 容器中获取服务实例。必须在 setup 中调用。 查找顺序: 1. 先检查当前组件自身是否声明了容器 2. 如果没有,则沿组件树向上查找最近的祖先组件的容器(含 App 容器) 3. 如果整个组件树中都没有提供容器,则回退到全局的根容器 伪代码: ```ts const container = inject(CONTAINER_TOKEN); return container.get(token); ``` ### 第二组:全局根级作用域 — useRootService / declareRootProviders 直接操作全局的根容器,不依赖 Vue 组件树,可以在任何地方调用(包括 main.ts、工具函数等)。 #### declareRootProviders ```ts function declareRootProviders(providers: NewableProvider): void; function declareRootProviders(providers: FunctionProvider): void; ``` 在全局根容器上声明服务提供者,绑定的服务在整个应用中全局共享。 #### useRootService ```ts function useRootService(token: CommonToken): T; ``` 从全局根容器中获取服务实例。只能读取 `declareRootProviders` 声明的服务,不会查找 App 容器或组件容器。 **注意**:`useRootService` 并非 `declareRootProviders` 在组件内的专属配对 API。`useService` 的查找链最终也会回退到根容器,**组件内应统一使用 `useService`**。只有极少数情况才需要在组件内使用 `useRootService`:同一个 token 被 `declareProviders`/`declareAppProviders`/`declareRootProviders` 多次绑定,`useService` 因就近原则无法取到根容器中的实例时,才需要明确指定从根容器获取。 ### 第三组:App 级作用域 — useAppService / declareAppProviders / declareAppProvidersPlugin 通过 `app.runWithContext` 在指定 Vue App 实例的上下文中操作容器,适用于多 App 实例场景,每个 App 拥有独立的容器。 #### declareAppProviders ```ts function declareAppProviders(providers: NewableProvider, app: App): void; function declareAppProviders(providers: FunctionProvider, app: App): void; ``` 在指定 App 实例的上下文中声明服务提供者。 **注意**:需要显式传入 `app` 实例作为第二个参数,实践中几乎没有场景会把 `app` 传递到组件内部,因此此 API 基本不会在组件 setup 中调用。推荐在 `main.ts` 中使用,或改用 `declareAppProvidersPlugin` 以 Vue 插件形式注册。 行为逻辑: - 如果该 App 已经有容器,则直接追加绑定 - 如果该 App 尚未有容器,则以全局根容器为 parent 创建子容器,绑定服务,通过 `app.provide` 注入。App 卸载时自动销毁容器 伪代码: ```ts app.runWithContext(() => { const container = new Container(); app.provide(CONTAINER_TOKEN, container); container.bind(ClassName).toSelf(); }); ``` #### useAppService ```ts function useAppService(token: CommonToken, app: App): T; ``` 在指定 App 实例的上下文中获取服务实例。查找范围从 App 容器开始,找不到时可回退到全局根容器,无法读取组件内通过 `declareProviders` 声明的服务。 **注意**:需要显式传入 `app` 实例作为第二个参数,实践中几乎没有场景会把 `app` 传递到组件内部,因此此 API 基本不会在组件 setup 中调用。组件内请使用 `useService` 代替,它的查找链同样能覆盖 App 容器和根容器。 #### declareAppProvidersPlugin ```ts function declareAppProvidersPlugin(providers: Provider): (app: App) => void; ``` 返回一个 Vue 插件函数,可直接用于 `app.use()`。 ```ts const app = createApp(App); app.use(declareAppProvidersPlugin([ServiceA, ServiceB])); app.mount('#app'); ``` --- ## 装饰器 ### @Computed 将 getter 属性转换为 Vue `computed` 响应式计算属性。支持 `@Computed` 和 `@Computed()` 两种用法,效果完全一致。 ```ts // 用法一:不带括号 @Computed public get fullName() { return this.firstName + this.lastName; } // 用法二:带括号 @Computed() public get fullName() { return this.firstName + this.lastName; } ``` 功能说明: - 对 getter 属性进行性能优化,只有依赖变化时才重新执行 - 采用懒创建策略:首次在 reactive 代理上访问时才创建 ComputedRef,创建后在原始实例上定义同名数据属性存储 ComputedRef,后续由 reactive 的 Auto_Unwrap 机制自动解包 - 支持 writable computed:如果原型链上存在同名 setter,自动创建可写的 `computed({ get, set })` 只读 computed 示例: ```ts import { Computed } from '@kaokei/use-vue-service'; class CountService { public count = 1; @Computed public get doubleCount() { return this.count * 2; } } ``` Writable computed 示例: ```ts import { Computed } from '@kaokei/use-vue-service'; class UserService { public firstName = '张'; public lastName = '三'; @Computed public get fullName() { return this.firstName + this.lastName; } public set fullName(val: string) { this.firstName = val.slice(0, 1); this.lastName = val.slice(1); } } ``` ### @Raw 标记属性或整个类不参与 Vue 响应式追踪。支持 `@Raw` 和 `@Raw()` 两种调用形式,效果完全一致。支持三种装饰目标。 本库采用 **opt-out** 响应式策略:服务实例的所有属性默认都是响应式的,只有需要排除的属性才使用 `@Raw` 标记。 **场景一:field 装饰器** 装饰普通类字段,无论初始值还是后续赋值都会自动调用 `markRaw`,确保该字段的值永远不被 Vue 响应式系统代理。 ```ts // 不带括号 @Raw public chartInstance = {}; // 带括号 @Raw() public chartInstance = {}; ``` 示例: ```ts import { Raw } from '@kaokei/use-vue-service'; class MapService { @Raw public mapInstance: any = null; public zoom = 10; public initMap(el: HTMLElement) { this.mapInstance = new SomeMapSDK(el); // 自动 markRaw } } ``` **场景二:accessor 装饰器** 装饰 `accessor` 关键字声明的自动访问器字段,读取和写入都经由原始对象(`toRaw`),确保值不被响应式系统代理。 ```ts // 不带括号 @Raw accessor chartInstance = {}; // 带括号 @Raw() accessor chartInstance = {}; ``` 示例: ```ts import { Raw } from '@kaokei/use-vue-service'; class ChartService { @Raw accessor chartInstance: any = null; @Raw() accessor editorInstance: any = null; public initChart(el: HTMLElement) { this.chartInstance = new ECharts(el); this.editorInstance = new MonacoEditor(el); } } ``` **场景三:class 装饰器** 装饰整个类,该类的实例在激活时不会被 `reactive()` 包裹,整个实例保持原始对象状态,完全脱离 Vue 响应式系统。 ```ts // 不带括号 @Raw class RawService { public data = {}; } // 带括号 @Raw() class RawService { public data = {}; } ``` 示例: ```ts import { Raw } from '@kaokei/use-vue-service'; @Raw class ConfigService { public apiUrl = 'https://api.example.com'; public timeout = 5000; } ``` 适用场景:复杂的第三方 SDK 对象(如 ECharts 实例、Monaco Editor 实例等),避免转为响应式导致的性能问题或功能异常。 ### @RunInScope 在 Vue 的 `EffectScope` 中运行方法,自动管理副作用生命周期。支持 `@RunInScope` 和 `@RunInScope()` 两种用法,效果完全一致。 ```ts // 用法一:不带括号 @RunInScope public setup() { watchEffect(() => { /* ... */ }); } // 用法二:带括号 @RunInScope() public setup() { watchEffect(() => { /* ... */ }); } ``` 注意:`@RunInScope` **不会自动调用**被装饰的方法,需要用户主动调用才会生效。这与 `@PostConstruct` 不同。 每次调用被装饰方法时: 1. 获取或创建实例的 Root_Scope(每个实例最多一个) 2. 在 Root_Scope 内创建新的 Child_Scope 3. 在 Child_Scope 中执行原始方法体 4. 返回 Child_Scope 给调用者 实例销毁时,Root_Scope 自动清理,其下所有 Child_Scope 中的副作用一并销毁。**绝大多数场景下不需要手动管理返回的 EffectScope**,只有确实需要提前停止某次调用产生的副作用时,才需要保留返回值。 常规用法(不关心返回值): ```ts import { watchEffect } from 'vue'; import { RunInScope } from '@kaokei/use-vue-service'; class DemoService { public count = 0; @RunInScope public setup() { watchEffect(() => { console.log('count 变化了:', this.count); }); } } // 主动调用后,watchEffect 才开始运行 demoService.setup(); ``` 需要手动停止副作用时,方式一(推荐):在方法签名上显式声明返回 `EffectScope`: ```ts import type { EffectScope } from 'vue'; import { watchEffect } from 'vue'; import { RunInScope } from '@kaokei/use-vue-service'; class DemoService { @RunInScope public startWatch(): EffectScope { watchEffect(() => { /* ... */ }); return null as unknown as EffectScope; // 占位,实际返回值由装饰器接管 } } const scope = demoService.startWatch(); scope.stop(); ``` 方式二:在调用侧强制转换类型: ```ts import type { EffectScope } from 'vue'; const scope = demoService.startWatch() as unknown as EffectScope; scope.stop(); ``` > 注意:TypeScript 装饰器目前无法自动修改返回类型,这是语言层面的限制。 ### @autobind 方法装饰器,将方法绑定到 `reactive(this)` 上(Vue 响应式兼容版),确保方法被解构或作为回调传递时 `this` 不会丢失。 ```ts // 仅支持无括号调用 @autobind public handleClick() { /* this 始终指向 reactive proxy */ } ``` **关键特性:** - 兼容 `@Raw` 装饰器:检测 `context.metadata[RAW_CLASS_KEY]`,在 `@Raw` 类中回退为普通 `bind(this)` - 不依赖 `@Injectable` 或 `@Inject` - 不支持 `decorate()` 函数(内部使用 `addInitializer`) **使用场景:** Vue SFC 模板 `@click="service.method"` 不需要此装饰器(编译器自动处理);JS 中解构方法、`setTimeout` 回调、`Promise.then` 回调等场景需要。 ```ts import { autobind } from '@kaokei/use-vue-service'; class CountdownService { public second = 0; @autobind tick(): void { this.second--; if (this.second > 0) { setTimeout(this.tick, 1000); // 直接方法引用,this 自动正确 } } } ``` --- ## Token 常量 ### FIND_CHILD_SERVICE `FIND_CHILD_SERVICE` 是一个 `Token` 实例。通过 `useService` 获取一个工具函数,调用该函数可以在**子孙容器树**中查找绑定了指定 token 的第一个服务实例,找不到则返回 `undefined`。 ```ts import { useService, FIND_CHILD_SERVICE } from '@kaokei/use-vue-service'; const findChild = useService(FIND_CHILD_SERVICE); const childService = findChild(SomeService); // T | undefined ``` 也可以在服务中通过 `@Inject` 使用: ```ts class DemoService { @Inject(FIND_CHILD_SERVICE) public findService!: FindChildService; public handleClick() { const child = this.findService(ChildService); child?.doSomething(); } } ``` **查找原理(重要):** 查找的底层是**容器树**,而非组件树。只有调用过 `declareProviders` 的组件才会创建并持有容器。 `FIND_CHILD_SERVICE` 通过 `toDynamicValue` 绑定在容器上,工厂函数中的 `context.container` 就是该绑定所在的容器。因此 `findService(Token)` 的查找起点是**持有该容器的节点的子容器树**,而不一定是调用 `useService(FIND_CHILD_SERVICE)` 的那个组件。 具体来说,如果当前组件没有绑定容器,`useService` 会向上找到某个祖先组件的容器,`findService` 的查找起点就是那个**祖先组件的容器**。这意味着兄弟组件的子容器也可能进入查找范围: ``` ParentComp (绑定了容器 A) └── CurrentComp (未绑定容器,useService 向上找到容器 A) ├── ChildComp1 (绑定了容器 B,持有 ChildService1) └── ChildComp2 (绑定了容器 C,持有 ChildService2) └── SiblingComp (绑定了容器 D,持有 SiblingService) ``` 上例中 `findService` 的查找起点是容器 A,容器 B、C、D 都在查找范围内。如果要确保查找起点就是当前组件,可以调用 `declareProviders([])` 绑定一个空容器。 ### FIND_CHILDREN_SERVICES `FIND_CHILDREN_SERVICES` 是一个 `Token` 实例。功能与 `FIND_CHILD_SERVICE` 相同,区别在于返回子孙容器树中绑定了指定 token 的**所有**服务实例组成的数组,而非仅第一个。 ```ts import { useService, FIND_CHILDREN_SERVICES } from '@kaokei/use-vue-service'; const findAll = useService(FIND_CHILDREN_SERVICES); const allServices = findAll(SomeService); // T[] ``` 也可以在服务中通过 `@Inject` 使用: ```ts class DemoService { @Inject(FIND_CHILDREN_SERVICES) public findAllService!: FindChildrenServices; public handleClick() { const children = this.findAllService(ChildService); children.forEach(s => s.doSomething()); } } ``` 查找原理与 `FIND_CHILD_SERVICE` 完全一致,同样受容器树起点的影响。 --- ## 内部机制 ### 容器创建与响应式 每个容器在创建时会注册两个全局钩子: - `onActivation`:当服务实例被创建时,自动调用 `reactive()` 将其转为响应式对象(`@Raw` 装饰的类除外) - `onDeactivation`:当容器销毁时,自动清理实例上的 EffectScope ```ts container.onActivation((context, obj) => isObject(obj) ? reactive(obj) : obj); container.onDeactivation((obj) => removeScope(obj)); ``` ### EffectScope 管理 每个服务实例通过 Symbol key 挂载唯一的 EffectScope。`@Computed` 和 `@RunInScope` 装饰器都使用这个 scope 来收集响应式副作用。实例销毁时 scope 自动 stop,清理所有副作用。 ### 子组件服务查找 `FIND_CHILD_SERVICE` 和 `FIND_CHILDREN_SERVICES` 通过遍历容器的子容器树来查找服务。容器之间通过 parent-children 关系形成树状结构。查找起点是绑定这两个 token 的容器所在节点,而非调用 `useService` 的组件节点。 --- ## 完整使用示例 ### 基本用法 ```ts // service.ts import { Inject } from '@kaokei/use-vue-service'; export class LoggerService { public log(...msg: any[]) { console.log('from logger ==>', ...msg); } } export class CountService { public count = 0; @Inject(LoggerService) public accessor logger: LoggerService; public addOne() { this.count++; this.logger.log('addOne ==>', this.count); } } ``` ```vue ``` ### 三层级服务作用域 ```ts import { createApp } from 'vue'; import { declareRootProviders, useRootService, declareAppProvidersPlugin, useAppService, declareProviders, useService, } from '@kaokei/use-vue-service'; // 全局根级(任意位置调用) declareRootProviders([GlobalConfigService]); const config = useRootService(GlobalConfigService); // App 级(main.ts) const app = createApp(App); app.use(declareAppProvidersPlugin([AppService])); app.mount('#app'); // 组件级(在 setup 中) declareProviders([ComponentService]); const service = useService(ComponentService); ``` ### 使用 Token 和自定义绑定 ```ts import { Token, declareProviders, useService } from '@kaokei/use-vue-service'; const API_URL = new Token('API_URL'); declareProviders((container) => { container.bind(API_URL).toConstantValue('https://api.example.com'); container.bind(UserService).toSelf(); }); const apiUrl = useService(API_URL); const userService = useService(UserService); ``` ### @PostConstruct 生命周期 ```ts import { PostConstruct } from '@kaokei/use-vue-service'; class DataService { public data: any[] = []; @PostConstruct public init() { // 实例化后自动执行,适合做初始化逻辑 this.data = [1, 2, 3]; } } ``` ### LazyToken 解决循环依赖 ```ts import { Inject, LazyToken } from '@kaokei/use-vue-service'; class ServiceA { @Inject(new LazyToken(() => ServiceB)) accessor serviceB: ServiceB; } class ServiceB { @Inject(new LazyToken(() => ServiceA)) accessor serviceA: ServiceA; } ``` ### @Raw class 用法 ```ts import { Raw } from '@kaokei/use-vue-service'; // 整个实例不被 reactive() 包裹,完全脱离 Vue 响应式系统 @Raw class HeavySDKService { public instance: any = null; public init() { this.instance = new SomeHeavySDK(); } } ``` ### FIND_CHILD_SERVICE 精确控制查找起点 ```ts import { declareProviders, useService, FIND_CHILD_SERVICE } from '@kaokei/use-vue-service'; // 在当前组件中绑定空容器,确保查找起点就是当前组件 declareProviders([]); const findChild = useService(FIND_CHILD_SERVICE); const childService = findChild(ChildService); ``` --- ## 在线示例 | 示例 | 说明 | |------|------| | 01-basic-usage | 基本用法:declareProviders + useService | | 02-service-injection | 服务间依赖注入:@Inject 装饰器 | | 03-three-level-scope | 三层级服务作用域:组件级、App 级、全局根级 | | 04-computed-decorator | @Computed 装饰器 | | 05-raw-decorator | @Raw 装饰器(field / accessor / class) | | 06-run-in-scope | @RunInScope 装饰器 | | 07-find-child-service | FIND_CHILD_SERVICE 和 FIND_CHILDREN_SERVICES | | 08-token-and-binding | Token 系统与自定义绑定 | | 09-vue-router-integration | Vue Router 集成 | | 10-post-construct | @PostConstruct 生命周期 | | 11-lazy-token | LazyToken 解决循环依赖 | | 12-app-providers-plugin | declareAppProvidersPlugin 插件形式 | URL 格式:`https://codesandbox.io/p/sandbox/github/kaokei/use-vue-service/tree/main/examples/<示例目录>` --- ## 与其他方案的对比 | 特性 | use-vue-service | Vuex/Pinia | Angular DI | |------|----------------|------------|------------| | 状态管理方式 | 服务类 + 依赖注入 | 全局 Store | 服务类 + 依赖注入 | | 作用域 | 组件级/App级/全局 | 全局 | 模块级/组件级 | | TypeScript 支持 | 原生类,类型推断好 | 需要额外类型定义 | 原生类 | | 响应式策略 | opt-out(默认全部响应式) | opt-in(显式声明) | 需要 RxJS | | 装饰器语法 | TC39 Stage 3(无需配置) | 无 | 需要 experimentalDecorators | | 学习成本 | 低(熟悉 Angular 更低) | 低 | 中 | | 框架依赖 | Vue 3 | Vue | Angular |