SOLOMON
ABOUT
LINK

NgModule 的作用域

Oct 16, 2017 · #angular

前言

在开始聊这个话题之前,先提一个我遇到过问题:假设有一个 SpinnerService,这是一个可以在进行发送 HTTP 请求等异步操作的显示一个加载动画的 Service 。

这样看来,它应该是每个 Module 和 Component 中都可能用到的一个 Service,那么我们把放到 SharedModuleproviders 当中。然后在需要这个 SpinnerService 的 Module 中导入这个 SharedModule 即可。代码看起来大概是这样的:

 1  // app/shared/spinner.service.ts
 2  @Injectable()
 3  export class SpinnerService {
 4    show () { /* code logic */ }
 5    hide () { /* code logic */ }
 6  }
 7  
 8  // app/shared/shared.module.st
 9  @NgModule({
10    providers: [SpinnerService]11    //other staffs
12  })
13  export class SharedModule {}
14  
15  // app/some/some.module.ts
16  @NgModule({
17    imports: [SharedModule],
18    // other staffs
19  })
20  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 ):

1  export class Car {
2    public engine: Engine;
3    constructor() {
4      this.engine = new Engine();
5    }
6  }

当然也可以这么写(使用 Dependency Injection ):

export class Car {
  constructor(public engine: Engine) {}
}

两者看起来区别不大,而且当需要调用 engine 的时候,用法也是相同的。但是后者的好处却有不少:

  1. EngineCar 的结构是完全独立的:传入 Car 中的是 Engine 的一个实例而不是在 Car 的新建这个实例。所以如果 Engine 的构造函数发生变化的时候也不需要修改 Car 中的构造函数。

  2. 因为传入 Car 中的是一个实例,所以可以复用这个实例,减少内存占用。

  3. 因为传入的只是一个实例,所以甚至可以传入一个虚假用于单元测试的 Engine

Angular 通过 Hierarchical Dependency Injectors 实现了上述的 Dependency Injection 。 Hierarchical Dependency Injectors 具体的实现,例如 Service 的复用, Component 的 Injectors 等都是很值得一说的,不过和本文相关性不大,这里按下不表。

Providers & Declarations

我们知道一个 Module 中可以有 Components , Directives , 和 Pipes ,还有 Services 。前三者都是与 HTML 模板相关的,需要放在在 Module 的 declarations 中;后者一般用来处理数据,放在 providers 中。

那么 providersdeclarations 的区别是什么呢?其中一个就是两者的作用域不同: providers 中是全局的;而 declarations 则是本地的,只有该 Module 中可以使用。

Services 为什么会是全局的呢?其实很好理解,因为 Services 经过编译之后最后都是生成一个类,在导入或是导出它们的时候都会限定在命名空间内。

所以, Services 应该只在 providers 中出现一次;而其他三者则是在需要的 Module 的 declarations 地方都出现。

当然,在所有出现的地方都需要写一遍也是非常烦杂。这时我们就可以把他们放到一个 SharedModuledeclarationsexports ,最后在需要的 Module 中导入该 SharedModule 即可。

对应到上面的问题,你会发现确实也只有在 SharedModuleproviders 中出现了 SpinnerService 呀,但是问题还是存在啊。

其实不止 SharedModule ,导入了 SharedModuleSomeModule 相当于也提供了 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 放到 AppModuleproviders 里即可。

但是,这样的简单粗暴地将每一个 Service 都交由 AppModule 提供的解决方法违反了我们一贯的原则:尽可能保持每个 Moudle 的功能和结构简单。

所以,我们确实应该将 SpinnerService 移出 SharedModule ,然而也不应该放进 AppModule 而是可以考虑放进一个新建的 CoreModule 中。而这个 CoreModule 也应该作为一个纯粹的只提供 Services 的 Module ,而只在 AppModule 中导入它。

当然,因为只在 AppModule 中导入,所以如果有一些只需要在 AppComponent 中使用的 Component ,如 NavComponentFooterComponent 等也可以考虑放到其中。

References

  1. 文章中提到了可以使用一个虚假的 Service 用于 Component 的单元测试, 这里 介绍了具体应该怎么做。

  2. Angular 的 Hierarchical Dependency Injectors 系统,这是一个很有趣的系统,每一个 Component 都有一个与之对应的可编辑的 Injector 。具体可以查看的 Angular 的官方文档: Hierarchical Dependency Injectors

  3. 写 Angular 应用的一个原则都是保持每一个 Module 的功能和结构的简单和统一,这一点和 Unix 的哲学不谋而合:Write programs that do one thing and do it well. 那么我们怎么应该这么设计一个好的 Module 呢?Angular 官方的 NgModule FAQs 中其实给出了 答案 。从中我们可以看出,CoreModule 这种只提供 Service 和 SharedModule 这种只提供 Components ,Directives 和 Pipes 的 Module 是目前来说官方认为最好的设计。

Content licensed under CC-BY-SA-4.0.