对比Angular1和Angular2中的依赖注入

214518_unop_59456

原文点这里Victor Savkin 是Angular核心团队成员,路由模块就是他开发的。

我将会通过这篇文章演示如何在Angular2中实现Angular1里面那些使用DI(依赖注入)的常见场景。

我们从最简单的组件开始

我们从实现一个简单的login组件开始。

214551_iyqd_59456

然后,同样的组件用Angular2来实现是这样的:

214603_slfh_59456

经验丰富的开发者都知道,把login组件绑定在login service上是有问题的。这样很难独立测试这个组件。同时,这样也降低了组件的可复用性。如果我们有两个app,里面有两个login service,我们将无法复用login组件。

我们可以通过在system loader上进行monkey patch的方式来弥补这个问题,这样我们就可以替换login service了,然而这并不是正确的解决方案。这里最根本的问题出在设计层面上,DI就是用来帮助我们解决这一问题的。

DI的用法

我们应该构造函数里面注射一个LoginService实例,而不直接创建,这样就可以解决问题。

214615_93ha_59456

所以,我们需要告诉框架应该如何创建这个service的实例。

214625_kpwd_59456

好,我们在Angular2里面来实现一下同样的例子。

214633_q4hd_59456

与Angular 1里面类似,我们需要告诉框架如何创建这个service。在Angular 2中,我们可以把这个service添加到providers列表里面来实现。

214643_oyx0_59456

配置依赖注入provider的“默认”位置在NgModule里面。在当前这个例子中,只会创建一个login service实例,这个实例在login组件和它的所有后代组件里面都可以使用。

把DI限定在某个组件子树中

Angular 2中的DI更加灵活。有时候,你并不想让某个service对同一个模块里面启动的所有组件都可用。而是想让它限定在某个特定的组件子树中。为了实现这一点,你可以在指令或者组件装饰器里面加一个providers配置项。

214653_z7up_59456

在这个例子中,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的另一个不同的实现。

214704_6jco_59456

配置Login Service

DI的另一个好处在于,我们不需要担心依赖本身所依赖的内容。login组件依赖于login service,但是它不需要知道这个service本身依赖于什么。

比方说,这个service需要使用某个配置对象。在Angular 1里面可以这样做:

214714_52ro_59456

对应的Angular 2版本如下:

214725_l1fz_59456

注入组件依赖的HTML元素

当组件需要和它的DOM元素进行交互的时候会有这种需求。以下是Angular 1里面的实现方式:

214733_wnsu_59456

Angular 2里面实现得更加优雅。它会使用同样的DI机制把HTML元素注入到组件的构造函数里面。

214744_tdrk_59456

注入其它指令

多个指令需要互相协作的情况也很常见。例如,如果你需要实现tab页和pane,tab组件需要知道pane组件的存在。

以下是Angular 1里面的实现方式:

214801_nnqr_59456

214810_phjo_59456

我们使用了”require”属性来访问”tab”控制器。

类似地,Angular 2可以实现得更加优雅:

214819_qu7t_59456

我们甚至可以更进一步!tab组件可以使用ContentChildren装饰器来获得pane列表,而不是让pane自己在相关的tab上注册。

214829_nqwb_59456

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解析算法的运行方式:

214845_zvar_59456

所以,如果Pane依赖于Tab,Angular就会检查pane元素是不是恰好拥有一个Tab的实例。如果没有,它会继续检查父层元素。这个过程会反复进行,直到找到一个Tab实例或者到达根注射器为止。

你可以审查页面上的任何元素,然后通过ngProbe属性来获得它的注射器对象。当抛出异常的时候你也可以看到元素上的注射器。

214856_elt3_59456

我知道这看起来有点儿复杂,但是实际情况是,类似的机制在Angular 1里面已经存在了。你可以使用”require”来注入合适的指令。但是,这一机制在Angular 1 里面并未开发完整,这就是为什么我们不能充分利用它的原因。

Angular 2在设计层面上采用了这一机制。结果就是,我们不再需要其它机制了。

高级用法示例

到这里为止,我们已经学习了在Angular 1和Angular 2里面都能运行的例子。接下来,我会给你们展示一些高级用法示例,这些例子在Angular 1里面是完全无法实现的。

可选依赖

如果需要把某个依赖标记成可选的,可以使用Optional装饰器。

214909_chhq_59456

控制可见性

你可以更加明确地指定从哪里获取依赖。例如,你可以在同一个HTML元素上请求另一个指令。

214917_fmsp_59456

或者,你可以在同一个模板里面请求指令。

214925_xmu3_59456

为同一个Service提供两种实现

由于Angular 1里面只有一个注射器对象,所以在同一个app里面LoginService不能有两种实现。在Angular 2里面,由于每一个HTML元素都有一个注射器对象,这一点就不是问题。

214936_d1ma_59456

SubApp1 里面创建的服务和指令会使用CustomPaymentService1,而SubApp2 里面会使用CustomPaymentService2,虽然它们两个都声明了依赖于PaymentService。

 

小结

  • DI是Angular 2的核心机制之一。
  • 它允许你依赖于接口编程,而不是具体类型。
  • 它可以让你的代码更加松耦合。
  • 它提升了可测试性。
  • Angular 2采用了统一的API来给组件注入依赖。
  • Angular 2中的DI机制更加强大。
2 1 收藏 1 评论

可能感兴趣的话题



直接登录
最新评论
跳到底部
返回顶部