Skip to content

Commit bc465e5

Browse files
committed
优化文档
1 parent 31713f1 commit bc465e5

11 files changed

+242
-150
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# 假设如下场景
2+
3+
父组件是 ParentComp,绑定的服务是 ParentService。
4+
5+
父组件下依赖的子组件有 ChildComp1、ChildComp2、ChildComp3...,对应的服务有 ChildService1、ChildService2、ChildService3...
6+
7+
## 父组件明确指定子组件
8+
9+
一般情况下,父组件需要依赖哪些子组件是明确的,因为可以从 template 中明确知道使用了哪些子组件。
10+
11+
特殊情况是使用了 slot 传递子组件,这时候子组件就是不确定的。
12+
13+
当然这里的不确定是指定义父组件阶段,等到父组件实例化阶段,slot 对应的子组件又是明确的了。
14+
15+
## 子组件不确定父组件
16+
17+
子组件不确定父组件是显然的,因为子组件是可以在多个不同的父组件中使用的,既然可以存在任意数量的父组件,那么显然父组件就是不确定的。
18+
19+
当然这里的不确定是指定义子组件阶段,等到子组件实例化阶段,父组件又是明确的了。
20+
21+
## 父组件获取子组件的服务
22+
23+
虽然一般情况下父组件依赖的子组件是明确的,但是父组件不能在父组件初始化时就立即获取子组件对应的服务,因为子组件可能并没有实例化。
24+
比如子组件不满足 v-if 的条件。那么此时子组件就没有完成实例化,所以此时就不能获取子组件对应的服务。
25+
26+
虽然不能在实例化阶段理解获取子组件以及子组件对应的服务,但是可以通过 findService 在运行时实时查询子组件对应的服务。
27+
比如在用户点击某个按钮时,在事件处理回调函数中可以调用 findService 实时查询子组件的服务,此时依赖的是业务逻辑保证子组件一定是存在的。
28+
当然也可以是不存在,只时需要做判空处理。
29+
30+
需要注意`findService(token, component)`需要 2 个参数,第 1 个参数是服务的 token,第 2 个参数是当前组件的实例。
31+
32+
## 子组件获取父组件的服务
33+
34+
虽然子组件在定义阶段不能确定父组件是谁,但是在实例化阶段是可以立即获取父组件对应的服务的。
35+
因为子组件在实例化阶段,父组件是一定已经完成实例化的,所以此时父组件对应的服务已经存在了,子组件自然就可以获取父组件对应的服务了。
36+
37+
在子组件的实例化阶段,可以直接使用 useService 直接获取父组件的服务。
38+
39+
需要注意`useService(token)`只需要 1 个参数,就是父组件服务的 token,但是`useService`需要依赖`getCurrentInstance``hasInjectionContext`,也就是`useService`只能在 setup 环境下使用。
40+
41+
## 总结
42+
43+
在组件定义阶段,父组件是知道自己依赖哪些子组件的。但是子组件却不知道自己的父组件是谁。
44+
45+
在组件实例化阶段,子组件是可以立即获取父组件以及父组件对应的服务的,但是父组件却不能立即获取子组件实例以及对应的服务,因为不能保证子组件 100%存在。
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# 假设如下场景
2+
3+
父组件是 ParentComp,绑定的服务是 ParentService。
4+
5+
父组件下依赖的子组件有 ChildComp1、ChildComp2、ChildComp3...,对应的服务有 ChildService1、ChildService2、ChildService3...
6+
7+
## 子组件获取父组件的服务
8+
9+
虽然子组件可以获取父组件的服务,但是如果在获取服务时,直接根据`useService(ParentService)`来获取父组件的服务,就会导致子组件是完全依赖着`ParentService`
10+
11+
准确的说是子组件的父组件必须绑定了`ParentService`,如果父组件没有绑定`ParentService`,那么子组件就获取不到`ParentService`,自然就会抛异常。
12+
13+
这就会导致子组件和父组件的耦合性过大,也就导致了子组件的复用性大大降低。为了实现一定程度的解耦,可以这么做。
14+
15+
在子组件中使用 token 来获取父组件的服务。
16+
17+
```ts
18+
const parentService = useService(parentServiceToken);
19+
```
20+
21+
当然这里的 ParentServiceToken 有两种方案:
22+
23+
方案 1:
24+
25+
```ts
26+
const parentServiceToken = new Token<ParentService>('parent service token');
27+
```
28+
29+
方案 1 的优点是简单方便,但是缺点是 parentServiceToken 和 ParentService 还是紧紧耦合在一起的。
30+
31+
方案 2:
32+
33+
```ts
34+
interface IParentService {
35+
property_need: string;
36+
method_need(): void;
37+
}
38+
const parentServiceToken = new Token<IParentService>('parent service token');
39+
```
40+
41+
@todo
42+
43+
方案 2 的优点是 parentServiceToken 和 ParentService 已经完全解耦了,目前 parentServiceToken 的类型完全取决于 IParentService 这个类型定义。
44+
45+
父组件在绑定服务时,只需要确保 ParentService 实现了 IParentService 这个接口即可。

docs/notes/03.不同场景.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
## 场景
2+
3+
#### 场景 1:一个服务可以被多个组件绑定,一个组件也可以绑定多个服务
4+
5+
这里说的绑定是指调用`declareProviders`方法,背后的逻辑是创建了一个独立的 container,所以就算是同一个服务,被不同的组件绑定,那么生成的服务实例也是不一样的。
6+
7+
如果想要不同的组件共享同一个服务实例,那么应该将这个服务提升到这些组件的公共祖先组件中进行绑定,自然所有子孙组件都可以获取到相同的服务实例对象了。
8+
9+
这里的绑定还有另一层含义,就是一旦组件被卸载,该组件绑定的所有服务也会被释放。
10+
11+
组件和服务之间不会出现以下场景:
12+
13+
1. 同一个组件实例关联多个相同的服务实例
14+
2. 同一个服务实例关联多个组件实例
15+
16+
目前可行的一个 Workaround 是采用别名的方式引用同一个服务实例。也就是`container.bind(别名).toService(服务名)`
17+
这样我们就可以给同一个服务绑定多个不同的别名。这些别名会指向同一个服务实例对象。
18+
19+
#### 场景 2:组件中可以访问自身绑定的服务
20+
21+
可以通过`useService(token)`获取对应的服务实例对象。
22+
23+
注意这里`useService(token)`一定需要在`declareProviders([token])`之后。也就是需要先声明绑定关系,再实例化服务。
24+
25+
#### 场景 3:服务可以访问绑定的组件
26+
27+
从而可以通过组件访问 props,emit 等属性。
28+
29+
```ts
30+
import type { ComponentInternalInstance } from 'vue';
31+
import { CURRENT_COMPONENT, Inject } from '@kaokei/use-vue-service';
32+
33+
class DemoService {
34+
@Inject(CURRENT_COMPONENT)
35+
public component!: ComponentInternalInstance;
36+
}
37+
```
38+
39+
#### 场景 4:[子组件获取父组件的服务](./02.子组件获取父组件服务.md)
40+
41+
1. 【推荐】最好是解耦父组件服务的引用,也就是通过 token 来获取父组件的服务,而不是直接使用父组件的服务名作为 token。
42+
43+
2. 【不推荐】直接使用父组件的服务名作为 token 来获取父组件的服务。虽然不推荐,但是副作用也仅仅是降低子组件的复用性而已。
44+
如果我们再定义子组件时就已经确定这个子组件只会在这一个父组件中使用,其实也并不是大问题。
45+
46+
#### 场景 5:父组件获取子组件的服务
47+
48+
1. 【推荐】通过 findService 实时查找子组件对应的服务。可以跨多个组件层级获取深层子组件的服务。
49+
50+
2. 【推荐】通过 ref 获取子组件实例,然后通过子组件实例访问子组件服务。需要依赖 defineExpose 暴露相应的服务实例。
51+
缺点是只能获取父组件自身的子组件,不能获取更深层的子组件对应的服务。
52+
53+
3. 【强烈禁止】这种方案是将子组件的服务绑定到父组件上,也就是将子组件的服务当作父组件的服务来使用。这样父组件和子组件都能获取到这个服务实例了。
54+
缺点是明明是子组件的服务,却绑定在父组件上。最终导致该服务的生命周期和子组件不一致。
55+
另一个缺点是组件服务的作用域非常不清晰,代码维护成本过高。后续删除子组件时,很容易忘记删除对应的服务。
56+
57+
## 参考
58+
59+
https://zhuanlan.zhihu.com/p/3604866493

docs/notes/04.响应式方案.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
## 默认整体实例不是响应式,手动指定属性为响应式属性
2+
3+
可以使用 ref/shallowRef/reactive 等 API 将指定属性设置为响应式变量,这样在 vue 组件的 template 中就可以直接消费这些响应式属性。
4+
5+
但是它们都有一个特点就是不能重新直接赋值,直接重新赋值会导致响应式丢失。当然也可以在赋值前再次调用 ref/shallowRef/reactive 这些 API 来保证响应式功能。
6+
7+
优点是没有增加新的概念,全部都是 vue 自身的 API,很容易理解。
8+
9+
缺点是容易无意间丢失响应式能力。另外如果是采用 ref/shallowRef 这些 API,那么就需要面对烦人的`ref.value`
10+
11+
```ts
12+
import { computed, shallowRef } from 'vue';
13+
14+
export class User {
15+
private userInfo = shallowRef<{ nickName: string }>();
16+
public nick = computed(() => {
17+
return this.userInfo.value?.nickName;
18+
});
19+
}
20+
```
21+
22+
## 默认整体实例是响应式,手动指定属性为 markRaw
23+
24+
本库还是采用了对整个 class 实例对象进行 reactive,然后可以针对部分属性进行 markRaw。
25+
26+
优点是默认所有属性都是响应式的,减少了心智负担。
27+
28+
```ts
29+
import { markRaw } from 'vue';
30+
import { Computed } from '@kaokei/use-vue-service';
31+
32+
export class User {
33+
private userInfo = {} as { nickName: string };
34+
35+
public bigObject = markRaw(someBigObject);
36+
37+
@Computed
38+
public nick() {
39+
return this.userInfo.value?.nickName;
40+
}
41+
}
42+
```

docs/notes/05.装饰器.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
## @Computed 装饰器
2+
3+
[When "reactive" uses "class" and "computed", the "computed" attribute will not Reactive](https://github.com/vuejs/core/issues/1036)
4+
5+
这个 issue 说明了为什么不能直接在 class 的属性上赋值 computed 对象。因为 computed 初始化的时机是非常早的,此时 this 还没有变成响应式对象。
6+
所以 computed 不起作用。
7+
8+
可选的一种方案就是在 this 初始化之后,手动调用 init 方法,在 init 方法中在初始化 computed 相关的属性。
9+
如果是使用@PostConstruct 装饰器,此时也不需要在业务代码中手动调用 init 方法,init 方法会自动执行。
10+
11+
另一种更好的方案就是使用 getter 属性再配合@Computed 装饰器,这样就既能享受 computed 带来的相响应式能力,又能解决 getter 方法没有缓存的问题。
12+
13+
## MarkRaw 装饰器
14+
15+
增加 markRaw 属性装饰器
16+
17+
@todo
File renamed without changes.
File renamed without changes.

docs/notes/99.基本命令.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
## pnpm test
2+
3+
运行所有单元测试
4+
5+
```sh
6+
# 测试指定的单元测试文件,可能匹配到多个符合条件的文件
7+
pnpm test 任意文件路径
8+
```
9+
10+
## pnpm coverage
11+
12+
查看单元测试覆盖率
13+
14+
## pnpm dev
15+
16+
运行 demo 示例代码
17+
18+
## pnpm release
19+
20+
```sh
21+
pnpm release patch
22+
pnpm release minor
23+
pnpm release major
24+
```
25+
26+
修改完代码,并且单元测试都通过之后,自动进行版本升级。
27+
28+
## pnpm build
29+
30+
打包源代码到 dist 目录,用于发布到 npm 仓库
31+
32+
## pnpm public
33+
34+
发布到 npm 仓库

docs/notes/README.md

Lines changed: 0 additions & 17 deletions
This file was deleted.

docs/notes/problem.md

Lines changed: 0 additions & 75 deletions
This file was deleted.

0 commit comments

Comments
 (0)