深入理解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 错误处理的几种方法
Jun 13 Javascript
jQuery 拖动层(在可视区域范围内)
May 24 Javascript
jQuery判断密码强度实现思路及代码
Apr 24 Javascript
关于获取DIV内部内容报错的原因分析及解决办法
Jan 29 Javascript
一个字符串中出现次数最多的字符 统计这个次数【实现代码】
Apr 29 Javascript
jQuery+CSS实现简单切换菜单示例
Jul 27 Javascript
node.js中 stream使用教程
Aug 28 Javascript
Node.js利用Net模块实现多人命令行聊天室的方法
Dec 23 Javascript
Angular17之Angular自定义指令详解
Jan 21 Javascript
Node.js中的cluster模块深入解读
Jun 11 Javascript
JavaScript中如何对多维数组(矩阵)去重的实现
Dec 04 Javascript
在Chrome DevTools中调试JavaScript的实现
Apr 07 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
动态加载图片路径 保持JavaScript控件的相对独立性
2010/09/06 Javascript
关于javascript模块加载技术的一些思考
2014/11/28 Javascript
JQuery的ON()方法支持的所有事件罗列
2015/02/28 Javascript
JavaScript使用replace函数替换字符串的方法
2015/04/06 Javascript
javascript十六进制及二进制转化的方法
2015/05/06 Javascript
手机端转盘抽奖代码分享
2015/09/10 Javascript
深入浅析Node.js 事件循环
2015/12/20 Javascript
Bootstrap编写一个兼容主流浏览器的受众门户式风格页面
2016/07/01 Javascript
React组件的三种写法总结
2017/01/12 Javascript
vue实现仿淘宝结账页面实例代码
2017/11/08 Javascript
微信小程序scroll-view实现字幕滚动
2018/07/14 Javascript
基于three.js实现的3D粒子动效实例代码
2019/04/09 Javascript
vue 导航内容设置选中状态样式的例子
2019/11/01 Javascript
微信公众号网页分享功能开发的示例代码
2020/05/27 Javascript
nuxt 每个页面head标签内容设置方式
2020/11/05 Javascript
在python中使用正则表达式查找可嵌套字符串组
2017/10/24 Python
python 限制函数调用次数的实例讲解
2018/04/21 Python
使用pandas对两个dataframe进行join的实例
2018/06/08 Python
Python数据结构之栈、队列及二叉树定义与用法浅析
2018/12/27 Python
对dataframe数据之间求补集的实例详解
2019/01/30 Python
python 并发编程 多路复用IO模型详解
2019/08/20 Python
python+opencv实现车牌定位功能(实例代码)
2019/12/24 Python
python读取tif图片时保留其16bit的编码格式实例
2020/01/13 Python
CSS3 特效范例整理
2011/08/22 HTML / CSS
html5移动端自适应布局的实现
2020/04/15 HTML / CSS
美国家用电器和电子产品商店:Abt
2016/09/06 全球购物
佳能加拿大网上商店:Canon eStore Canada
2018/04/04 全球购物
优秀员工年终发言演讲稿
2014/01/01 职场文书
写给女朋友的道歉信
2014/01/08 职场文书
党的群众路线教育实践活动先进个人材料
2014/12/24 职场文书
个园导游词
2015/02/04 职场文书
小学教师教育随笔
2015/08/14 职场文书
奖学金申请书(范文)
2019/08/14 职场文书
留学文书中的个人陈述,应该注意哪些问题?
2019/08/23 职场文书
详解CSS中的特指度和层叠问题
2021/07/15 HTML / CSS
python 详解turtle画爱心代码
2022/02/15 Python