当前位置: 移动技术网 > IT编程>脚本编程>AngularJs > 深入理解Angular中的依赖注入

深入理解Angular中的依赖注入

2017年12月12日  | 移动技术网IT编程  | 我要评论

一、什么是依赖注入

控制反转(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):指定了被依赖对象的类型,注入器会根据此类型创建对应的对象。

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

用代码示例如下:

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中的配置,从而完成注入。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网