原文点这里,Victor Savkin是Angular核心团队成员,路由模块就是他开发的。
我将会通过这篇文章演示如何在Angular2中实现Angular1里面那些使用DI(依赖注入)的常见场景。
我们从最简单的组件开始
我们从实现一个简单的login组件开始。
然后,同样的组件用Angular2来实现是这样的:
经验丰富的开发者都知道,把login组件绑定在login service上是有问题的。这样很难独立测试这个组件。同时,这样也降低了组件的可复用性。如果我们有两个app,里面有两个login service,我们将无法复用login组件。
我们可以通过在system loader上进行monkey patch的方式来弥补这个问题,这样我们就可以替换login service了,然而这并不是正确的解决方案。这里最根本的问题出在设计层面上,DI就是用来帮助我们解决这一问题的。
DI的用法
我们应该构造函数里面注射一个LoginService实例,而不直接创建,这样就可以解决问题。
所以,我们需要告诉框架应该如何创建这个service的实例。
好,我们在Angular2里面来实现一下同样的例子。
与Angular 1里面类似,我们需要告诉框架如何创建这个service。在Angular 2中,我们可以把这个service添加到providers列表里面来实现。
配置依赖注入provider的“默认”位置在NgModule里面。在当前这个例子中,只会创建一个login service实例,这个实例在login组件和它的所有后代组件里面都可以使用。
把DI限定在某个组件子树中
Angular 2中的DI更加灵活。有时候,你并不想让某个service对同一个模块里面启动的所有组件都可用。而是想让它限定在某个特定的组件子树中。为了实现这一点,你可以在指令或者组件装饰器里面加一个providers配置项。
在这个例子中,AppPart组件让LoginService服务只对自己和所有后代可用,包括login组件。login service的实例将会创建在App组件上。所以,如果有多个子组件依赖它,所有子组件都会得到同一个实例。
这样我们就把两个注意点分离开了:现在login组件依赖于某个抽象的login service,然后app模块会创建这个service的具体实现。这样做的结果是,login组件不再关注它拿到的login service的具体实现。这就意味着我们可以单独测试我们的组件。同时,我们还可以在多个app里面复用同一个组件。
注意,Angular 1依赖于字符串来配置DI。而Angular 2默认使用类型注解的方式,但是,如果需要更强的灵活性,有办法可以降级成字符串的方式。
使用不同的Login Service
我们可以对app进行配置,从而使用login service的另一个不同的实现。
配置Login Service
DI的另一个好处在于,我们不需要担心依赖本身所依赖的内容。login组件依赖于login service,但是它不需要知道这个service本身依赖于什么。
比方说,这个service需要使用某个配置对象。在Angular 1里面可以这样做:
对应的Angular 2版本如下:
注入组件依赖的HTML元素
当组件需要和它的DOM元素进行交互的时候会有这种需求。以下是Angular 1里面的实现方式:
Angular 2里面实现得更加优雅。它会使用同样的DI机制把HTML元素注入到组件的构造函数里面。
注入其它指令
多个指令需要互相协作的情况也很常见。例如,如果你需要实现tab页和pane,tab组件需要知道pane组件的存在。
以下是Angular 1里面的实现方式:
我们使用了”require”属性来访问”tab”控制器。
类似地,Angular 2可以实现得更加优雅:
我们甚至可以更进一步!tab组件可以使用ContentChildren装饰器来获得pane列表,而不是让pane自己在相关的tab上注册。
Query会解决开发者在Angular 1中面临的以下问题:
- pane总是有顺序的。
- 当发生变化的时候,QueryList会通知tab组件。
- Pane没有必要知道Tab的存在。这样Pane组件会更容易测试和复用。
统一API
Angular 1里面有好几个API可以用来给指令注入依赖。谁没有被“factory”,“service”,“provider”,“constant” 和“value”之间的区别困惑过?有些对象是根据参数的位置注入的(例如HTML元素),有些是根据名称注入的(例如,LoginService)。有些依赖会一直自动提供(例如link函数里面的HTML元素),有一些必须使用”require”进行配置,还有一些需要使用参数名称进行配置。
Angular 2 提供了统一的API用来注入服务、指令,和HTML元素。所有这些内容都会被注入到组件的构造函数里面。所以,Angular 2里面需要学习的API更少。同时你的组件会变得更加容易测试。
但是,它是如何运行的呢?当组件需要的时候,它是怎么知道要注入什么HTML元素的呢?它的运行方式如下:
框架会构建一颗注射器树,与DOM元素的结构保持一致。
<tab><pane title=”one”></pane><pane title=”two”></pane></tab>
对应的注射器树如下:
Injector matching <tab>
|
|__Injector matching <pane title=”one”>
|
|__Injector matching <pane title=”two”>
由于每一个DOM元素都有一个注射器,框架就可以提供上下文信息或者局部信息,例如HTML元素、属性,以及相关的指令。
以下就是DI解析算法的运行方式:
所以,如果Pane依赖于Tab,Angular就会检查pane元素是不是恰好拥有一个Tab的实例。如果没有,它会继续检查父层元素。这个过程会反复进行,直到找到一个Tab实例或者到达根注射器为止。
你可以审查页面上的任何元素,然后通过ngProbe属性来获得它的注射器对象。当抛出异常的时候你也可以看到元素上的注射器。
我知道这看起来有点儿复杂,但是实际情况是,类似的机制在Angular 1里面已经存在了。你可以使用”require”来注入合适的指令。但是,这一机制在Angular 1 里面并未开发完整,这就是为什么我们不能充分利用它的原因。
Angular 2在设计层面上采用了这一机制。结果就是,我们不再需要其它机制了。
高级用法示例
到这里为止,我们已经学习了在Angular 1和Angular 2里面都能运行的例子。接下来,我会给你们展示一些高级用法示例,这些例子在Angular 1里面是完全无法实现的。
可选依赖
如果需要把某个依赖标记成可选的,可以使用Optional装饰器。
控制可见性
你可以更加明确地指定从哪里获取依赖。例如,你可以在同一个HTML元素上请求另一个指令。
或者,你可以在同一个模板里面请求指令。
为同一个Service提供两种实现
由于Angular 1里面只有一个注射器对象,所以在同一个app里面LoginService不能有两种实现。在Angular 2里面,由于每一个HTML元素都有一个注射器对象,这一点就不是问题。
SubApp1 里面创建的服务和指令会使用CustomPaymentService1,而SubApp2 里面会使用CustomPaymentService2,虽然它们两个都声明了依赖于PaymentService。
小结
- DI是Angular 2的核心机制之一。
- 它允许你依赖于接口编程,而不是具体类型。
- 它可以让你的代码更加松耦合。
- 它提升了可测试性。
- Angular 2采用了统一的API来给组件注入依赖。
- Angular 2中的DI机制更加强大。