8.《Angular依赖注入》

一、什么是依赖注入

控制反转 Inversion of Control (简称IOC) 最早在2004年由Martin Fowler提出,是针对面向对象设计不断复杂化而提出的一种设计原则,是一种利用面向对象编程法则来降低应用程序耦合的设计模式。IoC强调的对代码引用的控制权由调用方法转移到外部容器,在运行时通过某种方式注入进来,实现控制的反转,这大大降低了服务类之间的耦合度。依赖注入是一种最常用的实现IoC的方式。

实现IOC的框架叫IOC容器,Angular实现控制反转的手段就是依赖注入

依赖注入 Dependency Injection(简称DI)是用来创建对象及其依赖的其它对象的一种方式。 当依赖注入系统创建某个对象实例时,会负责提供该对象所依赖的对象(称为该对象的依赖)。在依赖注入模式中,应用组件无需关注所依赖对象的创建和初始化过程,可以认为框架已初始化好了,开发者只管调用即可。依赖注入有利于应用程序中各模块之间的解耦,使得代码更容易维护。这种优势可能一开始体现不出来,但随着项目复杂度的增加,各模块、组件、第三方服务等相互调用更频繁时,依赖注入的优点就体现出来了。开发者可以专注于所依赖对象的消费,无需关注这些依赖对象的产生过程,这将大大提升开发效率。

二、使用依赖注入的好处

1.松耦合和可重用性
2.提高可测试性
RealLoginService在其他小组开发完后才进行替换,之前的开发引用MockLoginService的静态代码进行测试。

提高可测试性.png

三、Angular中如何实现依赖注入

1.注入器(Inject):

每一个组件都有一个注入器实例,负责注入组件需要的对象,注入器是Angular提供的一个服务类,一般情况下不需要直接调用注入器的方法,注入器会自动通过组件的构造函数将组件所需的对象注入进组件。

constructor(private productService:ProductService){...}
此组件的的构造函数声明了一个productService属性,他的类型是ProductService

Angular的注入器在看到这样一个构造函数声明时,其会在整个Angular应用中去寻找ProductService的实例,如果能找到这个实例,那么就会将ProductService注入到productService里去。

2.提供器(Provider):

用于配置注入器,注入器通过它来创建被依赖对象的实例,Provider把标识映射到工厂方法中,被依赖的对象就是通过该方法创建的。

providers:[ProductService]
等同于 providers:[{provide:ProductService, useClass:ProductService}]
provide指定了提供器的token, useClass表示实例话属性为new。
providers:[{provider:ProductService, useClass:AnotherProductService}]
providers:[{provide:ProductService, useFactory:()=>{...}]

3.提供器的作用域规则

  • 当一个提供器声名在一个模块中时,所有组件都可注入。
  • 当一个提供器声名在一个组件时,只对此组件和其子组件可见,其他组件不可注入。
  • 当声明在模块中的提供器和声明在组件的提供器具有相同的token时,声明在组件的提供器会覆盖声明在模块中的提供器。
  • 一般情况下我们会优先将服务提供器声明在模块中,只有在服务必须于某个组件相对于其他组件不可见时才声明在组件中,而这种情况十分少见。

四、在模块中注入服务

在根组件中注入这个服务,所有子组件都能共享这个服务。
在模块中注入服务和之前的注入场景稍有不同。Angular在启动程序时会启动一个根模块,并加载它所依赖的其他模块,此时会生成一个全局的根注入器,由该注入器创建的依赖注入对象在整个应用程序级别可见,并共享一个实例。同时根模块会指定一个根组件并启动,由该根组件添加的依赖注入对象是组件树级别可见,在根组件以及子组件中共享一个实例。

import {NgModule} from '@angular/core'
import {RouterModule} from "@angular/router";
import {FormsModule} from "@angular/forms";
import {BrowserModule} from "@angular/platform-browser";
import {HttpModule} from "@angular/http";

import {rootRouterConfig} from "./app.routes";
import {AppComponent} from "./app.component";



import {ContactService, UtilService} from "./shared";


@NgModule({
  declarations: [
    AppComponent

  ],
  imports     : [BrowserModule, FormsModule, HttpModule, RouterModule.forRoot(rootRouterConfig)],
  providers   : [ContactService, UtilService],
  bootstrap   : [AppComponent]
})
export class AppModule {

}

五、层级注入

Angular以组件为基础,项目开发中自然会有层级嵌套的情况,这种组织关系组成了组件树。根组件下面的各层级的子组件,可以出现在任何层级的任何组件中,每个组件可以拥有一个或多个依赖对象的注入,每个依赖对象对于注入器而言都是单例。

//生成唯一标识服务
import {Injectable} from "@angular/core";
@Injectable()
export class Random{
  public num;
  constructor(){
    this.num=Math.random();
  }
}
//子组件A
import {Component} from "@angular/core";
import {Random} from "./Random";
@Component({
  selector:'contact-a',
  providers:[Random],
  template:`<div>ContactA:{{random.num}}</div>`
})
export class contactAComponent{
  random:Random;
  constructor(r:Random){
    this.random=r;
  }
}
//子组件B
import {Component} from "@angular/core";
import {Random} from "./Random";
@Component({
  selector:'contact-b',
  providers:[Random],
  template:`<div>ContactB:{{random.num}}</div>`
})
export class contactBComponent{
  random:Random;
  constructor(r:Random){
    this.random=r;
  }
}
//父组件
import {Component} from "@angular/core";
@Component({
  selector:'contact-list',
  template:`
      <h1>Contact-List</h1>
      <contact-a></contact-a>
      <contact-b></contact-b>
`
})
export class ContactListComponent{
  constructor(){}
}

结果将输出:
Contact-List
ContactA:0.4500488165839276
ContactB:0.5389674473022938

上述的结果说明,每个子组件都创建了自己独立的注入器,也就是说通过依赖注入的Random服务都是独立的,如果把注入器提升到父组件中,则结果将会不一样。

//子组件A
import {Component} from "@angular/core";
import {Random} from "./Random";
@Component({
  selector:'contact-a',
  //providers:[Random],
  template:`<div>ContactA:{{random.num}}</div>`
})
export class contactAComponent{
  random:Random;
  constructor(r:Random){
    this.random=r;
  }
}
//子组件B
import {Component} from "@angular/core";
import {Random} from "./Random";
@Component({
  selector:'contact-b',
  //providers:[Random],
  template:`<div>ContactB:{{random.num}}</div>`
})
export class contactBComponent{
  random:Random;
  constructor(r:Random){
    this.random=r;
  }
}
//父组件
import {Component} from "@angular/core";
import {Random} from "./Random";
@Component({
  selector:'contact-list',
  providers:[Random],
  template:`
      <h1>Contact-List</h1>
      <contact-a></contact-a>
      <contact-b></contact-b>
`
})
export class ContactListComponent{
  constructor(){}
}

此时,结果变为
Contact-List
ContactA:0.6257492668005642
ContactB:0.6257492668005642

上述的输出结果说明了子组件继承了父组件的注入器,所以子组件使用了相同的Random实例,输出了相同的结果。

那么,该如何选择在根组件还是在子组件中注入服务呢?
这取决于想让注入的依赖服务具有局限性还是全局性,由于每个注入器总是将它提供的服务维持单例,因此,如果不需要针对每个组件都提供独立的服务单例,就可以在根组件中注入,整个组件树共享根注入器提供的服务实例;如果需要针对每个组件提供不同的服务实例,就应该在格子组件中配置providers元数据来注入服务。

Angular如何查找到合适的服务实例呢?
在组件的构造函数视图注入某个服务的时候,Angular会先从当前组件的注入器中查找,找不到就继续往父组件的注入器查找,直到根组件注入器,最后到应用根注入器,此时找不到的话就会报错。

六、使用工厂函数

import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';


import {AppComponent} from './app.component';
import {Product1Component} from './product1/product1.component';
import {ProductService} from './shared/product.service';
import {Product2Component} from './product2/product2.component';
import {LoggerService} from './shared/logger.service';
import {AnotherProductService} from './shared/another-product.service';


@NgModule({
  declarations: [
    AppComponent,
    Product1Component,
    Product2Component
  ],
  imports: [
    BrowserModule
  ],
  providers: [
    {
      provide: ProductService,
      useFactory: () => {
        let logger = new LoggerService();
        let dev = Math.random() > 0.5;
        if (dev) {
          return new ProductService(logger);
        } else {
          return new AnotherProductService(logger);
        }
      }
    }
    , LoggerService],
  bootstrap: [AppComponent]
})
export class AppModule {
}

这样我们根据生成随机数的不同而调用不同的服务,注意Product1Component和Product2Component两个组件使用的服务永远是相同的,这意味着工厂方法创建的对象是一个单例对像,工厂方法只在创建第一个需要被注入的对象时调用一次,然后在整个应用中所有被注入的ProductService的实例都是同一个对象。

现在有两个问题,一是我们手动实例化ProductService,这样会造成我们的工厂方法会跟LoggerService紧密的耦合在一起,而实际上我们是有声明LoggerService这个提供器的,接下来我们将在工厂方法里使用使用LoggerService提供器。

@NgModule({
 declarations: [
   AppComponent,
   Product1Component,
   Product2Component
 ],
 imports: [
   BrowserModule
 ],
 providers: [
   {
     provide: ProductService,
     useFactory: (logger: LoggerService) => {
       let dev = Math.random() > 0.5;
       if (dev) {
         return new ProductService(logger);
       } else {
         return new AnotherProductService(logger);
       }
     },
     deps: [LoggerService]
   }
   , LoggerService],
 bootstrap: [AppComponent]
})
export class AppModule {
}

使用deps声明工厂方法需要的参数。

第二个问题,在我们的例子中实例话哪个对象是由一个随机数决定的,在真实情况中我们可能会通过某个变量来控制。这时我们声明一个新的提供器

@NgModule({
  declarations: [
    AppComponent,
    Product1Component,
    Product2Component
  ],
  imports: [
    BrowserModule
  ],
  providers: [
    {
      provide: ProductService,
      useFactory: (logger: LoggerService, isDev) => {
        if (isDev) {
          return new ProductService(logger);
        } else {
          return new AnotherProductService(logger);
        }
      },
      deps: [LoggerService, 'IS_DEV_ENV']
    }
    , LoggerService,
    {
      provide: 'IS_DEV_ENV', useValue: false
    }],
  bootstrap: [AppComponent]
})
export class AppModule {
}

也可以使用值对象

@NgModule({
  declarations: [
    AppComponent,
    Product1Component,
    Product2Component
  ],
  imports: [
    BrowserModule
  ],
  providers: [
    {
      provide: ProductService,
      useFactory: (logger: LoggerService, appConfig) => {
        if (appConfig.isDev) {
          return new ProductService(logger);
        } else {
          return new AnotherProductService(logger);
        }
      },
      deps: [LoggerService, 'APP_CONFIG']
    }
    , LoggerService,
    {
      provide: 'APP_CONFIG', useValue: {isDev:false}
    }],
  bootstrap: [AppComponent]
})
export class AppModule {
}

七、在服务中注入服务

//logger.service.ts
import {Injectable} from "@angular/core";
@Injectable()
export class LoggerService{
  log(message:string){
    console.log(message);
  }
}


//contact.service.ts
import {Injectable} from "@angular/core";
import {LoggerService} from "./logger.service";
@Injectable()//添加装饰器@Injectable()
export class ContactService{
  //构造函数中注入所依赖服务
  constructor(private _logger:LoggerService){}
  getCollections(){
    this._logger.log('获取联系人...')
  }
}


//在组件的providers元数据中注册服务
providers:[LoggerService,ContactService]
  • 只有声明了@Injectable()这个装饰器的服务才可以被注入其他服务。
  • 组件中没有@Injectable()也可在构造函数中注入服务的原因是组件中@Component是@Injectable()的子类。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • 一、什么是依赖注入 控制反转(IoC) 控制反转的概念最早在2004年由Martin Fowler提出,是针对面向...
    Keriy阅读 3,170评论 0 8
  • 版本:Angular 5.0.0-alpha 依赖注入是重要的应用设计模式。它使用得非常广泛,以至于几乎每个人都称...
    soojade阅读 2,985评论 0 3
  • 最近刚刚看完了阎真的《活着之上》,这本仅差一个名次遗憾未拿到矛盾文学奖的心灵读本,用理想与现实的鲜明对照,拷问着学...
    忧郁的神经阅读 334评论 0 0
  • 来北京一年多了,总想写点什么,想说点什么。此时在地铁上,心是累的,身体是疲倦的。 一年前带着梦想揣着满身的激情来到...
    我不告诉你明天的样子阅读 166评论 0 0