前言
在开始聊这个话题之前,先提一个我遇到过问题:假设有一个 SpinnerService
,这是一个可以在进行发送 HTTP 请求等异步操作的显示一个加载动画的 Service。
这样看来,它应该是每个 Module 和 Component 中都可能用到的一个 Service,那么我们把放到 SharedModule
的 providers
当中。然后在需要这个 SpinnerService
的 Module 中导入这个 SharedModule
即可。代码看起来大概是这样的:
// app/shared/spinner.service.ts
@Injectable()
export class SpinnerService {
show() {
/* code logic */
}
hide() {
/* code logic */
}
}
// app/shared/shared.module.st
@NgModule({
providers: [SpinnerService]
// ... other staffs
})
export class SharedModule {}
// app/some/some.module.ts
@NgModule({
imports: [SharedModule]
// ... other staffs
})
export class SomeModule {}
代码看起来没什么问题,而且也能正常运行。但是如果你在两个不同的 Component 中分别调用 SpinnerService
中的 show()
和 hide()
函数,你会发现加载动画没有如愿的消失;更近一步,如果在 Service 中有共用的数据的,你会发现两个不同的 Component 中对数据的修改也无法同步。Component 中的 Service 是由 Module 提供的,所以问题就出在 Module 上。
有的读者的可能已经猜到了:虽然两个 Module 中会提供一个 SpinnerService
,但是他们不是同一个实例。
Dependency Injection
在继续解释为什么不是同一个实例的之前,我想先粗略地说一下为什么应该会是同一个实例。这就涉及到一个概念:Dependency Injection,动态注入。
以 angular.io 上的例子为例:假设有一个 Car
类,其依赖一个 Engine
类。
于是你可以这么写(不使用 Dependency Injection):
export class Car {
public engine: Engine;
constructor() {
this.engine = new Engine();
}
}
当然也可以这么写(使用 Dependency Injection):
export class Car {
constructor(public engine: Engine) {}
}
两者看起来区别不大,而且当需要调用 engine
的时候,用法也是相同的。但是后者的好处却有不少:
Engine
和Car
的结构是完全独立的:传入Car
中的是Engine
的一个实例而不是在Car
的新建这个实例。所以如果Engine
的构造函数发生变化的时候也不需要修改Car
中的构造函数。因为传入
Car
中的是一个实例,所以可以复用这个实例,减少内存占用。因为传入的只是一个实例,所以甚至可以传入一个虚假用于单元测试的
Engine
。
Angular 通过 Hierarchical Dependency Injectors 实现了上述的 Dependency Injection。Hierarchical Dependency Injectors 具体的实现,例如 Service 的复用,Component的 Injectors 等都是很值得一说的,不过和本文相关性不大,这里按下不表。
Providers & Declarations
我们知道一个 Module 中可以有 Components、Directives、Pipes 和 Services。前三者都是与 HTML 模板相关的,需要放在在 Module 的 declarations
中;后者一般用来处理数据,放在 providers
中。
那么 providers
和 declarations
的区别是什么呢?其中一个就是两者的作用域不同:providers
中是全局的;而 declarations
则是本地的,只有该 Module 中可以使用。
Services 为什么会是全局的呢?其实很好理解,因为 Services 经过编译之后最后都是生成一个类,在导入或是导出它们的时候都会限定在命名空间内。
所以,Services 应该只在 providers
中出现一次;而其他三者则是在需要的 Module 的 declarations
地方都出现。
当然,在所有出现的地方都需要写一遍也是非常烦杂。这时我们就可以把他们放到一个
SharedModule
的declarations
和exports
,最后在需要的 Module 中导入该SharedModule
即可。
对应到上面的问题,你会发现确实也只有在 SharedModule
的 providers
中出现了 SpinnerService
呀,但是问题还是存在啊。
其实不止 SharedModule
,导入了 SharedModule
的 SomeModule
相当于也提供了 SpinnerService
。所以在两个 Module 中就会有两个由不同的 Module 提供的,两个不同的 SpinnerService
。
导入拥有提供了 Services 的 Module,相当与自己提供了相同的 Services。这样的例子这样的情况你可能早就接触过了:当你在 AppModule
中导入了 HttpModule
之后,你就可以使用 Http
这个全局 Service 来发送 HTTP 请求了。
这里的
AppModule
指 Root Module,下同。
另一方面,如果一个 Module 既有 Components 也有 Services 时则需要分别对待了:在 AppModule
中导入这个 Module 的时候需要调用 forRoot()
,它返回的是一个 ModuleWithProviders
;而在其他的 Module 则是直接导入这个 Module 或者调用 forChild()
。例如 RouterModule
就既有 Component <router-outlet>
和 Directive routerLink
,也有 Service ActivatedRoute
。
Best Practice
至此,要解决文章开头的问题可以很简单:将 SpinnerService
放到 AppModule
的 providers
里即可。
但是,这样的简单粗暴地将每一个 Service 都交由 AppModule
提供的解决方法违反了我们一贯的原则:尽可能保持每个 Moudle 的功能和结构简单。
所以,我们确实应该将 SpinnerService
移出 SharedModule
,然而也不应该放进 AppModule
而是可以考虑放进一个新建的 CoreModule
中。而这个 CoreModule
也应该作为一个纯粹的只提供 Services 的 Module,而只在 AppModule
中导入它。
当然,因为只在
AppModule
中导入,所以如果有一些只需要在AppComponent
中使用的 Component,如NavComponent
和FooterComponent
等也可以考虑放到其中。
References
文章中提到了可以使用一个虚假的 Service 用于 Component 的单元测试,这里介绍了具体应该怎么做。
Angular 的 Hierarchical Dependency Injectors 系统,这是一个很有趣的系统,每一个 Component 都有一个与之对应的可编辑的 Injector。具体可以查看的 Angular 的官方文档:Hierarchical Dependency Injectors。
写 Angular 应用的一个原则都是保持每一个 Module 的功能和结构的简单和统一,这一点和 Unix 的哲学不谋而合:Write programs that do one thing and do it well.那么我们怎么应该这么设计一个好的 Module 呢?Angular 官方的 NgModule FAQs 中其实给出了答案。从中我们可以看出,
CoreModule
这种只提供 Services 和SharedModule
这种只提供 Components,Directives 和 Pipes 的 Module 是目前来说官方认为最好的设计。