angular之依赖注入

记得刚开始学习angular时,直接去撸文档,看得我头皮发麻…各种英雄各种组件各种依赖…让我不知如何下手。
但是公司有项目要强行使用angular框架开发呀,没办法,不管懂不懂只能硬着头皮撸代码了,事实证明,angular文档很多东西只有通过实践过后再回来阅读,才能理得清楚它里面的许多概念,所以,本篇文章用于记录一下我对于angular的核心————依赖注入 的学习和理解。

什么是依赖注入?

依赖注入是一种程序的设计模式。在angular中几乎每个地方都有使用它,angular从1升级到2版本删掉了许多的功能,但是把依赖注入保留了下来,肯定是有它的特别之处的。

为什么要使用依赖注入呢?

我们借助angular文档中的例子来说明。

export class Car {

    public engine: Engine;
    public tires: Tires;
    public description = 'No DI';

    constructor() {
        this.engine = new Engine();
        this.tires = new Tires();
    }

    // Method using the engine and tires
    drive() {
        return `${this.description} car with ` +
        `${this.engine.cylinders} cylinders and ${this.tires.make} tires.`;
    }
}

这里定义了一个Car类,通过构造函数初始化设定了类的一些属性,虽然看似没什么问题,但是这个Car类过于脆弱、缺乏弹性并且难以测试。为什么这么说呢?主要原因如下:

  • 在构造函数中产生Engine类实例,若Engine类改变了,那么必须破坏这个Car类(比如Engine类需要传如参数)。(脆弱的原因)
  • 该Cat类不能使用不同的Engine类 (缺乏弹性的原因)
  • 我们在对该Cat类测试时不清楚它的内部依赖了什么(一个类依赖什么就看构建该类实例时需要传什么参数) (难以测试的原因)

那么,我们如果来解决这些问题呢?
知道了原因那么我们就可以对症下药了,解决方法如下:

export class Car {
    public description = 'DI';

    constructor(public engine: Engine, public tires: Tires) { }
    // Method using the engine and tires
    drive() {
        return `${this.description} car with ` +
        `${this.engine.cylinders} cylinders and ${this.tires.make} tires.`;
    }
}

做了什么改变呢?我们把Car类的依赖Engine和Tires移到了构造函数中,Car不用再进行new引擎engine和轮胎tires了,Car需要什么engine和tires我们就传入什么,Car仅仅要做的是使用他们。
那么现在,我们来创造一个Car类实例:

let car = new Car(new Engine(), new Tires());

哈哈,这两个依赖与Car成功解耦了。不管Engine和Tires怎么变,和Car内部没有任何关系。那么比如现在我们用Car使用Engine2类实例:

class Engine2 {
constructor(public cylinders: number) { }
}
// Super car with 12 cylinders and Flintstone tires.
let bigCylinders = 12;
let car = new Car(new Engine2(bigCylinders), new Tires());

目前为止,解决了Car类它的三大问题。若要测试,也是非常方便的,我们知道了它的所有依赖,那么只需传入mock对象就能测试它。

class MockEngine extends Engine { cylinders = 8; }
class MockTires  extends Tires  { make = 'YokoGoodStone'; }

// Test car with 8 cylinders and YokoGoodStone tires.
let car = new Car(new MockEngine(), new MockTires());

虽然,目前Car的问题解决了,但是它的消费者(即创建Car实例时)又出现了问题,什么问题呢?Car的消费者每次希望获得一个Car,都要创建Car,Engine,Tires(当然,实际情况还可能更多)。这对于消费者来说,是非常痛苦的事。所以,我们需要某种机制来帮消费者把这麻烦事给干了。
有一种方法就是工厂模式:

import { Engine, Tires, Car } from './car';

// BAD pattern!
export class CarFactory {
    createCar() {
        let car = new Car(this.createEngine(), this.createTires());
        car.description = 'Factory';
        return car;
    }

    createEngine() {
        return new Engine();
    }

    createTires() {
        return new Tires();
    }
}

当消费者需要一辆车时,只需创建CarFactory的实例,调取createCar方法即可,目前来看,确实可以帮助消费者解决问题,但当应用规模变大后,多个依赖存在着相互联系,那么会导致这个类变成一个巨型蜘蛛网,它将难以控制和维护。
从消费者的角度,如果我想要一辆Car,那么我就创建一辆Car,它依赖什么我根本不用管,如果能这样真是太好了。
所以,这就是依赖注入框架存在的原因!
想象一下,现在如果有一个注入器叫injector的东东,它就像魔法棒一样,我要什么它就能给我变什么出来,现在,我想要一辆Car:

let car = injector.get(Car);

是不是棒棒的!现在Car和消费者只要简单地请求想要什么,注入器就会交付给他们。
我们知道了什么是依赖注入,以及它的优点。那么下面我们来看看它在angular中是怎么实现的吧。

Angular依赖注入

在angular中,可显示创建注入器或者隐式创建注入器。我们先看一下angular中通常使用的隐式创建注入器,是如何工作的。

隐式创建注入器

在入口文件main.ts中,你会看到这么一段代码:

platformBrowserDynamic().bootstrapModule(AppModule);

而这段代码的执行,也就意味着Angular启动时自动为我们创建了应用级注入器。什么意思呢?就是说,在angular中,我们在组件里,只需要注入提供商(什么叫提供商?这里先简单这样说,比如Engine类(轮胎产商)要为Car类(汽车制造厂)提供一个engine(轮胎)实例,那么这个Engine类就是Car类的engine提供商)来配置注入器,那么这些提供商就能为应用提供服务。而注入提供商可以在两个地方进行注入:

  • NgModule中
  • 应用组件中
在NgModule中注入提供商

AppModule.ts:

@NgModule({
imports: [
    BrowserModule
],
declarations: [
    AppComponent,
    CarComponent,
    HeroesComponent,
/* . . . */
],
providers: [
    UserService,
    { provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
],
bootstrap: [ AppComponent ]
})
export class AppModule { }

通过providers可注册一系列的提供商。

在组件中注册提供商

AppComponent.ts:

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

import { HeroService }        from './hero.service';

@Component({
    selector: 'app-heroes',
    providers: [HeroService],
    template: `
        <h2>Heroes</h2>
        <app-hero-list></app-hero-list>
    `
})
export class HeroesComponent { }

组件也是通过providers对HeroService提供商进行注册。

现在,你可能疑惑在这俩者注册有什么区别呢?不都是注册吗?下面讲说明两者的区别。

关于应用级的注入器

当angular启动后,它已经默默的创建了应用级的注入器,而什么是应用级注入器?它有什么特点呢?

首先,我们知道,angular在创建组件时,会形成一个组件树,而与此同时,应用级注入器也产生了注入作用域。比如说,现在我在NgModule或根组件中注入了一个提供商,那么根组件下的所有子组件都可以使用这个提供商,我们暂且把这个提供商称之为全局提供商吧。而现在,我又在子组件中注入了一个相同的提供商,那么,当前组件就会优先使用这个提供商,不再会使用全局提供商了。组件每次发现自己存在其它的依赖注入时,就会以冒泡的形式从自己向根组件及NgModule去查找提供商,找到符合的提供商就停止,没找到angular则会报错 No providers!。
而在一个注入器的范围内,依赖都是单例的。什么意思呢?比如现在在Ngmodule中注册了一个全局提供商HeroService,而在子组件HeroesComponent,和子组件HeroListComponent都依赖于这个提供商,那么这两个组件使用的是同一个实例,即当子组件HeroesComponent对这个实例的某个属性值进行修改了,那么子组件HeroListComponent中的这个属性值也会随之修改。相对于其它框架来说,这对于困难的组件通信是非常方便的。

hero.service.ts

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

import { HEROES }     from './mock-heroes';

@Injectable()
export class HeroService {

constructor() {  }
    heroes: HEROES[]; // 当heroes被改变时,子组件的heroes也会改变
    getHeroesList() {
        return this.heroes;
    }
    addHeroes() {
        this.heroes.push(HEROES);
    }
}

NgModule.ts根模块

import { HeroService } from './hero.service';
@NgModule({
imports: [
    BrowserModule
],
declarations: [
    AppComponent,
    CarComponent,
    HeroesComponent,
/* . . . */
],
providers: [
    HeroService
],
bootstrap: [ AppComponent ]
})
export class AppModule { }

子组件HeroListComponent.ts

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

import { Hero }        from './hero';
import { HeroService } from './hero.service';

@Component({
    selector: 'app-hero-list',
    template: `
    <div *ngFor="let hero of heroes">
        {{hero.id}} - {{hero.name}}
    </div>
    `
    })
    export class HeroListComponent {
    heroes: Hero[];

    constructor(heroService: HeroService) {
        this.heroes = heroService.getHeroesList();
    }
}

子组件HeroesComponent.ts

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

import { Hero }        from './hero';
import { HeroService } from './hero.service';

@Component({
    selector: 'app-hero-list',
    template: `
    <div>
        <button (click)="addHeroes()">添加英雄</button>
    </div>
    `
    })
    export class HeroListComponent {
    heroes: Hero;

    constructor(heroService: HeroService) {}
    addHeroes() {
        this.heroService.addHeroes();
    }
}
在NgModule中还是根组件中注入提供商?

由于在在NgModule和根组件中注册提供商产生的效果是一样的,那么我应该把全局提供商添加到根模块AppModule中还是根组件AppComponent中呢?本篇文章不作详细解释,详情可看文档NgModule FAQ

显示创建注入器

injector = ReflectiveInjector.resolveAndCreate([Car, Engine, Tires]);
let car = injector.get(Car);

在必要时,我们可以这样写,但是通常情况下,这种方式是不推荐的。

服务

在例子中,我们用了HeroService这个例子,它可为我们提供组Hero服务提供商。我们再来仔细看一下它。

HeroService.ts

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

import { HEROES }     from './mock-heroes';

@Injectable()
export class HeroService {

constructor() {  }
    heroes: HEROES[]; // 当heroes被改变时,子组件的heroes也会改变
    getHeroesList() {
        return this.heroes;
    }
    addHeroes() {
        this.heroes.push(HEROES);
    }
}

我想你可能会疑惑这个@Injectable()装饰器的作用是什么?文档中这样说的:”@Injectable()标识一个类可以被注入器实例化。通常,在试图实例化没有被标识为@Injectable()的类时,注入器会报错。”,这句话是什么意思呢,就是说只要有@Injectable装饰器,那么你就可以在当前类中注入其它依赖,如果没有,那么注入其它依赖时就会报错!而在上面的HeroService类中,没有任何其它的依赖注入,那么其实可以不用标示@Injectable()。若现在需要注入一个logger服务。

logger.service.ts

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

@Injectable()
export class Logger {
    logs: string[] = []; // capture logs for testing

    log(message: string) {
        this.logs.push(message);
        console.log(message);
    }
}

那么添加 providers: [Logger] 把它注册到HeroService中即可,而此时HeroService必须要有@Injectable()标识。而angular建议为每个服务类都添加@Injectable(),包括那些没有依赖严格来说并不需要它的。因为:

  • 面向未来: 没有必要记得在后来添加依赖的时候添加 @Injectable()。
  • 一致性:所有的服务都遵循同样的规则,不需要考虑为什么某个地方少了一个。

而对于使用@Component装饰的组件类,为啥可以进行依赖注入呢?实际上对于@Component(@Directive和@Pipe也一样),它是 Injectable 的子类型。正是这些@Injectable()装饰器是把一个类标识为注入器实例化的目标。

最后

对于注入器的提供商们, 还有各种变化形式,在下篇将继续详细讲解。