Angular 中的依赖注入link
Angular 中的依賴注入link
依賴注入(DI)是一種重要的應用設(shè)計模式。 Angular 有自己的 DI 框架,在設(shè)計應用時常會用到它,以提升它們的開發(fā)效率和模塊化程度。
依賴,是當類需要執(zhí)行其功能時,所需要的服務或?qū)ο蟆?DI 是一種編碼模式,其中的類會從外部源中請求獲取依賴,而不是自己創(chuàng)建它們。
在 Angular 中,DI 框架會在實例化該類時向其提供這個類所聲明的依賴項。本指南介紹了 DI 在 Angular 中的工作原理,以及如何借助它來讓你的應用更靈活、高效、健壯,以及可測試、可維護。
你可以運行本章這個范例應用的現(xiàn)場演練?/?下載范例。
我們先看一下英雄指南中英雄管理特性的簡化版。這個簡化版不使用 DI,我們將逐步把它轉(zhuǎn)換成使用 DI 的。
src/app/heroes/heroes.component.ts
src/app/heroes/hero-list.component.ts
src/app/heroes/hero.ts
src/app/heroes/mock-heroes.ts
content_copyimport { 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?文件中定義了一個內(nèi)存集合)中獲取英雄。
src/app/heroes/hero-list.component.ts (class)
content_copyexport class HeroListComponent {heroes = HEROES; }這種方法在原型階段有用,但是不夠健壯、不利于維護。 一旦你想要測試該組件或想從遠程服務器獲得英雄列表,就不得不修改?HeroesListComponent?的實現(xiàn),并且替換每一處使用了?HEROES?模擬數(shù)據(jù)的地方。
創(chuàng)建和注冊可注入的服務
DI 框架讓你能從一個可注入的服務類(獨立文件)中為組件提供數(shù)據(jù)。為了演示,我們還會創(chuàng)建一個用來提供英雄列表的、可注入的服務類,并把它注冊為該服務的提供商。
在同一個文件中放多個類容易讓人困惑。我們通常建議你在單獨的文件中定義組件和服務。
如果你把組件和服務都放在同一個文件中,請務必先定義服務,然后再定義組件。如果在服務之前定義組件,則會在運行時收到一個空引用錯誤。
也可以借助?forwardRef()?方法來先定義組件,就像這個博客中解釋的那樣。
你還可以使用前向引用來打破循環(huán)依賴,參見?DI 一章中的例子。
創(chuàng)建可注入的服務類
Angular CLI?可以用下列命令在?src/app/heroes?目錄下生成一個新的?HeroService?類。
content_copyng generate service heroes/hero下列命令會創(chuàng)建?HeroService?的骨架。
src/app/heroes/hero.service.ts (CLI-generated)
content_copyimport { Injectable } from '@angular/core';@Injectable({providedIn: 'root', }) export class HeroService {constructor() { } }@Injectable()?是每個 Angular 服務定義中的基本要素。該類的其余部分導出了一個?getHeroes?方法,它會返回像以前一樣的模擬數(shù)據(jù)。(真實的應用可能會從遠程服務器中異步獲取這些數(shù)據(jù),不過這里我們先忽略它,專心實現(xiàn)服務的注入機制。)
src/app/heroes/hero.service.ts
content_copyimport { 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è)置元數(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?實例。
你可以通過制定帶有依賴類型的構(gòu)造函數(shù)參數(shù)來要求 Angular 在組件的構(gòu)造函數(shù)中注入依賴項。下面的代碼是?HeroListComponent?的構(gòu)造函數(shù),它要求注入?HeroService。
src/app/heroes/hero-list.component (constructor signature)
content_copyconstructor(heroService: HeroService)當然,HeroListComponent?還應該使用注入的這個?HeroService?做一些事情。 這里是修改過的組件,它轉(zhuǎn)而使用注入的服務。與前一版本并列顯示,以便比較。
hero-list.component (with DI)
hero-list.component (without DI)
content_copyimport { 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();} }必須在某些父注入器中提供?HeroService。HeroListComponent?并不關(guān)心?HeroService?來自哪里。 如果你決定在?AppModule?中提供?HeroService,也不必修改?HeroListComponent。
注入器樹與服務實例
在某個注入器的范圍內(nèi),服務是單例的。也就是說,在指定的注入器中最多只有某個服務的最多一個實例。
應用只有一個根注入器。在?root?或?AppModule?級提供?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è)計一個類,能讓它更易于測試。 要想高效的測試應用的各個部分,你所要做的一切就是把這些依賴列到構(gòu)造函數(shù)的參數(shù)表中而已。
比如,你可以使用一個可在測試期間操縱的模擬服務來創(chuàng)建新的?HeroListComponent。
src/app/test.component.ts
content_copyconst 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 wouldconst component = new HeroListComponent(mockService);expect(component.heroes.length).toEqual(expectedHeroes.length); });欲知詳情,參見測試一章。
那些需要其它服務的服務
服務還可以具有自己的依賴。HeroService?非常簡單,沒有自己的依賴。不過,如果你希望通過日志服務來報告這些活動,那么就可以使用同樣的構(gòu)造函數(shù)注入模式,添加一個構(gòu)造函數(shù)來接收一個?Logger?參數(shù)。
這是修改后的?HeroService,它注入了?Logger,我們把它和前一個版本的服務放在一起進行對比。
src/app/heroes/hero.service (v2)
src/app/heroes/hero.service (v1)
src/app/logger.service
content_copyimport { 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;} }該構(gòu)造函數(shù)請求注入一個?Logger?的實例,并把它保存在一個名叫?logger?的私有字段中。 當要求獲取英雄列表時,getHeroes()?方法就會記錄一條消息。
注意,雖然?Logger?服務沒有自己的依賴項,但是它同樣帶有?@Injectable()?裝飾器。實際上,@Injectable()?對所有服務都是必須的。
當 Angular 創(chuàng)建一個構(gòu)造函數(shù)中有參數(shù)的類時,它會查找有關(guān)這些參數(shù)的類型,和供注入使用的元數(shù)據(jù),以便找到正確的服務。 如果 Angular 無法找到參數(shù)信息,它就會拋出一個錯誤。?只有當類具有某種裝飾器時,Angular 才能找到參數(shù)信息。?@Injectable()?裝飾器是所有服務類的標準裝飾器。
裝飾器是 TypeScript 強制要求的。當 TypeScript 把代碼轉(zhuǎn)譯成 JavaScript 時,一般會丟棄參數(shù)的類型信息。只有當類具有裝飾器,并且?tsconfig.json?中的編譯器選項?emitDecoratorMetadata?為?true?時,TypeScript 才會保留這些信息。CLI 所配置的?tsconfig.json?就帶有?emitDecoratorMetadata: true。
這意味著你有責任給所有服務類加上?@Injectable()。
依賴注入令牌
當使用提供商配置注入器時,就會把提供商和一個?DI 令牌關(guān)聯(lián)起來。 注入器維護一個內(nèi)部令牌-提供商的映射表,當請求一個依賴項時就會引用它。令牌就是這個映射表的鍵。
在簡單的例子中,依賴項的值是一個實例,而類的類型則充當鍵來查閱它。 通過把?HeroService?類型作為令牌,你可以直接從注入器中獲得一個?HeroService?實例。
src/app/injector.component.ts
content_copyheroService: HeroService;當你編寫的構(gòu)造函數(shù)中需要注入基于類的依賴項時,其行為也類似。 當你使用?HeroService?類的類型來定義構(gòu)造函數(shù)參數(shù)時,Angular 就會知道要注入與?HeroService?類這個令牌相關(guān)的服務。
src/app/heroes/hero-list.component.ts
content_copyconstructor(heroService: HeroService)很多依賴項的值都是通過類來提供的,但不是全部。擴展的?provide?對象讓你可以把多種不同種類的提供商和 DI 令牌關(guān)聯(lián)起來。
-
欲知詳情,參見不同種類的提供商。
可選依賴
HeroService?需要一個記錄器,但是如果找不到它會怎么樣?
當組件或服務聲明某個依賴項時,該類的構(gòu)造函數(shù)會以參數(shù)的形式接收那個依賴項。 通過給這個參數(shù)加上?@Optional()?注解,你可以告訴 Angular,該依賴是可選的。
content_copyimport { Optional } from '@angular/core'; content_copyconstructor(@Optional() private logger: Logger) {if (this.logger) {this.logger.log(some_message);} }當使用?@Optional()?時,你的代碼必須能正確處理 null 值。如果你沒有在任何地方注冊過 logger 提供商,那么注入器就會把?logger?的值設(shè)置為 null。
@Inject()?和?@Optional()?都是參數(shù)裝飾器。它們通過在需要依賴項的類的構(gòu)造函數(shù)上對參數(shù)進行注解,來改變 DI 框架提供依賴項的方式。
欲知詳情,參見多級注入器。
小結(jié)
本頁中你學到了 Angular 依賴注入的基礎(chǔ)知識。 你可以注冊多種提供商,并且知道了如何通過為構(gòu)造函數(shù)添加參數(shù)來請求所注入的對象(比如服務)。
在以下頁面中可以深入了解 Angular DI 體系的能力及高級特性:
-
要深入了解嵌套注入器,參見多級依賴注入
-
到?DI 令牌與提供商中學習更多知識。
-
依賴注入實戰(zhàn)中講了一些你能用 DI 做的一些有意思的事。
總結(jié)
以上是生活随笔為你收集整理的Angular 中的依赖注入link的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring IOC实现
- 下一篇: 1. Nest Js