Skip to content

最佳实践

declareProviders 必须在 setup 顶部调用

declareProviders 一定要在 setup 的顶部使用。如果先调用了 useService,然后再调用 declareProviders,就会导致获取的服务可能不是我们期望的。

不要滥用服务

虽然基于服务的依赖注入用起来很方便,但是也不应该滥用,还是需要考虑使用场景。建议在容器组件中使用服务,在受控组件中还是推荐使用 props/emit 的方式进行交互。

服务分层建议

建议自己基于 axios/superagent 封装自己的 HttpClientService,然后基于 HttpClientService 封装自己的 DaoService

个人建议是一个项目只需要一个 DaoService,并不需要按照模块划分成多个。当然你也可以按照领域划分成不同的 DaoService,只是会过于繁琐。一个项目大多数情况下很难超过 200 条接口,维护在一个服务中是可以接受的。

再然后 StudentServiceTeacherServiceClassService 这些业务 Service 直接注入 DaoService 即可。

服务的生命周期应该和组件一致

数据的生命周期应该和组件的生命周期一致,当组件销毁时,数据也应该跟着销毁。

子组件的生命周期应该受到父组件的生命周期的约束。当父组件销毁时,子组件也要跟着销毁。

目前来看,数据默认会绑定到全局。其他情况数据应该绑定到路由组件,以及子路由组件上。

不建议通过 props 传递 service

何时使用 declareProviders

可以说在 99% 的场景中我们都不需要使用 declareProviders,除非业务足够复杂。

当我们不使用 declareProviders 时,意味着所有的服务都是全局的,那么所有的服务的生命周期都是全局的。

如果我们的业务稍微复杂一点,我们可以在页面路由层面使用 declareProviders,这样这些服务的生命周期就是路由级别的,当切换路由的时候,这些服务也会被卸载。

如果我们的业务更加复杂,可以在 2 级路由、3 级路由都这样操作。

还有一种场景就是某个组件本身需要使用 declareProviders,即我们希望每个组件实例都有自己的服务实例,而不是所有的组件共享服务。比如 Table 组件我们可以提供一个选择了 table 的哪些行的、以及全选/取消全选这种服务。显然我们希望这个服务是每个 table 组件独享的。

useExisting vs useClass

参考 Angular 的文档,可以更好的了解两者的区别。

使用 useClass,只要 provide 的名字不一样,就算 useClass 指向的服务相同,那么也算是不同的服务,最终相当于得到了同一个服务的多个实例。

useExisting 则是刚好相反,即使 provide 不相同,但是只要 useExisting 指向的服务是存在的,则立即返回这个服务实例,并不会创建一个新的实例。

使用 InjectionKey 保留类型信息

参考 Angular 的文档

ts
import { InjectionKey } from '@kaokei/use-vue-service';
type SomeServiceKey = InjectionKey<SomeService>;
const someServiceKey: SomeServiceKey = Symbol();

// someService 的类型就是 SomeService
// 这就是 InjectionKey 所起到的作用
// 如果 SomeService 是一个类的话,其实是没有必要这样做的
// 但是如果 SomeService 是一个接口的话,则只能这样做才能保留类型信息
const someService = useService(someServiceKey);

根组件和根 Injector 的关系

本库自带了一个全局唯一的根 Injector,如果整个应用只有一个根组件,可以理解为根组件和根 Injector 是绑定的。

但是整个应用是可能有多个根组件的,在 Vue 中,我们可以多次调用 createApp 来创建多个根组件。每个根组件都是独立的。这样就导致根 Injector 还是处在最顶层。我们可以手动调用:

ts
app1.use(declareAppProvidersPlugin([ServiceA]));
app2.use(declareAppProvidersPlugin([ServiceB]));

上面的代码的作用是手动给每个根组件绑定一个 Injector,这样就能做到某些服务只给某个 app 使用。

禁止使用 watch

watch 有两个优势:

第一,watch 某个属性,属性变化时更新 10 个属性。如果通过 computed 来实现的话,就需要重复 10 遍代码。

第二,watch 某个属性,属性变化时执行某些副作用,比如请求某个 api、输出日志等。这种功能显然是不能通过 computed 来实现的。

既然如此,为什么还是不建议使用 watch 呢?

因为第一点出现的场景不会特别夸张,就算有 2、3 个属性共同依赖某个属性,那也是建议都写成 computed,而不是把更新逻辑放在 watch 中。

第二点建议是在触发事件的地方去手动调用更新逻辑,而不是依赖数据驱动副作用。

总结以上两点的本质原因在于,我们可以利用数据驱动模版更新,但是我们不应该利用数据驱动其他副作用。

这涉及到心智模型的问题,期望的心智模型是:数据驱动模版,模版响应事件,在事件中修改数据,数据反过来又驱动模版更新。

这中间最多可以接受 computed 对数据层做一层聚合,但是仍然可以看作是数据层的一部分。

但是如果引入了 watch 就不一样了,因为它破坏了这个简单的心智模型:

  • 数据变化了不仅仅会驱动模版更新,还会触发 watch 中定义的副作用,谁也不知道这个副作用最终又是怎么影响数据和模版的。
  • 原本的逻辑是模版响应事件,我们直接在事件中更新数据,数据再反应到模版上。但是引入 watch 之后,我们可能会写出这样的代码:在事件中只会更新某个数据,然后在 watch 中观察这个数据,然后执行相应的副作用,最终修改了我们想要的数据,数据驱动模版更新。

很明显心智模型变得更加复杂了。