进阶教程
# 进阶教程
# 简介
本教程继续介绍下面 2 个 api,可以用来支持更加复杂场景的需求。
# 解答问题
上篇文章中,即基础教程中提到的三个问题,如下:
- 为什么我们不自己 new CountService(),而要通过 useService 获取实例对象?
- useService(CountService) 这里并没有传递 LoggerService,那么这里是如何初始化 LoggerService 的?
- 在不同的组件中调用 useService(CountService),获取到的实例是不是同一个对象?
其实这三个问题可以用一个名词来解答,就是依赖注入
。
第一个问题的答案就是,因为我们要使用依赖注入,我们要通过依赖注入来获取实例对象,而不是通过手动 new 来获取实例对象。至于为什么需要依赖注入,这是因为依赖注入相对于手动 new 更具有优势。
第二个问题的答案就是,useService 会自动帮我们实例化一个 LoggerService 的实例,并且传递给 CountService 构造函数。这也是依赖注入其中一个最重要的优势的体现。依赖注入框架会自动帮助我们实现依赖的注入,包括依赖的依赖的注入,这是一个递归的注入过程。
第三个问题的答案就是,useService 有一个非常重要的特性,在同一个Injector
中同一个服务
一定是单例
的。
所以默认情况下,在不同的组件中调用 useService(CountService),获取到的实例是同一个对象,这种默认的特性正好完美的解决了跨组件通信的问题。具体体现在当任意一个组件内修改了countService.count
属性,两个组件都会重新渲染。
当然,针对特定的复杂场景我们可能需要实现一个服务可以有多个实例,我们可以有两种做法。
- 第一种是使用不同的 Injector,不同 Injector 中的服务都是互相独立的。
- 第二种就是在同一个 Injector 中,可以针对同一个服务取不同的名字,这样让 Injector 以为是不同的服务,从而得到多个实例。
# 给组件绑定 Injector
默认情况下,本库提供了一个全局的根 Injector,它的生命周期是和应用一致的。所以如果是用户信息这类全局性质的数据,那么默认的 Injector 就能满足需求了。
但是如果是某个页面的业务数据,我们期望进入页面的时候初始化数据,离开页面的时候应该销毁数据。那么就应该把这个服务关联到这个页面对应的组件上。这样服务的生命周期就和页面的生命周期一致了。
我们可以借助 declareProviders 来关联组件和 Injector。
import { declareProviders } from "@kaokei/use-vue-service";
在介绍如何使用 declareProviders 函数之前,我们必须弄清楚 useService 是如何工作的。前面已经介绍过 useService 是一个 hooks 函数,是只能在 setup 函数中使用的。
我们还知道一个 vue 项目,最终的产出物就是一棵 vue 组件树,然后 vue 框架会通过 vue 组件树渲染成 dom 树。这个渲染细节我们暂时不用去关心,这里只需要关注 vue 组件树,在这棵树中,任意一个组件节点都有它的父节点,直到根结点。
当我们在某个组件中调用 useService(CountService) 函数时,它会首先从当前组件关联的 Injector 中寻找是否存在 CountService 服务的 provider。如果没有找到,则进入到父组件关联的 Injector 中寻找 CountService 的 provider。如果还没有找到,则继续到更上一层父组件中寻找,直到找到相应服务的 provider,那么就通过这个 provider 获取一个对象出来。这个对象就是 useService(CountService)的返回值。当然还有一种情况就是直到根组件都没有找到 provider,针对这种情况,useService 做了一层 fallbak 机制,就是把 CountService 类当作默认 provider,然后用这个默认的 provider 来获取实例对象。
通过上面简单的介绍,我们应该对 useService 解析机制有一个大概的认识,它和 js 中的原型链的解析机制以及 nodejs 中的 node_modules 解析机制都是非常相似的,应该不是很难理解。
通过上面的介绍,我们知道默认情况下,在不同的组件中调用 useService(CountService)时,肯定是都找不到对应的 provider 的,最终都会冒泡到根组件上,在根组件对应的 Injector 中使用 CountService 类作为默认的 provider 获取实例对象,又因为同一个 Injector 中,同一个服务只有一个实例,所以不同的组件中获取到的是同一个 countService 实例对象。
接下来介绍如何通过 declareProviders 函数来关联组件和 Injector。
提示
有关根组件和根 Injector 的关系上面的介绍在某些细节上存在一些瑕疵,但是不妨碍理解整体的工作机制。具体细节差别可以参考这里
# 定义 A 组件-没有 declareProviders
<template>
<div>
<span>{{ countService.count }}</span>
<button type="button" @click="countService.addOne()">+1</button>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { useService } from "@kaokei/use-vue-service";
import { CountService } from "../services/count.service";
export default defineComponent({
setup() {
// 返回的就是CountService类的实例
// 并且是reactive的
const countService = useService(CountService);
return {
// 可以在模板中直接绑定数据和事件
countService,
};
},
});
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 定义 B 组件-使用 declareProviders
<template>
<div>
<span>{{ countService.count }}</span>
<button type="button" @click="countService.addOne()">+1</button>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { useService } from "@kaokei/use-vue-service";
import { CountService } from "../services/count.service";
export default defineComponent({
setup() {
// 手动定义CountService服务的provider
// 这里就是在当前组件上关联了一个新的Injector
declareProviders([CountService]);
// 返回的就是CountService类的实例
// 并且是reactive的
const countService = useService(CountService);
return {
// 可以在模板中直接绑定数据和事件
countService,
};
},
});
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 分析 A/B 组件的差异
对比上面的 A 组件和 B 组件,我们发现唯一的差别就是 B 组件中调用了declareProviders([CountService]);
,这行代码的功能就是在 B 组件关联了一个新的 Injector,并且配置了 CountService 这个服务的 provider。
这行代码其实是一种简写,完整的代码应该是这样的:
declareProviders([
{
provide: CountService,
useClass: CountService,
},
]);
2
3
4
5
6
从完整的代码里,我们应该可以明确的看出来,其中provide
属性定义了服务的名字,也就是指定了是哪个服务,一般称之为服务标识符。useClass
属性则是指定了如何生成服务的实例。provide 和 useClass 所在的对象被称为 provider。关于具体的 provider 解释,具体可以参考这里。
我们还能发现 declareProviders 函数的参数是一个数组,其实是一个 provider 数组,这是很好理解的,因为一个组件是可以依赖多个不同的服务的,所以可以通过 declareProviders 函数一次性注册多个服务的 provider。
接下来开始分析 A 组件和 B 组件中 useService 的差异了。
在 A 组件中,因为没有使用 declareProviders,所以在当前组件以及父组件中都没有找到 CountService 服务的 provider,一直到根组件中都没有找到对应的 provider,所以只能把 CountService 类当作默认的 provider 获取实例对象。然后作为 useService 函数的返回值。
在 B 组件中,因为 B 组件本身就已经定义了 CountService 服务的 provider,所以不用到父组件中去寻找了,更不需要到根组件中寻找了。直接在当前组件关联的 Injector 中获取服务的实例对象,然后作为 useService 函数的返回值。
最后的总结就是 A 组件中的 countService 对象是在根组件关联的 Injector 中,B 组件中的 countService 对象是在 B 组件关联的 Injector 中。
这样我们就达成了我扪想要的效果了,即通过 declareProviders 手动管理服务的位置,从而管理服务的生命周期,其实也是管理了该服务对哪个组件(及其子孙组件)可见,达到类似作用域的功能。同时我们也客观上实现了同一个服务可以具有多个实例对象,因为在定义 CountService 服务时是不用关心在哪里使用的,只需要定义一次,但是根 Injector 和 B 组件关联的 Injector 中都有 CountService 实例对象。
这里还是要多说一句,一开始我们一直强调同一个服务在同一个 Injector 下只有一个实例,这是默认行为。然后这里我们又花费了大篇文章介绍怎么实现同一个服务获取多个实例对象。看起来有些冲突,或者说是多此一举。实际上并不是这样的,这是因为业务的复杂性决定的,大多数简单的场景下我们是不需要 declareProviders 的,但是当业务场景足够复杂的时候,我们还是需要一种机制去实现多例的功能。为了满足不同的业务场景,我们肯定是需要提供这种基础能力的。