Angular 依賴注入

1、什麼是依賴注入

控制反轉(IoC)html

控制反轉的概念最先在2004年由Martin Fowler提出,是針對面向對象設計不斷複雜化而提出的一種設計原則,是利用面向對象編程法則來下降應用耦合的設計模式。編程

IoC強調的是對代碼引用的控制權由調用方轉移到了外部容器,在運行是經過某種方式注入進來,實現了控制反轉,這大大下降了程序之間的耦合度。依賴注入是最經常使用的一種實現IoC的方式,另外一種是依賴查找bootstrap

依賴注入(Dependency Injection)canvas

固然,按照慣例咱們應該舉個例子, 哦對,咱們主要說明的是依賴注入,依賴查找請自行查閱資料。設計模式

假設咱們有一個能作漢堡的設備(HRobot),須要用肉(meat)和一些沙拉(salad)做爲原料,咱們能夠這樣實現:數組

export class HRobot {
    public meat: Meat;
    public salad: Salad;
    constructor() {
        this.meat = new Meat();
        this.salad = new Salad();
    }
    cook() {}
}

看一下好像沒有什麼問題,可能你已經發現,咱們的原材料都是放在機器裏面的,若是咱們想吃別的口味的漢堡恐怕就要去鄉村基了。
爲了能夠吃到別的口味的漢堡,咱們不得不改造一下咱們的HRobot:app

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());

好比,咱們想吃雞肉漢堡,只須要個它一塊雞肉就好:ide

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提供的依賴注入框架,它可讓你伸手就有漢堡吃!

2、 Angular依賴注入

在介紹Angular依賴注入以前,先來理一下三個概念:

  • 注入器(Injector):就想製造工廠,提供了一系列的接口,用於建立依賴對象的實例。

  • 提供商(Provider):用於配置注入器,注入器經過它來建立被依賴對象的實例,Provider把令牌(Token)映射到工廠方法,被依賴的對象就是經過這個方法建立的。

  • 依賴(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須要依賴MeatSalad:

export class Robot {
    //...
    consructor(public meat: Meat, public salad: Salad) {}
    //...
}

固然,看了頭大是應該的,由於上面的東西壓根就不須要本身動手寫,Angular的依賴注入框架已經自動幫咱們完成了(注入器的生成和調用)。

1. 在組件中注入服務
Angular在底層作了大量的初始化工做,這極大地下降了咱們使用依賴注入的成本,如今要完成依賴注入,咱們只須要三步:

  • 經過import導入被依賴的對象服務

  • 在組件中配置注入器。在啓動組件時,Angular會讀取@Component裝飾器裏的providers元數據,它是一個數組,配置了該組件須要使用的全部依賴,Angular的依賴注入框架會根據這個列表去建立對應的示例。

  • 在組件構造函數中聲明須要注入的依賴。注入器會根據構造函數上的聲明,在組件初始化時經過第二步中的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中注入的服務。能夠推斷出在根模塊中注入的服務是擁有最高優先級的,你能夠在任何地方放心使用。

3、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中提供的四中常見的註冊方式:

  • 類Provider(ClassProvider

  • 值Provider(ValueProvider

  • 別名Provider(ExistingProvider

  • 工廠Provider(FactoryProvider

1. 類Provider

類`Provider` 基於令牌(`Token`)指定依賴項,這種方式但是讓依賴被動態指定爲其餘不一樣的具體實現,只要接口不變,對於使用方就是透明的。好比數據渲染服務(`Render`),`Render`服務對上層提供的接口是固定的,卻是底層的渲染方式能夠不一樣:
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`:
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`中設定一個條件,使其可以根據條件返回實例化咱們須要的服務就能夠了。爲了實現這樣的功能咱們能夠在根模塊中這樣注入:
// 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}`);
        }
    }
}

而後在組件構造函數中寫上須要的服務就好。

4、限定方式的依賴注入

想象一場景,你應用中的某個服務的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`中的配置,從而完成注入。
相關文章
相關標籤/搜索