Angular9 依賴注入

2020-07-03 15:01 更新

依賴注入(DI)是一種重要的應用設計模式。 Angular 有自己的 DI 框架,在設計應用時常會用到它,以提升它們的開發(fā)效率和模塊化程度。

依賴,是當類需要執(zhí)行其功能時,所需要的服務或對象。 DI 是一種編碼模式,其中的類會從外部源中請求獲取依賴,而不是自己創(chuàng)建它們。

在 Angular 中,DI 框架會在實例化該類時向其提供這個類所聲明的依賴項。本指南介紹了 DI 在 Angular 中的工作原理,以及如何借助它來讓你的應用更靈活、高效、健壯,以及可測試、可維護。

我們先看一下英雄指南中英雄管理特性的簡化版。這個簡化版不使用 DI,我們將逐步把它轉換成使用 DI 的。

  1. Path:"src/app/heroes/heroes.component.ts" 。

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


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

  1. Path:"src/app/heroes/heroes.component.ts" 。

    import { Component }   from '@angular/core';
    import { HEROES }      from './mock-heroes';


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

  1. Path:"src/app/heroes/hero.ts" 。

    export interface Hero {
      id: number;
      name: string;
      isSecret: boolean;
    }

  1. Path:"src/app/heroes/mock-heroes.ts" 。

    import { Hero } from './hero';


    export const HEROES: Hero[] = [
      { id: 11, isSecret: false, name: 'Dr Nice' },
      { id: 12, isSecret: false, name: 'Narco' },
      { id: 13, isSecret: false, name: 'Bombasto' },
      { id: 14, isSecret: false, name: 'Celeritas' },
      { id: 15, isSecret: false, name: 'Magneta' },
      { id: 16, isSecret: false, name: 'RubberMan' },
      { id: 17, isSecret: false, name: 'Dynama' },
      { id: 18, isSecret: true,  name: 'Dr IQ' },
      { id: 19, isSecret: true,  name: 'Magma' },
      { id: 20, isSecret: true,  name: 'Tornado' }
    ];

HeroesComponent 是頂層英雄管理組件。 它唯一的目的是顯示 HeroListComponent,該組件會顯示一個英雄名字的列表。

HeroListComponent 的這個版本從 HEROES 數(shù)組(它在一個獨立的 "mock-heroes" 文件中定義了一個內存集合)中獲取英雄。

Path:"src/app/heroes/hero-list.component.ts (class)" 。

export class HeroListComponent {
  heroes = HEROES;
}

這種方法在原型階段有用,但是不夠健壯、不利于維護。 一旦你想要測試該組件或想從遠程服務器獲得英雄列表,就不得不修改 HeroesListComponent 的實現(xiàn),并且替換每一處使用了 HEROES 模擬數(shù)據(jù)的地方。

創(chuàng)建和注冊可注入的服務

DI 框架讓你能從一個可注入的服務類(獨立文件)中為組件提供數(shù)據(jù)。為了演示,我們還會創(chuàng)建一個用來提供英雄列表的、可注入的服務類,并把它注冊為該服務的提供者。

同一個文件中放多個類容易讓人困惑。我們通常建議你在單獨的文件中定義組件和服務。

如果你把組件和服務都放在同一個文件中,請務必先定義服務,然后再定義組件。如果在服務之前定義組件,則會在運行時收到一個空引用錯誤。

也可以借助 forwardRef() 方法來先定義組件,就像這個博客中解釋的那樣。

創(chuàng)建可注入的服務類

Angular CLI 可以用下列命令在 "src/app/heroes" 目錄下生成一個新的 HeroService 類。

ng generate service heroes/hero

下列命令會創(chuàng)建 HeroService 的骨架。

Path:"src/app/heroes/hero.service.ts (CLI-generated)" 。

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


@Injectable({
  providedIn: 'root',
})
export class HeroService {
  constructor() { }
}

@Injectable() 是每個 Angular 服務定義中的基本要素。該類的其余部分導出了一個 getHeroes 方法,它會返回像以前一樣的模擬數(shù)據(jù)。(真實的應用可能會從遠程服務器中異步獲取這些數(shù)據(jù),不過這里我們先忽略它,專心實現(xiàn)服務的注入機制。)

Path:"src/app/heroes/hero.service.ts" 。

import { Injectable } from '@angular/core';
import { HEROES } from './mock-heroes';


@Injectable({
  // we declare that this service should be created
  // by the root application injector.
  providedIn: 'root',
})
export class HeroService {
  getHeroes() { return HEROES; }
}

用服務提供者配置注入器

我們創(chuàng)建的類提供了一個服務。@Injectable() 裝飾器把它標記為可供注入的服務,不過在你使用該服務的 provider 提供者配置好 Angular 的依賴注入器之前,Angular 實際上無法將其注入到任何位置。

該注入器負責創(chuàng)建服務實例,并把它們注入到像 HeroListComponent 這樣的類中。 你很少需要自己創(chuàng)建 Angular 的注入器。Angular 會在執(zhí)行應用時為你創(chuàng)建注入器,第一個注入器是根注入器,創(chuàng)建于啟動過程中。

提供者會告訴注入器如何創(chuàng)建該服務。 要想讓注入器能夠創(chuàng)建服務(或提供其它類型的依賴),你必須使用某個提供者配置好注入器。

提供者可以是服務類本身,因此注入器可以使用 new 來創(chuàng)建實例。 你還可以定義多個類,以不同的方式提供同一個服務,并使用不同的提供者來配置不同的注入器。

注入器是可繼承的,這意味著如果指定的注入器無法解析某個依賴,它就會請求父注入器來解析它。 組件可以從它自己的注入器來獲取服務、從其祖先組件的注入器中獲取、從其父 NgModule 的注入器中獲取,或從 root 注入器中獲取。

你可以在三種位置之一設置元數(shù)據(jù),以便在應用的不同層級使用提供者來配置注入器:

  • 在服務本身的 @Injectable() 裝飾器中。

  • NgModule@NgModule() 裝飾器中。

  • 在組件的 @Component() 裝飾器中。

@Injectable() 裝飾器具有一個名叫 providedIn 的元數(shù)據(jù)選項,在那里你可以指定把被裝飾類的提供者放到 root 注入器中,或某個特定 NgModule 的注入器中。

@NgModule()@Component() 裝飾器都有用一個 providers 元數(shù)據(jù)選項,在那里你可以配置 NgModule 級或組件級的注入器。

所有組件都是指令,而 providers 選項是從 @Directive() 中繼承來的。 你也可以與組件一樣的級別為指令、管道配置提供者。

注入服務

HeroListComponent 要想從 HeroService 中獲取英雄列表,就得要求注入 HeroService,而不是自己使用 new 來創(chuàng)建自己的 HeroService 實例。

你可以通過制定帶有依賴類型的構造函數(shù)參數(shù)來要求 Angular 在組件的構造函數(shù)中注入依賴項。下面的代碼是 HeroListComponent 的構造函數(shù),它要求注入 HeroService。

Path:"src/app/heroes/hero-list.component (constructor signature)" 。

constructor(heroService: HeroService)

當然,HeroListComponent 還應該使用注入的這個 HeroService 做一些事情。 這里是修改過的組件,它轉而使用注入的服務。與前一版本并列顯示,以便比較。

//hero-list.component (with DI)


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.getHeroes();
  }
}

//hero-list.component (without DI)


import { Component }   from '@angular/core';
import { HEROES }      from './mock-heroes';


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

必須在某些父注入器中提供 HeroService。HeroListComponent 并不關心 HeroService 來自哪里。 如果你決定在 AppModule 中提供 HeroService,也不必修改 HeroListComponent

注入器樹與服務實例

在某個注入器的范圍內,服務是單例的。也就是說,在指定的注入器中最多只有某個服務的最多一個實例。

應用只有一個根注入器。在 rootAppModule 級提供 UserService 意味著它注冊到了根注入器上。 在整個應用中只有一個 UserService 實例,每個要求注入 UserService 的類都會得到這一個服務實例,除非你在子注入器中配置了另一個提供者。

Angular DI 具有分層注入體系,這意味著下級注入器也可以創(chuàng)建它們自己的服務實例。 Angular 會有規(guī)律的創(chuàng)建下級注入器。每當 Angular 創(chuàng)建一個在 @Component() 中指定了 providers 的組件實例時,它也會為該實例創(chuàng)建一個新的子注入器。 類似的,當在運行期間加載一個新的 NgModule 時,Angular 也可以為它創(chuàng)建一個擁有自己的提供者的注入器。

子模塊和組件注入器彼此獨立,并且會為所提供的服務分別創(chuàng)建自己的實例。當 Angular 銷毀 NgModule 或組件實例時,也會銷毀這些注入器以及注入器中的那些服務實例。

借助注入器繼承機制,你仍然可以把全應用級的服務注入到這些組件中。 組件的注入器是其父組件注入器的子節(jié)點,它會繼承所有的祖先注入器,其終點則是應用的根注入器。 Angular 可以注入該繼承譜系中任何一個注入器提供的服務。

比如,Angular 既可以把 HeroComponent 中提供的 HeroService 注入到 HeroListComponent,也可以注入 AppModule 中提供的 UserService。

測試帶有依賴的組件

基于依賴注入設計一個類,能讓它更易于測試。 要想高效的測試應用的各個部分,你所要做的一切就是把這些依賴列到構造函數(shù)的參數(shù)表中而已。

比如,你可以使用一個可在測試期間操縱的模擬服務來創(chuàng)建新的 HeroListComponent。

Path:"src/app/test.component.ts" 。

const expectedHeroes = [{name: 'A'}, {name: 'B'}]
const mockService = <HeroService> {getHeroes: () => expectedHeroes }


it('should have heroes when HeroListComponent created', () => {
  // Pass the mock to the constructor as the Angular injector would
  const component = new HeroListComponent(mockService);
  expect(component.heroes.length).toEqual(expectedHeroes.length);
});

那些需要其它服務的服務

服務還可以具有自己的依賴。HeroService 非常簡單,沒有自己的依賴。不過,如果你希望通過日志服務來報告這些活動,那么就可以使用同樣的構造函數(shù)注入模式,添加一個構造函數(shù)來接收一個 Logger 參數(shù)。

這是修改后的 HeroService,它注入了 Logger,我們把它和前一個版本的服務放在一起進行對比。

  1. Path:"src/app/heroes/hero.service (v2)" 。

    import { Injectable } from '@angular/core';
    import { HEROES }     from './mock-heroes';
    import { Logger }     from '../logger.service';


    @Injectable({
      providedIn: 'root',
    })
    export class HeroService {


      constructor(private logger: Logger) {  }


      getHeroes() {
        this.logger.log('Getting heroes ...');
        return HEROES;
      }
    }

  1. Path:"src/app/heroes/hero.service (v1)" 。

    import { Injectable } from '@angular/core';
    import { HEROES }     from './mock-heroes';


    @Injectable({
      providedIn: 'root',
    })
    export class HeroService {
      getHeroes() { return HEROES; }
    }

  1. Path:"src/app/logger.service" 。

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


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


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

該構造函數(shù)請求注入一個 Logger 的實例,并把它保存在一個名叫 logger 的私有字段中。 當要求獲取英雄列表時,getHeroes() 方法就會記錄一條消息。

注意,雖然 Logger 服務沒有自己的依賴項,但是它同樣帶有 @Injectable() 裝飾器。實際上,@Injectable() 對所有服務都是必須的。

當 Angular 創(chuàng)建一個構造函數(shù)中有參數(shù)的類時,它會查找有關這些參數(shù)的類型,和供注入使用的元數(shù)據(jù),以便找到正確的服務。 如果 Angular 無法找到參數(shù)信息,它就會拋出一個錯誤。 只有當類具有某種裝飾器時,Angular 才能找到參數(shù)信息。 @Injectable() 裝飾器是所有服務類的標準裝飾器。

裝飾器是 TypeScript 強制要求的。當 TypeScript 把代碼轉譯成 JavaScript 時,一般會丟棄參數(shù)的類型信息。只有當類具有裝飾器,并且 "tsconfig.json" 中的編譯器選項 emitDecoratorMetadatatrue 時,TypeScript 才會保留這些信息。CLI 所配置的 "tsconfig.json" 就帶有 emitDecoratorMetadata: true。

這意味著你有責任給所有服務類加上 @Injectable()。

依賴注入令牌

當使用提供者配置注入器時,就會把提供者和一個 DI 令牌關聯(lián)起來。 注入器維護一個內部令牌-提供者的映射表,當請求一個依賴項時就會引用它。令牌就是這個映射表的鍵。

在簡單的例子中,依賴項的值是一個實例,而類的類型則充當鍵來查閱它。 通過把 HeroService 類型作為令牌,你可以直接從注入器中獲得一個 HeroService 實例。

Path:"src/app/injector.component.ts" 。

heroService: HeroService;

當你編寫的構造函數(shù)中需要注入基于類的依賴項時,其行為也類似。 當你使用 HeroService 類的類型來定義構造函數(shù)參數(shù)時,Angular 就會知道要注入與 HeroService 類這個令牌相關的服務。

Path:"src/app/heroes/hero-list.component.ts" 。

constructor(heroService: HeroService)

很多依賴項的值都是通過類來提供的,但不是全部。擴展的 provide 對象讓你可以把多種不同種類的提供者和 DI 令牌關聯(lián)起來。

可選依賴

HeroService 需要一個記錄器,但是如果找不到它會怎么樣?

當組件或服務聲明某個依賴項時,該類的構造函數(shù)會以參數(shù)的形式接收那個依賴項。 通過給這個參數(shù)加上 @Optional() 注解,你可以告訴 Angular,該依賴是可選的。

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

constructor(@Optional() private logger?: Logger) {
  if (this.logger) {
    this.logger.log(some_message);
  }
}

當使用 @Optional() 時,你的代碼必須能正確處理 null 值。如果你沒有在任何地方注冊過 logger 提供者,那么注入器就會把 logger 的值設置為 null。

@Inject()@Optional() 都是參數(shù)裝飾器。它們通過在需要依賴項的類的構造函數(shù)上對參數(shù)進行注解,來改變 DI 框架提供依賴項的方式。

小結

本節(jié)中你學到了 Angular 依賴注入的基礎知識。 你可以注冊多種提供者,并且知道了如何通過為構造函數(shù)添加參數(shù)來請求所注入的對象(比如服務)。

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號