依賴注入(DI – Dependency Injection)是一種重要的應用設計模式。Angular裏面也有自己的DI框架,在設計應用時經常會用到它,它可以我們的開發效率和模塊化程度。
依賴,是當類需要執行其功能時,所需要的服務或對象。DI是一種編碼模式,其中的類會從外部源中請求獲取依賴,而不需要我們自己創建它們。
Angular系統中通過在類上添加@Injectable裝飾器來告訴系統這個類(服務)是可注入的。當然了這僅僅只是告訴Angular系統這個類(服務)類是可注入的。但是這個服務可以在哪裏使用,由誰提供,就得靠注入器和提供商來一起確定了。
-
注入器: 注入器負責服務實例的創建,並把它們注入到你想要注入的類中。從而確定服務的使用範圍和服務的生命週期。
-
提供商: 服務由誰提供。Angular本身沒法自動判斷你是打算自行創建服務類的實例,還是等注入器來創建它。如果想通過注入器來創建,必須在每個注入器裏面爲每個服務指定服務提供商。
一 注入器(Injector)
Angular依賴注入中的注入器(Injector),用來管理服務。包括服務的創建,服務的獲取。
Angular依賴注入系統中的注入器(Injector)是多級的。實際上,應用程序中有一個與組件樹平行的注入器樹。你可以在組件樹中的任何級別上重新配置注入器,注入提供商。
還有一點要特別注意,Angular注入器是冒泡機制的。當一個組件申請獲得一個依賴時,Angular先嚐試用該組件自己的注入器來滿足它。如果該組件的注入器沒有找到對應的提供商,它就把這個申請轉給它父組件的注入器來處理。如果當前注入器也無法滿足這個申請,它就繼續轉給它在注入器樹中的父注入器。這個申請繼續往上冒泡—直到Angular找到一個能處理此申請的注入器或者超出了組件樹中的祖先位置爲止。如果超出了組件樹中的祖先還未找到,Angular就會拋出一個錯誤。
所有所有的一切最終都是服務組件的。每個組件的注入器其實包含兩部分:組件本身注入器的,組件所在NgMoudle對應的注入器。
在我們的Angular系統中我們可以認爲NgModule是一個注入器,Component也是一個注入器。
1.1 NgMoudle(模塊)級注入器
NgMoudle(模塊)級注入器會告訴Angualr系統把服務作用在NgModule上。這個服務器可以在這個NgModule範圍下所有的組件上使用。NgModule級注入服務有兩種方式:一個是在 @NgModule()的providers元數據中指定、另一種是直接在@Injectable()的providedIn選項中指定模塊類。
NgMoudle級注入器兩種方式:@NgModule() providers 元數據中指定、或者直接在@Injectable() 的 providedIn 選項中指定某個模塊類。
1.1.1 通過@NgModule()的providers將服務注入到NgModule中
通過@NgModule()的providers將服務注入到NgModule中,限制服務只能在當前NgModule裏面使用。
export interface NgModule {
...
/**
* 本模塊需要創建的服務。這些服務可以在本模塊的任何地方使用。
* NgModule我們可以認爲是注入器,Provider是提供商。注入器通過提供商的信息來創建服務
*/
providers?: Provider[];
...
}
關於Provider(提供商)更加具體的用戶我們會在下文做詳細的解釋。
比如如下的代碼我們先定義一個NgmoduleProvidersService服務類。當前類只是添加了@Injectable()告訴Angular系統這是一個可注入的服務。
import {Injectable} from '@angular/core';
/**
* 我們將在NgmoduleProvidersModule中注入該服務。
* 然後在NgmoduleProvidersComponent裏面使用該服務
*/
@Injectable()
export class NgmoduleProvidersService {
constructor() {
}
// TODO:其他邏輯
}
接下來,想在NgmoduleProvidersModule模塊裏面所有的地方使用該服務。很簡單,我們在@NgModule元數據providers裏面指定這個類NgmoduleProvidersService就好了。(該服務的TOKEN是NgmoduleProvidersService,提供商也是他自己。關於提供商更加具體的用法,我們會在下文詳細講解)
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {NgmoduleProvidersComponent} from './ngmodule-providers.component';
import {NgmoduleProvidersRoutingModule} from './ngmodule-providers-routing.module';
import {NgmoduleProvidersService} from './ngmodule-providers.service';
@NgModule({
declarations: [
NgmoduleProvidersComponent
],
providers: [
NgmoduleProvidersService,
],
imports: [
CommonModule,
NgmoduleProvidersRoutingModule
]
})
export class NgmoduleProvidersModule {
}
1.1.2 通過@Injectable()的providedIn將服務注入到NgModule中
@Injectable()裝飾器裏面的元數據providedIn也可以直接指定NgModue。來告知服務可以在哪裏使用。providedIn的值可以有三種:一種是Type也是NgModule、一種是字符串’root’、一種是null。
export interface InjectableDecorator {
/**
* providedIn有三種值:Type<any>、 ‘root’、 null
* Type<any>指的是NgModule
*/
(): TypeDecorator;
(options?: {
providedIn: Type<any> | 'root' | null;
} & InjectableProvider): TypeDecorator;
new (): Injectable;
new (options?: {
providedIn: Type<any> | 'root' | null;
} & InjectableProvider): Injectable;
}
當providedIn是null的時候。咱們僅僅是告訴了系統這個類是可注入的。在其他的地方還使用不了。如果想使用需要在NgModule裝飾器或者Component裝飾器裏面的元數據providers中指定。
1.1.2.1 providedIn: ‘root’
providedIn: ‘root’。咱們可以簡單的認爲root字符串就代表頂級AppModule。表明當前服務可以在整個Angular應用裏面使用。而且在整個Angular應用中只有一個服務實例。
比如如下的代碼我們定義一個StartupService服務類。 providedIn: ‘root’。則
StartupService這個類當前項目的任務地方注入使用。而且都是同一份實例對象。
import {Injectable} from '@angular/core';
/**
* StartupService可以在系統的任務地方使用
*/
@Injectable({
providedIn: 'root'
})
export class StartupService {
constructor() {
}
// TODO: 其他邏輯
}
1.1.2.2 providedIn: NgModule
providedIn: NgModule。通過providedIn直接指定一個NgModule。讓當前服務只能在這個指定的NgModule裏面使用。
而且providedIn: NgModule這種情況是可以搖樹優化。只要在服務本身的 @Injectable() 裝飾器中指定提供商,而不是在依賴該服務的NgModule或組件的元數據中指定,你就可以製作一個可搖樹優化的提供商。當前前提是這個NgModule是懶加載的。
搖樹優化是指一個編譯器選項,意思是把應用中未引用過的代碼從最終生成的包中移除。如果提供商是可搖樹優化的,Angular編譯器就會從最終的輸出內容中移除應用代碼中從未用過的服務。 這會顯著減小你的打包體積。
providedIn: NgModule使用的時候有一個特別要特別注意的地方。舉個例子比如我們想在NgmoduleProvidersModule模塊中使用NgmoduleProviderInModuleService服務。如下的寫法是不對的。
import { Injectable } from '@angular/core';
import {NgmoduleProvidersModule} from './ngmodule-providers.module';
@Injectable({
providedIn: NgmoduleProvidersModule
})
export class NgmoduleProviderInModuleService {
constructor() { }
}
編譯的時候會拋出一個警告信息,編譯不過。
WARNING in Circular dependency detected:
爲了解決這個異常信息,讓代碼能正常編譯,我們需要藉助一個NgModule(NgmoduleProvidersResolveModule名字你隨便來)來過渡下。這個過渡NgModule賦值給providedIn。最後在我們真正想使用該服務的NgModule裏面imports這個過渡NgModule。說的有點繞來繞去的。我們直接用代碼來說明。
// 需要在模塊NgmoduleProvidersModule裏面使用的服務NgmoduleProviderInModuleService
import {Injectable} from '@angular/core';
import {NgmoduleProvidersResolveModule} from './ngmodule-providers-resolve.module';
/**
* providedIn中直接指定了當前服務可以在哪個模塊使用
* 特別說明:我們想在NgmoduleProvidersModule模塊裏面使用該服務,
* 如果providedIn直接寫NgmoduleProvidersModule,會報編譯錯誤,
* 所以我們定義了一箇中間模塊NgmoduleProvidersResolveModule,
* 然後在NgmoduleProvidersModule裏面引入了NgmoduleProvidersResolveModule。
*
* NgmoduleProvidersResolveModule相當於一個過渡的作用
*/
@Injectable({
providedIn: NgmoduleProvidersResolveModule
})
export class NgmoduleProviderInModuleService {
constructor() {
}
// TODO: 其他邏輯
}
// 過渡NgModule NgmoduleProvidersResolveModule
import {NgModule} from '@angular/core';
/**
* providedIn: NgModule的時候NgModule不能直接寫對應的NgModule,
* 需要一個過渡的NgModule。否則編譯報錯:WARNING in Circular dependency detected
*/
@NgModule({
})
export class NgmoduleProvidersResolveModule {
}
// NgmoduleProvidersModule 服務將在該模塊裏面使用。
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {NgmoduleProvidersComponent} from './ngmodule-providers.component';
import {NgmoduleProvidersRoutingModule} from './ngmodule-providers-routing.module';
import {NgmoduleProvidersService} from './ngmodule-providers.service';
import {NgmoduleProvidersResolveModule} from './ngmodule-providers-resolve.module';
@NgModule({
declarations: [
NgmoduleProvidersComponent
],
providers: [
NgmoduleProvidersService,
],
imports: [
CommonModule,
/**
* 導入了過渡的NgModule
*/
NgmoduleProvidersResolveModule,
NgmoduleProvidersRoutingModule
]
})
export class NgmoduleProvidersModule {
}
1.2 Component(組件)級注入器
組件級注入器,每個組件也是一個注入器。通過在組件級注入器中注入服務。這樣該組件實例或其下級組件實例都可以使用這個服務(當然我們也可以設置只在當前組件使用,子組件不能使用。這個就涉及到viewProviders和providers的區別了)。組件注入器提供的服務具有受限的生命週期。該組件的每個新實例都會獲得自己的一份服務實例。當銷燬組件實例時,服務實例也會被同時銷燬。所以組件級別的服務和組件是綁定在一起的。一起創建一起消失。
我們通過一個簡單的實例來看看組件級注入器的使用。
首先定義一個ComponentInjectService服務。
import { Injectable } from '@angular/core';
/**
* 當前服務在組件裏面使用,會在需要使用的組件裏面注入
*/
@Injectable()
export class ComponentInjectService {
constructor() { }
}
然後在組件裏面注入
import {ComponentInjectService} from './component-inject.service';
@Component({
selector: 'app-ngmodule-providers',
templateUrl: './ngmodule-providers.component.html',
styleUrls: ['./ngmodule-providers.component.less'],
providers: [ComponentInjectService], // providers提供的服務在當前組件和子組件都可以使用
// viewProviders: [ComponentInjectService], // viewProviders提供的服務在當前組件使用
})
export class NgmoduleProvidersComponent implements OnInit {
constructor(private service: ComponentInjectService) {
}
ngOnInit() {
}
}
二,提供商(Provider)
上面所有的實例代碼,咱們往注入器裏面注入服務的時候,使用的是最簡單的一種方式TypeProvider,也是咱們用的最多的一種方式。不管是@NgModule裝飾器裏面還是@Component裝飾器裏面。providers元數據裏面都是直接寫了服務類。類似如下的代碼。
@NgModule({
...
providers: [
NgmoduleProvidersService,
],
...
})
上面代碼中的providers對象是一個Provider(提供商)數組(當前注入器需要注入的依賴對象),在注入器中注入服務時咱們還必須指定這些提供商,否則注入器就不知道怎麼來創建此服務。Angular系統中我們通過Provider來描述與Token相關聯的依賴對象的創建方式。
簡而言之Provider是用來描述與Token關聯的依賴對象的創建方式。當我們使用Token向DI系統獲取與之相關連的依賴對象時,DI 會根據已設置的創建方式,自動的創建依賴對象並返回給使用者。中間過程我們不需要過。我們只需要知道哪個Token對應哪個(或者哪些)服務就好了。通過Token來獲取到對應的服務。所以關於Povider我們重點需要知道以下兩個東西:Token,Token對應對象的創建方式。
2.1 Povider Token
Token的作用是用來標識依賴對象的,Token值可以是Type、InjectionToken、OpaqueToken類的實例或字符串。通常不推薦使用字符串,因爲如果使用字符串存在命名衝突的可能性比較高。
你可以簡單的認爲Token是依賴對象的key。在我們需要使用依賴對象的時候我們可以通過這個key找到依賴對象。
2.2 對象的創建方式
給出了依賴對象的創建方式,注入器才能知道怎麼去創建對象。Provider有如下幾種方式:TypeProvider ,ValueProvider,ClassProvider,ConstructorProvider, ExistingProvider,FactoryProvider,any[]。
export declare type Provider = TypeProvider | ValueProvider | ClassProvider | ConstructorProvider | ExistingProvider | FactoryProvider | any[];
ConstructorProvider這種方式,咱們就不考慮了,我是在是沒找到這種方式的使用場景。
2.2.1 TypeProvider
export interface TypeProvider extends Type<any> {
}
TypeProvider用於告訴Injector(注入器),使用給定的Type創建對象,並且Token也是給定的Type。這也是我們用的最多的一種方式。比如如下。就是採用的TypeProvider方式。
@NgModule({
...
providers: [NgmoduleProvidersService], // NgmoduleProvidersService是我們定義的服務,TypeProvider方式
})
2.2.2 ClassProvider
ClassProvider用於告訴Injector(注入器),useClass指定的Type創建的對應對象就是Token對應的對象。
export interface ClassSansProvider {
/**
* token生成對象對應的class.
* 用該class生成服務對象
*/
useClass: Type<any>;
}
export interface ClassProvider extends ClassSansProvider {
/**
* 用於設置與依賴對象關聯的Token值,Token值可能是Type、InjectionToken、OpaqueToken的實例或字符串
*/
provide: any;
/**
* 用於標識是否multiple providers,若是multiple類型,則返回與Token關聯的依賴對象列表
* 簡單來說如果multi是true的話,通過provide(Token)獲取的依賴對象是一個列表。
* 同一個Token可以注入多個服務
*/
multi?: boolean;
}
簡單使用
export const TOKEN_MODULE_CLASS_PROVIDER = new InjectionToken<any>('TOKEN_MODULE_CLASS_PROVIDER');
// ModuleClassProviderService類是我們依賴對象
@NgModule({
...
providers: [
{
provide: TOKEN_MODULE_CLASS_PROVIDER, useClass: ModuleClassProviderService
}
],
...
})
export class ClassProviderModule {
}
2.2.3 ValueProvider
ValueProvider用於告訴Injector(注入器),useValue指定的值(可以是具體的對象也可以是string ,number等等之類的值)就是Token依賴的對象。
export interface ValueSansProvider {
/**
* 需要注入的值
*/
useValue: any;
}
export interface ValueProvider extends ValueSansProvider {
/**
* 用於設置與依賴對象關聯的Token值,Token值可能是Type、InjectionToken、OpaqueToken的實例或字符串
*/
provide: any;
/**
* 用於標識是否multiple providers,若是multiple類型,則返回與Token關聯的依賴對象列表
* 簡單來說如果multi是true的話,通過provide(Token)獲取的依賴對象是一個列表。
* 同一個Token可以注入多個服務
*/
multi?: boolean;
}
簡單實例。
export const TOKEN_MODULE_CONFIG = new InjectionToken<Config>('TOKEN_MODULE_CONFIG');
/**
* Config是我們自定義的一個配置對象
*/
const config = new Config();
config.version = '1.1.2';
@NgModule({
...
providers: [
{provide: TOKEN_MODULE_CONFIG, useValue: config},
],
...
})
export class ValueProviderModule {
}
2.2.4 FactoryProvider
FactoryProvider 用於告訴 Injector (注入器),通過調用 useFactory對應的函數,返回Token對應的依賴對象。
export interface FactorySansProvider {
/**
* 用於創建對象的工廠函數
*/
useFactory: Function;
/**
* 依賴對象列表(你也可以簡單的認爲是創建對象構造函數裏面需要的依賴對象)
*/
deps?: any[];
}
export interface FactoryProvider extends FactorySansProvider {
/**
* 用於設置與依賴對象關聯的Token值,Token值可能是Type、InjectionToken、OpaqueToken的實例或字符串
*/
provide: any;
/**
* 用於標識是否multiple providers,若是multiple類型,則返回與Token關聯的依賴對象列表
* 簡單來說如果multi是true的話,通過provide(Token)獲取的依賴對象是一個列表。
* 同一個Token可以注入多個服務
*/
multi?: boolean;
}
useFactory對應一個函數,該函數需要的對象通過deps提供,deps是一個Token數組。
// TOKEN
export const TOKEN_FACTORY_MODULE_DEPS = new InjectionToken<ModuleFactoryProviderService>('TOKEN_FACTORY_MODULE_DEPS');
export const TOKEN_FACTORY_MODULE = new InjectionToken<ModuleFactoryProviderService>('TOKEN_FACTORY_MODULE');
/**
* 創建ModuleFactoryProviderService對象,
* 該對象依賴另一個服務,通過deps提供
*/
function moduleServiceFactory(initValue) {
return new ModuleFactoryProviderService(initValue);
}
@NgModule({
...
providers: [
{ // 創建TOKEN_FACTORY_MODULE對應的服務時候,需要依賴的值
provide: TOKEN_FACTORY_MODULE_DEPS,
useValue: 'initValue'
},
{
provide: TOKEN_FACTORY_MODULE,
useFactory: moduleServiceFactory,
deps: [TOKEN_FACTORY_MODULE_DEPS]
}
],
...
})
export class FactoryProviderModule {
}
2.2.5 ExistingProvider
ExistingProvider用於告訴Injector(注入器),想獲取Token(provide)對應的對象的時候,使用useExisting(Token)對應的對象。
一定要記住useExisting對應的值也是一個Token。
export interface ExistingSansProvider {
/**
* 已經存在的 `token` (等價於 `injector.get(useExisting)`)
*/
useExisting: any;
}
export interface ExistingProvider extends ExistingSansProvider {
/**
* 用於設置與依賴對象關聯的Token值,Token值可能是Type、InjectionToken、OpaqueToken的實例或字符串
*/
provide: Type<any>;
/**
* 用於標識是否multiple providers,若是multiple類型,則返回與Token關聯的依賴對象列表
* 簡單來說如果multi是true的話,通過provide(Token)獲取的依賴對象是一個列表。
* 同一個Token可以注入多個服務
*/
multi?: boolean;
}
實例代碼。
@NgModule({
...
providers: [
ModuleExistingProviderServiceExtended, // 我們先通過TypeProvider的方式注入了ModuleExistingProviderServiceExtended
{provide: ModuleExistingProviderService, useExisting: ModuleExistingProviderServiceExtended}
],
...
})
export class ExistingProviderModule {
}
三,獲取依賴對象
通過上面的講解,我們已經知道怎麼的在指定的注入器裏面通過提供商注入相應的依賴對象。如果我們想在指定的地方(一般是組件裏面)使用依賴對象,就得先拿到對象。接下來我們就得叨叨怎麼拿到這個對象了。
通過提供者(providers)注入服務的時候,每個服務我們都給定了Token(Provider裏面的provide對象對應的值)。TypeProvider例外,其實TypeProvider雖然沒有明確的指出Token。其實內部的處理,Token就是TypeProvider設置的Type。
我們總結出獲取依賴對象有三種方式:
3.1 構造函數中通過@Inject獲取
藉助@Inject裝飾器獲取到指定的依賴對象。@Inject的參數就是需要獲取的依賴對象對應的Token。
/**
* 通過@Inject裝飾器獲取Token對應依賴的對象
*/
constructor(@Inject(TOKEN_MODULE_CLASS_PROVIDER) private service: ModuleClassProviderService) {
}
3.2 通過Injector.get(Token)獲取
先在構造函數中把Injector對象注入進來,然後在通過Injector.get(Token)獲取對象。同樣參數也是依賴對象對應的Token。
service: ModuleClassProviderService;
/**
* 藉助Injector服務來獲取Token對應的服務
*/
constructor(private injector: Injector) {
this.service = injector.get(TOKEN_MODULE_CLASS_PROVIDER);
}
3.3 構造函數中通過Type獲取
直接在構造函數中通過Type來獲取,這種獲取方式有個前提。必須是TypeProvider方式提供的服務。
constructor(private service: ModuleClassProviderService) {
}
四,Provider中的multi
上面講提供商(Provider)的時候多次出現了multi。multi表示同一個Token對應的服務可以是多個。當使用multi的時候。通過Token獲取依賴服務的時候是一個服務數組。其實也很好理解。比如網絡攔截器。是允許同一個Token有多個服務。每個攔截器做不同的邏輯處理。
文章最後給出文章涉及到的實例代碼下載地址https://github.com/tuacy/angular-inject,同時我們對Angular依賴注入的使用做一個簡單的總結。Angular裏面使用依賴注入步驟:
- 1.定義依賴對象的業務邏輯。
就是定義依賴對象服務類,確定服務類需要幹哪些事情。
- 2.明確我們依賴對象的作用範圍。
確定注入器,是用NgModule注入器呢,實時Component注入器。
- 3.依賴對象的Token確定。
依賴對象的獲取都是通過Token去獲取的。
- 4.依賴對象提供商的確定。
Provider用那種方式,TypeProvider呢,還是ValueProvider呢等等。
- 5.在需要使用依賴對象的地方獲取到依賴對象。