Angular4 - 依賴注入
1. 例子 (不是很恰當)
第一版
//car.ts
export class Car {
engine: Engine;
doors: Doors;
body: Body;
constructor() {
this.engine = new Engine();
this.body = new Body();
this.doors = new Doors();
}
run() {
this.engine.start();
}
}
/*車身*/
export class Body { }
/*車門*/
export class Doors { }
/*引擎*/
export class Engine {
start() {
console.log('start run');
}
}
上面的這個檔案隻是一個簡單的檔案,内部輸出四個類檔案,汽車,車身,車門,引擎。然後将這個檔案中的四個對象當作是一個資源對象在Angular應用中使用,為什麼叫做資源對象,因為這個檔案的四個對象是為了Angular程式運作過程中獨立存在的一種對象,不依賴于子產品元件。
然後我們在Angular程式中去引用建立car對象
//angular.component.ts
import {Component, OnInit} from '@angular/core';
import { Car } from '../../common/class/car';
@Component({
selector: 'app-angular',
templateUrl: './angular.component.html',
styleUrls: ['./angular.component.css'],
})
export class AngularComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
let car1 = new Car(); // 建立Car對象
let car2 = new Car(); // 建立Car對象
console.log(car1 === car2); //false
}
}
第一個問題是什麼呢?假如我想在調用的時候自定義一些參數,調用這決定自己的汽車使用那些配件,那麼上面的代碼就需要改了。
第二版
//car.ts
export class Car {
engine: Engine;
doors: Doors;
body: Body;
constructor(engine, body, doors) {
this.engine = engine;
this.body = body;
this.doors = doors;
}
run() {
this.engine.start();
}
}
/*車身*/
export class Body { }
/*車門*/
export class Doors { }
/*引擎*/
export class Engine {
start() {
console.log('start run');
}
}
//angular.component.ts
import {Component, OnInit} from '@angular/core';
import {Body, Car, Doors, Engine} from '../../common/class/car';
@Component({
selector: 'app-angular',
templateUrl: './angular.component.html',
styleUrls: ['./angular.component.css'],
})
export class AngularComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
let engine = new Engine();
let body = new Body();
let doors = new Doors();
let car = new Car(engine, body, doors);
car.run();
}
}
現在的問題是什麼嗎?剛才Car 類發生了變化,Angular程式中所有調用的地方都需要變化。對于Angular程式來說,按照Car的接口建立對象也就認了。可是Car對象發生變化了,Angular程式也要發生變化。對于追求開發效率來說,不能忍啊,有本事就Car你發生了變化,就變化,Angular程式不需要發生變化才牛逼嘛。好像是這個道理,我們現在看到當我們想要自定義傳入car構造函數的參數時,我們在調用的地方就更複雜了。好像這二者之間有點不對頭,想要自定義,還想調用的地方不變化。先來解決第一個問題: 就是調用的地方不發生變化。
第三版
//car.ts
import {Injectable} from '@angular/core';
/*車身*/
@Injectable()
export class Body { }
/*車門*/
@Injectable()
export class Doors { }
/*引擎*/
@Injectable()
export class Engine {
start() {
console.log('start run');
}
}
@Injectable()
export class Car {
constructor(
private engine: Engine,
private body: Body,
private doors: Doors) {}
run() {
this.engine.start();
}
}
//angular.component.ts
import {Component, OnInit, ReflectiveInjector} from '@angular/core';
import {Body, Car, Doors, Engine} from '../../common/class/car';
@Component({
selector: 'app-angular',
templateUrl: './angular.component.html',
styleUrls: ['./angular.component.css'],
})
export class AngularComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
let injector = ReflectiveInjector.resolveAndCreate([Engine, Body, Doors, Car]);
let car = injector.get(Car);
car.run();
}
}
通過調用 ReflectiveInjector 抽象類的 resolveAndCreate() 方法,建立注入器。然後通過調用注入器的 get() 方法,擷取 Token 對應的對象。注入到什麼地方,當然是IOC容器。 繼續變化,上面這種方式不是常用的手段,我們現在使用常用的手法。
第四版
//angular.component.ts
import {Component, OnInit, ReflectiveInjector} from '@angular/core';
import {Body, Car, Doors, Engine} from '../../common/class/car';
@Component({
selector: 'app-angular',
templateUrl: './angular.component.html',
styleUrls: ['./angular.component.css'],
providers: [Body, Car, Doors, Engine]
})
export class AngularComponent implements OnInit {
constructor(private car: Car) {
}
ngOnInit(): void {
this.car.run();
}
}
這個調用方式是最經常見到的方式。走到了這裡,現在我們需要将依賴注入說一下了。
2. 依賴注入
我們先說上面第一版的情況
如上圖所示: Car在被由angular程式(使用Car對象的子產品元件建立,然後調用者需要follow Car對象的構造函數)。這是使用者自身控制對象的産生。
然後我們看第三版,就是用注入器的方式,現在使用者不在直接去建立對象了,而是通過注入器來産生新的對象,第三版就是在通知angular IOC容器我需要哪些對象,你去幫我建立吧。
第四版其實已經将通知的内容都省略,IOC容器自己會去根據對象來産生相應的對象。前提是我們angular程式提供了這些可被注入的對象。
控制反轉是相對于應用程式(也就是angular.component.ts)來說的,它需要誰就建立誰。現在不一樣,建立的事情交給了IOC容器來做。這也就叫做控制反轉。
然後再說依賴注入:相對于IOC容器和資源對象來說的(Car),了解DI的關鍵是:“誰依賴誰,為什麼需要依賴,誰注入誰,注入了什麼”:
誰依賴于誰:當然是應用程式依賴于IoC容器;
為什麼需要依賴:應用程式需要IoC容器來提供對象需要的外部資源;
誰注入誰:很明顯是IoC容器注入應用程式某個對象,應用程式依賴的對象;
注入了什麼:就是注入某個對象所需要的外部資源(包括對象、資源、常量資料)。
相對于angular來說,Angular IOC容器來控制這些資源對象,維護,建立完對象之後注入到應用程式中去。但是IOC本身不知道我可以控制的對象有哪些,就需要通過providers 來告知。這也是上面第四版中加了providers 中繼資料的原因。
走到這裡,其實之前說的兩個問題已經說了一個,但是另外一個還沒有解決,就是使用者想要自定義參數建立對象。其實大家反過來想一下,現在使用者都已經不在乎外部資源怎麼建立了,還需要糾結這個問題嗎?因為我們的這個例子是建立一個對象,就會出現自定義傳入的參數,這也是為什麼在上面我會表明不太恰當的原因。angular的服務其實更多的是一種應用到多個子產品,多個元件的對象,很少回事這種建立一個實體對象。
至于說我們通過IOC容器建立的對象是不是單例的呢?其實也是需要考慮的,關于這方面請看文章Angular4 - 共享子產品 。
現在我們知道了providers 的作用,接着看一下providers吧。
3. providers
(1)Provider
import {NgModule, TemplateRef} from '@angular/core';
import { AngularComponent } from './angular.component';
import {RouterModule, Routes} from '@angular/router';
import {CommonModule as CommonPrivateModule} from '../../common/common.module';
import {CommonModule} from '@angular/common';
import { RouteComponent } from './route/route.component';
import {NgZorroAntdModule} from 'ng-zorro-antd';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {HttpServiceService} from '../../common/service/http-service.service';
const routes: Routes = [
{path: 'module', component: AngularComponent},
{path: 'route', component: RouteComponent}
];
@NgModule({
imports: [
CommonModule,
RouterModule.forChild(routes),
CommonPrivateModule,
CommonModule,
FormsModule,
ReactiveFormsModule,
NgZorroAntdModule
],
declarations: [AngularComponent, RouteComponent],
providers: [{provide: HttpServiceService, useClass: HttpServiceService}]
})
export class AngularModule { }
providers: [{provide: HttpServiceService, useClass: HttpServiceService}]
第一個是令牌 (token),它作為鍵值 (key) 使用,用于定位依賴值和注冊提供商。
第二個是一個提供商定義對象。 可以把它看做是指導如何建立依賴值的配方。 有很多方式建立依賴值,也有很多方式可以寫配方。
一般我們會直接寫providers: [HttpServiceService],這種書寫方式和上面的方式一樣,token與class同名。
(2). 四種提供方式
a) 類提供商(useClass)
providers: [{provide: HttpServiceService, useClass: HttpServiceService}]
這種方式是token為HttpServiceService, 使用class HttpServiceService進行執行個體化。
當然這個地方的useClass可以使用别名類提供商,假如現在HttpServiceService現在不能滿足新的開發需求,但是這個來在其他元件中還在使用,是以我們可以新開發一個新的類HttpServiceService1,與HttpServiceService實作同樣的接口,然後隻在這個地方使用,那我們就可以使用如下:
providers: [{provide: HttpServiceService, useClass: HttpServiceService1}]
b) 值提供商(userValue)
有時,提供一個預先做好的對象會比請求注入器從類中建立它更容易。
import { Component, Inject, InjectionToken, OnInit} from '@angular/core';
export let APP_CONFIG = new InjectionToken<string>('injectionToken');
@Component({
selector: 'app-angular',
templateUrl: './angular.component.html',
styleUrls: ['./angular.component.css'],
providers: [
{ provide: APP_CONFIG, useValue: 'Test' }
]
})
export class AngularComponent implements OnInit {
constructor(@Inject(APP_CONFIG) config: string) {
console.log(config);
}
ngOnInit(): void {
}
}
c) 工廠提供商(useFactory)
有時,我們需要動态建立這個依賴值,因為它所需要的資訊直到最後一刻才能确定。 也許這個資訊會在浏覽器的會話中不停地變化。還假設這個可注入的服務沒法通過獨立的源通路此資訊。這種情況下,請調用工廠提供商。
FactoryProvider 用于告訴 Injector (注入器),通過調用 useFactory 對應的函數,傳回 Token 對應的依賴對象。
FactoryProvider 的使用
function serviceFactory() {
return new Service();
}
const provider: FactoryProvider = {
provide: 'someToken', useFactory: serviceFactory, deps: []
};
FactoryProvider 接口
export interface FactoryProvider {
// 用于設定與依賴對象關聯的Token值,Token值可能是Type、InjectionToken、
// OpaqueToken的執行個體或字元串
provide: any;
// 設定用于建立對象的工廠函數
useFactory: Function;
// 依賴對象清單
deps?: any[];
// 用于辨別是否multiple providers,若是multiple類型,則傳回與Token關聯的依賴對象清單
multi?: boolean;
}
d) 别名-提供商(useExisting)
使用useExisting,提供商可以把一個令牌映射到另一個令牌上。實際上,第一個令牌是第二個令牌所對應的服務的一個别名,創造了通路同一個服務對象的兩種方法。
{ provide: OldLogger, useExisting: NewLogger}]
(3). Multi Providers
我們以值提供器作為例子:
import {Component, Inject, InjectionToken, Injector, OnInit} from '@angular/core';
export let APP_CONFIG = new InjectionToken<string>('injectionToken');
@Component({
selector: 'app-angular',
templateUrl: './angular.component.html',
styleUrls: ['./angular.component.css'],
providers: [
{ provide: APP_CONFIG, useValue: 'Test' },
{ provide: APP_CONFIG, useValue: 'Test Second'}
]
})
export class AngularComponent implements OnInit {
constructor(@Inject(APP_CONFIG) config: string) {
console.log(config);
}
ngOnInit(): void {
}
}
此時的輸出值為‘Test Second’, 因為使用同一個 token 注冊 provider,後面注冊的 provider 将會覆寫前面已注冊的 provider。如果我們加上multi,那麼我們得到的就是一個數組。
import {Component, Inject, InjectionToken, Injector, OnInit} from '@angular/core';
export let APP_CONFIG = new InjectionToken<string>('injectionToken');
@Component({
selector: 'app-angular',
templateUrl: './angular.component.html',
styleUrls: ['./angular.component.css'],
providers: [
{ provide: APP_CONFIG, useValue: 'Test', multi: true },
{ provide: APP_CONFIG, useValue: 'Test Second', multi: true }
]
})
export class AngularComponent implements OnInit {
constructor(@Inject(APP_CONFIG) config: string) {
console.log(config); //["Test", "Test Second"]
}
ngOnInit(): void {
}
}
3. @Injectable() 是必須的麼
如果所建立的服務不依賴于其他對象,是可以不用使用 Injectable 類裝飾器。但當該服務需要在構造函數中注入依賴對象,就需要使用 Injectable 裝飾器。不過比較推薦的做法不管是否有依賴對象,在建立服務時都使用 Injectable 類裝飾器。