在一個專案中,元件和服務之間存在錯綜複雜的關係,為了最小程度的耦合,我們需要來管理組織這種關係,依賴注入就是管理這種關係的一種方式。
為什麼要使用依賴注入
在學習一個概念之前,我們必須要知道我們為什麼要學習這個東西,這個東西究竟解決了什麼問題。就好比這裡講到的,依賴注入究竟解決了什麼問題。要解決這個問題,我們先來看看示例程式碼:
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.`;
}
}
複製程式碼
以上是來自angular官網的一段程式碼,我們可以看到一個Car
類依賴於Engine
和Tires
這兩個類,我們在Car
的建構函式中去例項這兩個依賴類。這有什麼問題?如果有一天我們的Tires
建構函式需要一個引數,那麼我們必須要在Car
的建構函式中去更改程式碼。
// ...
constructor() {
this.engine = new Engine();
this.tires = new Tires(params);
}
]
// ...
複製程式碼
這種程式碼是非常不靈活的。雖然我們可以進行如下結構調整
export class Car {
public engine: Engine;
public tires: Tires;
public description = 'No DI';
constructor(engine, tires) {
this.engine = engine;
this.tires = tires;
}
// Method using the engine and tires
drive() {
return `${this.description} car with ` +
`${this.engine.cylinders} cylinders and ${this.tires.make} tires.`;
}
}
const car = new Car(new Engine(), new Tires())
複製程式碼
這樣似乎解決了不靈活的問題,但是如果依賴項很多的話,我們都要去手動建立這些例項,也不太方便。其實建立依賴例項的過程完全可以交給一個專門的'工廠'來做,這就是angular裡面的Injector。
基本使用
- 在元件中使用
@Component({
selector: 'app-heroes',
providers: [Engine, Tires],
template: `
<h2>Heroes</h2>
<app-hero-list></app-hero-list>
`
})
export class HeroesComponent {
construtor(private engine: Engine) {
this.engine.start();
}
}
複製程式碼
在Angular中,一般我們將這些公共的依賴都會一些一個服務裡面。在上面的用法我們可以看到多了一個providers,另外就是在類的建構函式中增加了private engine: Engine
我們就可以去使用engine這個例項了,在這個過程中,我們並沒有去手動去建立依賴項的例項。這是因為angular的Injector幫我們自動建立了。在這裡有一個比較形象的比喻就是,一個廚子(Injector)根據菜譜(providers)去做菜(依賴的例項),但是究竟做哪些菜呢,客人說了算(private engine: Engine
也就是建構函式中的)
- 在服務中使用
import { Injectable } from '@angular/core';
@Injectable()
export class HeroService {
constructor(private engine: Engine) { }
}
複製程式碼
如果我們的一個服務本身就依賴於其他依賴項,那麼我們使用@Injectable()
裝飾器(即使一個服務並沒有依賴於其他服務,我們也推薦加上@Injectable()裝飾器),我們依然要提供providers。這裡由於服務通常跟檢視是沒有具體的關係,所以這裡我們不會引入@component
裝飾器,那麼我們在哪裡確定這個providers呢?我們可以在一個module
中的providers屬性中去定義,那麼這個module
中的所有元件都會去共用這一個例項,但是我們有時候我們不希望共用一個例項,而是一個新的例項,那麼我們可以在這個元件中的providers中重新定義,這樣我們就會得到一個新的例項。實際上這就是層級注入。利用層級注入我們既可以共用例項,也可以不共用例項非常方便。一般全域性使用的服務,我們會註冊在app.module模組之下,這樣在整個應用中都可以使用。
在上面我們說過通過依賴注入建立的例項是可以實現共享的,我們證明一下。
import { Component, OnInit, ReflectiveInjector } from '@angular/core';
import {DependenceComponent} from './dependence.component';
@Component({
selector: 'app-service',
templateUrl: './service.component.html',
styleUrls: ['./service.component.scss'],
})
@Injectable()
export class ServiceComponent implements OnInit {
constructor() {
let injector = ReflectiveInjector.resolveAndCreate([Dependence]);
let dependence1 = injector.get(Dependence);
let dependence2 = injector.get(Dependence);
console.log('dependence1 === dependence2', dependence1 === dependence2); // true
}
ngOnInit() {}
}
複製程式碼
在這裡我們可以看見列印出來的是true
,這裡我們採用的是手動建立例項,所以我們並不需要在providers中提供“菜譜”,實際上resolveAndCreate
的引數就是一個providers
Providers
我們有四種配置注入過程,即使用類、使用工廠、使用值、使用別名
- 使用類
{provide: MyService, useClass: MyService}
複製程式碼
這是我們最常見的情形在angular中,通常如果provide的值和useclass的值一樣,我們可以簡化為[MyService]
。
- 使用值 顯然並不是每種情況,我們都需要注入一個類,有時候可以僅僅是一個值
{provide: MyValue, useValue: 12345}
複製程式碼
- 使用別名
{provide: OldService, useClass: NewService}
複製程式碼
如果我們有兩個服務OldService
和NewService
介面都一致,出於某種原因,我們不得不使用OldService
作為Token,但是我們又想使用NewService
中的介面,那麼我們就可以使用別名。
- 使用存在的值
[ NewLogger,
// Not aliased! Creates two instances of `NewLogger`
{ provide: OldLogger, useClass: NewLogger}]
複製程式碼
這種情況下會建立兩個NewLogger的例項,這顯然不是我們想要的結果,這時我們就可以使用存在的
[ NewLogger,
// Alias OldLogger w/ reference to NewLogger
{ provide: OldLogger, useExisting: NewLogger}]
複製程式碼
- 使用工廠 如果我們的服務需要根據不同的輸入值,做出不同的響應,那麼就必須要接受一個引數,那麼我們就必須使用工廠
{provide: MyService, useFactory: (user: User) => {
user.isAdmin ? new adminService : customService,
deps: [User]
}}
複製程式碼
當使用工廠時,我們可以通過變數的不同值,去例項不同的類。也就是說我們需要根據不同的值返回不同的依賴例項的時候,那麼我們就需要使用工廠。
@Options 、@Host
目前為止我們的依賴都是存在的,但是實際情況並不是總是這樣。那麼我們可以通過@Optional裝飾器來解決這個問題。
import { Optional } from '@angular/core';
// ....
constructor(
@Optional() private dependenceService: DependenceService
) {}
複製程式碼
但是這裡DependenceService這個服務類的定義還是存在的,只是沒有準備好,例如沒有在providers中使用
依賴查詢的規則是按照注入器從當前元件向父級元件查詢,直到找到這個依賴為止,但是如果限定查詢路徑截止在宿主元件,那麼如果宿主元件中沒有就會報錯,我們可以通過@Host修飾器達到這一功能。
如果一個元件注入了依賴項,那麼這個元件就是這個依賴項的宿主元件,但是如果這個元件通過
ng-content
被嵌入到宿主元件,那麼這個宿主元件就是該依賴項的宿主元件。
Token
當我們在建構函式中使用private dependenceService: DependenceService
,injector就可以正確的知道我們要例項哪一個類,這是因為在這裡DependenceService
充當了Token的角色(也就是說類名是可以充當Token的),我們只需要在providers中去尋找具有相同Token的值就行,但是往往我們注入不是一個類,而是一個字串,function或者物件。而這裡string、方法名和物件是不能夠充當Token的,那麼這時我們就需要來手動建立一個Token:
import { InjectionToken } from '@angular/core';
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }]
複製程式碼
constructor(@Inject(APP_CONFIG) config: AppConfig) {
this.title = config.title;
}
複製程式碼
Inject 裝飾器顯示的宣告所依賴物件的型別
@Injectable()
class A {
constructor(private buffer: Buffer) {}
}
複製程式碼
等同於
class A {
constructor(@Inject(Buffer) private buffer: Buffer) {}
}
複製程式碼