深入理解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 Archive Network 集合
May 12 Javascript
JavaScript 学习笔记(十五)
Jan 28 Javascript
JS字符串函数扩展代码
Sep 13 Javascript
js清理Word格式示例代码
Feb 13 Javascript
分享javascript计算时间差的示例代码
Mar 19 Javascript
把普通对象转换成json格式的对象的简单实例
Jul 04 Javascript
分享jQuery封装好的一些常用操作
Jul 28 Javascript
CSS3 3D 技术手把手教你玩转
Sep 02 Javascript
浅谈JS之tagNaem和nodeName
Sep 13 Javascript
ionic环境配置及问题详解
Jun 27 Javascript
使用layui定义一个模块并使用的例子
Sep 14 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调用Oracle存储过程
2006/10/09 PHP
关于在php.ini中添加extension=php_mysqli.dll指令的说明
2007/06/14 PHP
如何在smarty中增加类似foreach的功能自动加载数据
2013/06/26 PHP
yii2中的rules 自定义验证规则详解
2016/04/19 PHP
24款非常有用的 jQuery 插件分享
2011/04/06 Javascript
用js获取电脑信息(是使用与IE浏览器)
2013/01/15 Javascript
jQuery页面图片伴随滚动条逐渐显示的小例子
2013/03/21 Javascript
sencha touch 模仿tabpanel导航栏TabBar的实例代码
2013/10/24 Javascript
JS组件Bootstrap实现弹出框和提示框效果代码
2015/12/08 Javascript
JavaScript使ifram跨域相互访问及与PHP通信的实例
2016/03/03 Javascript
浅谈Javascript数组(推荐)
2016/05/17 Javascript
功能强大的Bootstrap组件(结合js)
2016/08/03 Javascript
微信小程序 视图容器组件的详解及实例代码
2017/01/19 Javascript
轻松理解JavaScript闭包
2017/03/14 Javascript
Vue2.0实现购物车功能
2017/06/05 Javascript
node前端开发模板引擎Jade的入门
2018/05/11 Javascript
vue服务端渲染页面缓存和组件缓存的实例详解
2018/09/18 Javascript
vue计算属性computed、事件、监听器watch的使用讲解
2019/01/21 Javascript
jQuery实现异步上传一个或多个文件
2020/08/17 jQuery
[38:51]2014 DOTA2国际邀请赛中国区预选赛 Orenda VS LGD-CDEC
2014/05/22 DOTA
使用Python的Tornado框架实现一个Web端图书展示页面
2016/07/11 Python
Python采用Django开发自己的博客系统
2020/09/29 Python
python 读写中文json的实例详解
2017/10/29 Python
如何使用pyinstaller打包32位的exe程序
2019/05/26 Python
PyQt4编程之让状态栏显示信息的方法
2019/06/18 Python
python中pip的使用和修改下载源的方法
2019/07/08 Python
django之自定义软删除Model的方法
2019/08/14 Python
解决Django连接db遇到的问题
2019/08/29 Python
目前不被任何主流浏览器支持的CSS3属性汇总
2014/07/21 HTML / CSS
CSS3中Animation动画属性用法详解
2016/07/04 HTML / CSS
行政经理岗位职责
2013/11/09 职场文书
2014全国两会学习心得体会1000字
2014/03/10 职场文书
公司副总经理任命书
2014/06/05 职场文书
国庆节活动总结
2014/08/26 职场文书
防灾减灾日活动总结
2014/08/26 职场文书
JavaScript控制台的更多功能
2021/04/28 Javascript