深入理解Angular中的依赖注入


Posted in Javascript onJune 26, 2017

一、什么是依赖注入

控制反转(IoC)

控制反转的概念最早在2004年由Martin Fowler提出,是针对面向对象设计不断复杂化而提出的一种设计原则,是利用面向对象编程法则来降低应用耦合的设计模式。

IoC强调的是对代码引用的控制权由调用方转移到了外部容器,在运行是通过某种方式注入进来,实现了控制反转,这大大降低了程序之间的耦合度。依赖注入是最常用的一种实现IoC的方式,另一种是依赖查找。

依赖注入(Dependency Injection)

当然,按照惯例我们应该举个例子, 哦对,我们主要说明的是依赖注入,依赖查找请自行查阅资料。

假设我们有一个能做汉堡的设备(HRobot),需要用肉(meat)和一些沙拉(salad)作为原料,我们可以这样实现:

export class HRobot {
  public meat: Meat;
  public salad: Salad;
  constructor() {
    this.meat = new Meat();
    this.salad = new Salad();
  }
  cook() {}
}

看一下好像没有什么问题,可能你已经发现,我们的原材料都是放在机器里面的,如果我们想吃别的口味的汉堡恐怕就要去乡村基了。

为了可以吃到别的口味的汉堡,我们不得不改造一下我们的HRobot:

export class HRobot {
  public meat: Meat;
  public salad: Salad;
  constructor(public meat: Meat, public salad: Salad) {
    this.meat = meat;
    this.salad = salad;
  }
  cook() {}
}

现在,只要要直接给它meat和salad就好了,我们的HRobot()并不需要知道给它的是什么样的meat:

let hRobot = new HRobot(new Meat(), new Salad());

比如,我们想吃鸡肉汉堡,只需要个它一块鸡肉就好:

class Chicken extends Meat {
  meat = 'chiken';
}

let cRobot = new HRobot(new Chicken(), new Salad());

感觉还不错,我们再也不会为了吃一个鸡肉汉堡大费周章的去改造一台机器,这太不可思议了。

我可能想到了,你还是懒得弄块鸡肉给它,这时候可以使用工厂函数:

export class HRobotFactory {
  createHRobot() {
    let robot = new HRobot(this.createMeat(), this.createSalad());
  }

  createMeat() {
    return new Meat();
  }

  creatSalad() {
    return new Salad();
  }
}

现在有了工厂,就有源源不断的汉堡可以吃了,开不开心,惊不惊喜?

好吧,没有最懒,只有更懒,连工厂都懒得管理我也是无话可说,幸运的是我们有Angular提供的依赖注入框架,它可以让你伸手就有汉堡吃!

二、 Angular依赖注入

在介绍Angular依赖注入之前,先来理一下三个概念:

  1. 注入器(Injector):就想制造工厂,提供了一系列的接口,用于创建依赖对象的实例。
  2. 提供商(Provider):用于配置注入器,注入器通过它来创建被依赖对象的实例,Provider把令牌(Token)映射到工厂方法,被依赖的对象就是通过这个方法创建的。
  3. 依赖(Denpendence):指定了被依赖对象的类型,注入器会根据此类型创建对应的对象。

说了半天到底是什么样的?

深入理解Angular中的依赖注入

用代码示例如下:

var injector = new Injector(...);
var robot = injector.get(HRobot);
robot.cook();

Injector()的实现如下:

import { ReflecttiveInjector } form '@angular/core';

var injector = ReflectiveInjector.resolveAndCreat([
  {provide: HRobot, useClass: HRobot},
  {provide: Meat, useClass: Meat},
  {provide: Salad, useClass: Salad}
]);

还有注入器是这样知道知道初始化HRobot需要依赖Meat和Salad:

export class Robot {
  //...
  consructor(public meat: Meat, public salad: Salad) {}
  //...
}

当然,看了头大是应该的,因为上面的东西压根就不需要自己动手写,Angular的依赖注入框架已经自动帮我们完成了(注入器的生成和调用)。

1. 在组件中注入服务

Angular在底层做了大量的初始化工作,这极大地降低了我们使用依赖注入的成本,现在要完成依赖注入,我们只需要三步:

  1. 通过import导入被依赖的对象服务
  2. 在组件中配置注入器。在启动组件时,Angular会读取@Component装饰器里的providers元数据,它是一个数组,配置了该组件需要使用的所有依赖,Angular的依赖注入框架会根据这个列表去创建对应的示例。
  3. 在组件构造函数中声明需要注入的依赖。注入器会根据构造函数上的声明,在组件初始化时通过第二步中的providers元数据配置依赖,为构造函数提供对应的依赖服务,最终完成依赖注入。

例子来了:

// app.component.ts
//...
// 1. 导入被依赖对象的服务
import { MyService } from './my-service/my-service.service';

@Component({
  //...
  // 2. 在组件中配置注入器
  providers: [
    MyService
  ]
  //...
})

export class AppComponent {
  // 3. 在构造函数中声明需要注入的依赖
  constructor(private myService: MyService) {}
}

2. 在服务中注入服务

除了组件依赖服务,服务间依的相互调用也很寒常见。例如我们想给我们的汉堡机器人加上一个计数器,来记录它的生产状况,但是计数器又依靠电源来工作,我们就可以用一个服务来实现:

// power.service.ts

import { Injectable } from '@angular/core';

@Injectable()
export class PowerService {
  // power come from here..
}


// count.service.ts

import { Injectable } from '@angular/core';
import { PowerService } from './power/power.service';

@Injectable()
export class CountService {
  constructor(private power: PoowerService) {}
}

// app.component.ts  这里是当前组件,其实模块中的注入也一样,后面讲到
//...
providers: [
  CountService,
  PowerService
]

这里需要注意的是@Injectable装饰器是非必须的,因为只有一个服务依赖其他服务的时候才必须需要使用@Injectable显式装饰,来表示这个服务需要依赖,所以我们的PowerService并不是必须加上@Injectable装饰器的,可是,Angular官方推荐是否依赖其他服务,都应该使用@Injectable来装饰服务。

3. 在模块中注入服务

在模块中注册服务和在组件中注册服务的方法是一样的,只是在模块中注入的服务在整个组件中都是可用的。

// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
@NgModule({
 declarations: [
  AppComponent,
 ],
 imports: [
  BrowserModule
 ],
 providers: [CountService, PowerService],
 bootstrap: [AppComponent]
})
export class AppModule { }

与在组件中注入不同的是,在Angular应用启动的时候,它好首先加载这个模块需要的所有依赖,,此时会生成一个全局的根注入器,由该依赖创建的依赖注入对象会再整个应用中可见,并共享一个实例。

Angular没有模块级作用域这个概念,只有应用程序级作用域和组件级作用域,这种设计主要是考虑模块的扩展性,一个应用通常由多个模块合并和成,在@NgModule中注册的服务,默认在整个应用中可用。

下面说两种特殊情况:

假设在两个模块中使用同样的Token注入了同一个服务,并且这两个模块先后导入到了根组件中:

// ...
@NgModule({
imports: [
 AModule,
 BModule
]
// ...
})

那么后面导入的模块中的服务会覆盖前面导入模块中的服务,也就是说BModule中的服务会覆盖AModule中的服务,即使是在AModule中注入的服务,同样使用的是BMoudle中提供的实例。

还是假设两个模块同样使用同一个Token注入了同一个服务,但是BModule模块是导入在AModule模块中的:

// a.module.ts
// ...
@NgModule({
 imports: [BModule]
})

那么这种情况下两个模块使用的都是AModule中注入的服务。可以推断出在根模块中注入的服务是拥有最高优先级的,你可以在任何地方放心使用。

三、Provider

1. Provider的理解

Provider是有必要单独提出来一节的,上面第二节中我们其实只是简单的使用了其中一种的provider下面来详细说一下Provider

在Angular中,Provider描述了注入器(Injector)如何初始化令牌(Token)所对应的依赖服务。Provider一个运行时的依赖,注入器依靠它来创建服务对象的实例。

比如我们上面用到的例子:

// ...
@Component({
  //...
  // 2. 在组件中配置注入器
  providers: [
    MyService
  ]
  //...
})

实际上它的完整形式应该是这样的:

@Component({
  //...
  // 2. 在组件中配置注入器
  providers: [
    {provide: MyService, useClass: MyService}
  ]
  //...
})

所以说我们上面只使用了一种provider: 类Provider(ClassProvider)。

2. Provider注册方式

上面提到我只使用了其中一种注册方式,那么下面介绍Angular中提供的四中常见的注册方式:

  1. 类Provider(ClassProvider)
  2. 值Provider(ValueProvider)
  3. 别名Provider(ExistingProvider)
  4. 工厂Provider(FactoryProvider)

1. 类Provider

类Provider 基于令牌(Token)指定依赖项,这种方式可是让依赖被动态指定为其他不同的具体实现,只要接口不变,对于使用方就是透明的。比如数据渲染服务(Render),Render服务对上层提供的接口是固定的,倒是底层的渲染方式可以不同:

```ts
var inject = Injector.resolveAndCreate([
  {provide: Render, useClass: DomRender}
  //{provide: Render, useClass: DomRender} // canvas 渲染方式
  //{provide: Render, useClass: DomRender} // 服务的想染方式
])

// 调用方不用做任何修改
class AppComponent {
  construtor(private render: Render) {}
}
```

2. 值Provider

由于依赖的对象并不一定都是类,也可以是字符串、常量、对象等其他数据类型的,这可以方便用在全局变量、系统相关参数配置场景中。在创建Provider对象的时候,只需要使用useValue就可以声明一个值Provider

```ts
let freeMan = {
  freeJob: boolen;
  live: () => {return 'do something u cant do'}
};

@Component({
  // ...
  providers: [
    {provide: 'someone', useValue: freeMan}
  ]
})
```

3. 别名Provider

有了别名Provider,我们就可以在一个Provider中配置多个令牌(Token),其对于的对象指向同一个实例,从而实现了多个依赖、一个对象实例的作用:

// ...
  providers: [
    {provider: Power1, useClass: PowerService},
    {provider: Power2, useClass: PowerService}
  ]
  // ...

仔细想想,这样对吗?

显然是不对的,如果两个都使用了useClass那么按照令牌,将会创建两个不同的实例出来,那么应该怎么实现两个令牌同一个实例呢?答案是使用useExistiong:

// ...
  providers: [
    {provider: Power1, useClass: PowerService},
    {provider: Power2, useExisting: PowerService}
  ]
  // ...

4. 工厂Provider

工厂Provider允许我们根据不同的条件来实例化不同的服务,比如,我们在开发环境需要打印日志,但是在实际部署的时候可能并不需要打印这些东西,那么我们总不可能去找到整个应用中所有的console.log()这样的方法吧,这个时候我们可以使用工厂provider来帮我们处理,我们只需要在工厂provider中设定一个条件,使其能够根据条件返回实例化我们需要的服务就可以了。为了实现这样的功能我们可以在根模块中这样注入:

```ts
// app.module.ts
@NgModule({
// ...
providers: [
  HeroService,
  ConsoleService,
  {
    provide: LoggerService, 
    useFactory: (consoleService) => {
      return new LoggerService(true, consoleService);
    },
    deps: [ConsoleService]
  }
],
bootstrap: [AppComponent]
})
export class AppModule { }

哦哦,那两个服务是这样写的:

// console.service.ts
  // ...
  export class ConsoleService {
    log(message) {
      console.log(`ConsoleService: ${message}`);
    }
  }

  // logger.service.ts
  // ...
  export class LoggerService {
    constructor(private enable: boolean, 
      consoleService: ConsoleService
    ) { }

    log(message: string) {
      if (this.enable) {
        console.log(`LoggerService: ${message}`);
      }
    }
  }

然后在组件构造函数中写上需要的服务就好。

四、限定方式的依赖注入

想象一场景,你应用中的某个服务的provider被当做无效代码删掉了,那么你的应用可能就会出问题。还好这个问题早在设计的时候就已经考虑到了,我们可以使用Angular提供的@Optional和@Host装饰器来解决这个问题。

Optional可以兼容依赖不存在的情况,提高系统的健壮性;@Host可以限定查找规则,明确实例化的位置,避免一些莫名的共享对象问题。

@Optional

借助@Optional就可以实现可选注入:

// app.component.ts
// ...
import { Optional } from '@angular/core';
constructor(@Optional() private logger: LoggerService) {
  if (this.logger) {
    this.logger.log('i am choosed');
  }
}

像例子中的那样只需要在宿主组件(Host Component)的构造函数中增加@Optional装饰器即可。

需要注意的是,上面例子中的LoggerService并不是不存在,只是并没有根据providers元数据中配置被实例化出来。

@Host

Angular中依赖查找的规则是按照注入器从当前组件向父组件查找,直到找到要注入的依赖位置,如果找不到就会报错。我们可以使用Angular提供的@Host装饰器来解决 这个问题。

宿主组件如果一个组件注入了依赖项,那么这个组件就是这个依赖的宿主组件;如果这个组件通过<ng-content>被嵌入到了父组件,那这个父组件就是这个依赖的宿主组件。

1、宿主组件是当前组件

我们给组件构造函数加上@Host装饰器:

// ...
 @Component({
   selector: 'parent',
   template: `
     <h1>这里是父组件</h1>
   `
 })
 constructor(
   @Host()
   logger: LoggerService) {}
   // 加上@Host之后会报错,因为我们并没有在这个组件中注入LoggerService

   // 但是我们可以加上@Optional来避免报错
   //@Host()
   //@Optional()
   //logger: LoggerService) {}
 )

2、宿主组件是父组件

我们修改一下上面的组件为父组件:

// parent.component.ts
 // ...
 @Component({
   selector: 'parent',
   template: `
     <h1>这里是父组件</h1>
     <ng-content></content>
   `
   // 在父组件中注入 LoggerService
   providers: [LoggerService] 
 })
 constructor() {}

增加一个子组件:

// child.component.ts
 // ...
 @Component({
   selector: 'child',
   template: `
     <h1>这里是子组件</h1>
   `
 })
 constructor(
   @Host()
   @Optional()
   logger: LoggerService) 
 ){}

当然<parent>标签中应该这样写:

<parent>
   <child></child>
 </parent>

因为此时宿主组件是父组件,所以我们在父组件中注入LoggerService  Angular注入器会自动向上查找,找到ParentComponet中的配置,从而完成注入。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
Javascript中的相等与不等运算
Apr 25 Javascript
JQuery浮动DIV提示信息并自动隐藏的代码
Aug 29 Javascript
DOM 中的事件处理介绍
Jan 18 Javascript
js获取GridView中行数据的两种方法 分享
Jul 13 Javascript
javascript实现控制文字大中小显示
Apr 28 Javascript
JavaScript如何实现在文本框(密码框)输入提示语
Dec 25 Javascript
javascript 面向对象实战思想分享
Sep 07 Javascript
JS实现元素上下左右移动效果
Oct 18 Javascript
vue.js 双层嵌套for遍历的方法详解, 类似php foreach()
Sep 07 Javascript
优雅的处理vue项目异常实战记录
Jun 05 Javascript
简单学习5种处理Vue.js异常的方法
Jun 17 Javascript
浅谈layer的Icon样式以及一些常用的layer窗口使用方法
Sep 11 Javascript
详解AngularJS2 Http服务
Jun 26 #Javascript
详解用node.js实现简单的反向代理
Jun 26 #Javascript
JS作用域链详解
Jun 26 #Javascript
jQuery.Form实现Ajax上传文件同时设置headers的方法
Jun 26 #jQuery
纯JS实现简单的日历
Jun 26 #Javascript
vue2.0全局组件之pdf详解
Jun 26 #Javascript
JavaScript创建对象的七种方式(推荐)
Jun 26 #Javascript
You might like
php中的数组操作函数整理
2008/08/18 PHP
php文件怎么打开 如何执行php文件
2011/12/21 PHP
对象失去焦点时自己动提交数据的实现代码
2012/11/06 PHP
php微信公众号开发模式详解
2016/11/28 PHP
Prototype Function对象 学习
2009/07/12 Javascript
javascript重写alert方法的实例代码
2013/03/29 Javascript
js使用正则实现ReplaceAll全部替换的方法
2014/08/22 Javascript
JS将滑动门改为选项卡(需鼠标点击)的实现方法
2015/09/27 Javascript
jQuery的图片轮播插件PgwSlideshow使用详解
2016/08/11 Javascript
Vue实现双向绑定的方法
2016/12/22 Javascript
JavaScript限定范围拖拽及自定义滚动条应用(3)
2017/05/17 Javascript
es7学习教程之Decorators(修饰器)详解
2017/07/21 Javascript
Jquery cookie插件实现原理代码解析
2020/08/04 jQuery
基于vue的video播放器的实现示例
2021/02/19 Vue.js
Python爬虫实例爬取网站搞笑段子
2017/11/08 Python
利用Python如何批量更新服务器文件
2018/07/29 Python
selenium使用chrome浏览器测试(附chromedriver与chrome的对应关系表)
2018/11/29 Python
python实现图片转字符小工具
2019/04/30 Python
Python测试模块doctest使用解析
2019/08/10 Python
python随机数分布random均匀分布实例
2019/11/27 Python
没编程基础可以学python吗
2020/06/17 Python
Python常用外部指令执行代码实例
2020/11/05 Python
德国隐形眼镜店:LuckyLens
2018/07/29 全球购物
哈利波特商店:Harry Potter Shop
2018/11/30 全球购物
Proenza Schouler官方网站:纽约女装和配饰品牌
2019/01/03 全球购物
服务之星获奖感言
2014/01/21 职场文书
毕业生求职自荐书范文
2014/03/27 职场文书
二手房购房意向书范本
2014/04/01 职场文书
本科毕业生自荐信
2014/05/26 职场文书
文秘应届生求职信
2014/07/05 职场文书
汉语言文学毕业求职信
2014/07/17 职场文书
垂直极限观后感
2015/06/08 职场文书
初中团委工作总结
2015/08/13 职场文书
《詹天佑》教学反思
2016/02/20 职场文书
详解CSS故障艺术
2021/05/25 HTML / CSS
Python可视化学习之seaborn调色盘
2022/02/24 Python