本文阿寶哥將從六個方面入手,全方位帶你一塊兒探索麪向對象編程中 IoC(控制反轉)和 DI(依賴注入) 的設計思想。閱讀完本文,你將瞭解如下內容:javascript
在介紹什麼是 IoC 容器以前,阿寶哥來舉一個平常工做中很常見的場景,即建立指定類的實例。最簡單的情形是該類沒有依賴其餘類,但現實每每是殘酷的,咱們在建立某個類的實例時,須要依賴不一樣類對應的實例。爲了讓小夥伴們可以更好地理解上述的內容,阿寶哥來舉一個例子。前端
一輛小汽車 🚗 一般由 發動機、底盤、車身和電氣設備 四大部分組成。汽車電氣設備的內部構造很複雜,簡單起見,咱們只考慮三個部分:發動機、底盤和車身。vue
(圖片來源:https://www.newkidscar.com/ve...)java
在現實生活中,要造輛車仍是很困難的。而在軟件的世界中,這可難不倒咱們。👇是阿寶哥要造的車子,有木有很酷。node
(圖片來源:https://pixabay.com/zh/illust...)react
在開始造車前,咱們得先看一下 「圖紙」:git
看完上面的 「圖紙」,咱們立刻來開啓造車之旅。第一步咱們先來定義車身類:angularjs
1.定義車身類github
export default class Body { }
2.定義底盤類typescript
export default class Chassis { }
3.定義引擎類
export default class Engine { start() { console.log("引擎發動了"); } }
4.定義汽車類
import Engine from './engine'; import Chassis from './chassis'; import Body from './body'; export default class Car { engine: Engine; chassis: Chassis; body: Body; constructor() { this.engine = new Engine(); this.body = new Body(); this.chassis = new Chassis(); } run() { this.engine.start(); } }
一切已準備就緒,咱們立刻來造一輛車:
const car = new Car(); // 阿寶哥造輛新車 car.run(); // 控制檯輸出:引擎發動了
如今雖然車已經能夠啓動了,但卻存在如下問題:
爲了解決第一個問題,提供更靈活的方案,咱們能夠重構一下已定義的汽車類,具體以下:
export default class Car { body: Body; engine: Engine; chassis: Chassis; constructor(engine, body, chassis) { this.engine = engine; this.body = body; this.chassis = chassis; } run() { this.engine.start(); } }
重構完汽車類,咱們來從新造輛新車:
const engine = new NewEngine(); const body = new Body(); const chassis = new Chassis(); const newCar = new Car(engine, body, chassis); newCar.run();
此時咱們已經解決了上面提到的第一個問題,要解決第二個問題咱們要來了解一下 IoC(控制反轉)的概念。
IoC(Inversion of Control),即 「控制反轉」。在開發中, IoC 意味着你設計好的對象交給容器控制,而不是使用傳統的方式,在對象內部直接控制。
如何理解好 IoC 呢?理解好 IoC 的關鍵是要明確 「誰控制誰,控制什麼,爲什麼是反轉,哪些方面反轉了」,咱們來深刻分析一下。
new
的方式建立對象,是程序主動建立依賴對象; 而 IoC 是有專門一個容器來建立這些對象,即由 IoC 容器控制對象的建立;誰控制誰?固然是 IoC 容器控制了對象;控制什麼?主要是控制外部資源(依賴對象)獲取。
爲什麼是反轉?由於由容器幫咱們查找及注入依賴對象,對象只是被動的接受依賴對象,因此是反轉了;哪些方面反轉了?依賴對象的獲取被反轉了。
IoC 不是一種技術,只是一種思想,是面向對象編程中的一種設計原則,能夠用來減低計算機代碼之間的耦合度。
傳統應用程序都是由咱們在類內部主動建立依賴對象,從而致使類與類之間高耦合,難於測試; 有了 IoC 容器後,把建立和查找依賴對象的控制權交給了容器,由容器注入組合對象,因此對象之間是鬆散耦合。 這樣也便於測試,利於功能複用,更重要的是使得程序的整個體系結構變得很是靈活。
其實 IoC 對編程帶來的最大改變不是從代碼上,而是思想上,發生了 「主從換位」 的變化。應用程序原本是老大,要獲取什麼資源都是主動出擊,但在 IoC 思想中,應用程序就變成被動了,被動的等待 IoC 容器來建立並注入它所需的資源了。
對於控制反轉來講,其中最多見的方式叫作 依賴注入,簡稱爲 DI(Dependency Injection)。
組件之間的依賴關係由容器在運行期決定,形象的說,即由容器動態的將某個依賴關係注入到組件之中。 依賴注入的目的並不是爲軟件系統帶來更多功能,而是爲了提高組件重用的頻率,併爲系統搭建一個靈活、可擴展的平臺。
經過依賴注入機制,咱們只須要經過簡單的配置,而無需任何代碼就可指定目標須要的資源,完成自身的業務邏輯,而不須要關心具體的資源來自何處,由誰實現。
理解 DI 的關鍵是 「誰依賴了誰,爲何須要依賴,誰注入了誰,注入了什麼」:
那麼 IoC 和 DI 有什麼關係?其實它們是同一個概念的不一樣角度描述,因爲控制反轉的概念比較含糊(可能只是理解爲容器控制對象這一個層面,很難讓人想到誰來維護依賴關係),因此 2004 年大師級人物 Martin Fowler 又給出了一個新的名字:「依賴注入」,相對 IoC 而言,「依賴注入」 明確描述了被注入對象依賴 IoC 容器配置依賴對象。
總的來講, 控制反轉(Inversion of Control)是說建立對象的控制權發生轉移,之前建立對象的主動權和建立時機由應用程序把控,而如今這種權利轉交給 IoC 容器,它就是一個專門用來建立對象的工廠,你須要什麼對象,它就給你什麼對象。 有了 IoC 容器,依賴關係就改變了,原先的依賴關係就沒了,它們都依賴 IoC 容器了,經過 IoC 容器來創建它們之間的關係。
前面介紹了那麼多的概念,如今咱們來看一下未使用依賴注入框架和使用依賴注入框架之間有什麼明顯的區別。
假設咱們的服務 A 依賴於服務 B,即要使用服務 A 前,咱們須要先建立服務 B。具體的流程以下圖所示:
從上圖可知,未使用依賴注入框架時,服務的使用者須要關心服務自己和其依賴的對象是如何建立的,且須要手動維護依賴關係。若服務自己須要依賴多個對象,這樣就會增長使用難度和後期的維護成本。對於上述的問題,咱們能夠考慮引入依賴注入框架。下面咱們來看一下引入依賴注入框架,總體流程會發生什麼變化。
使用依賴注入框架以後,系統中的服務會統一註冊到 IoC 容器中,若是服務有依賴其餘服務時,也須要對依賴進行聲明。當用戶須要使用特定的服務時,IoC 容器會負責該服務及其依賴對象的建立與管理工做。具體的流程以下圖所示:
到這裏咱們已經介紹了 IoC 與 DI 的概念及特色,接下來咱們來介紹 DI 的應用。
DI 在前端和服務端都有相應的應用,好比在前端領域的表明是 AngularJS 和 Angular,而在服務端領域是 Node.js 生態中比較出名的 NestJS。接下來阿寶哥將簡單介紹一下 DI 在 AngularJS/Angular 和 NestJS 中的應用。
在 AngularJS 中,依賴注入是其核心的特性之一。在 AngularJS 中聲明依賴項有 3 種方式:
// 方式一: 使用 $inject annotation 方式 let fn = function (a, b) {}; fn.$inject = ['a', 'b']; // 方式二: 使用 array-style annotations 方式 let fn = ['a', 'b', function (a, b) {}]; // 方式三: 使用隱式聲明方式 let fn = function (a, b) {}; // 不推薦
對於以上的代碼,相信使用過 AngularJS 的小夥們都不會陌生。做爲 AngularJS 核心功能特性的 DI 仍是蠻強大的,但隨着 AngularJS 的普及和應用的複雜度不斷提升,AngularJS DI 系統的問題就暴露出來了。
這裏阿寶哥簡單介紹一下 AngularJS DI 系統存在的幾個問題:
因爲 AngularJS DI 存在以上的問題,因此在後續的 Angular 從新設計了新的 DI 系統。
之前面汽車的例子爲例,咱們能夠把汽車、發動機、底盤和車身這些認爲是一種 「服務」,因此它們會以服務提供者的形式註冊到 DI 系統中。爲了能區分不一樣服務,咱們須要使用不一樣的令牌(Token)來標識它們。接着咱們會基於已註冊的服務提供者建立注入器對象。
以後,當咱們須要獲取指定服務時,咱們就能夠經過該服務對應的令牌,從注入器對象中獲取令牌對應的依賴對象。上述的流程的具體以下圖所示:
好的,瞭解完上述的流程。下面咱們來看一下如何使用 Angular 內置的 DI 系統來 「造車」。
// car.ts import { Injectable, ReflectiveInjector } from '@angular/core'; // 配置Provider @Injectable({ providedIn: 'root', }) export class Body {} @Injectable({ providedIn: 'root', }) export class Chassis {} @Injectable({ providedIn: 'root', }) export class Engine { start() { console.log('引擎發動了'); } } @Injectable() export default class Car { // 使用構造注入方式注入依賴對象 constructor( private engine: Engine, private body: Body, private chassis: Chassis ) {} run() { this.engine.start(); } } const injector = ReflectiveInjector.resolveAndCreate([ Car, Engine, Chassis, Body, ]); const car = injector.get(Car); car.run();
在以上代碼中咱們調用 ReflectiveInjector 對象的 resolveAndCreate
方法手動建立注入器,而後根據車輛對應的 Token
來獲取對應的依賴對象。經過觀察上述代碼,你能夠發現,咱們已經不須要手動地管理和維護依賴對象了,這些 「髒活」、「累活」 已經交給注入器來處理了。
此外,若是要能正常獲取汽車對象,咱們還須要在 app.module.ts
文件中聲明 Car 對應 Provider,具體以下所示:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import Car, { Body, Chassis, Engine } from './car'; @NgModule({ declarations: [AppComponent], imports: [BrowserModule], providers: [{ provide: Car, deps: [Engine, Body, Chassis] }], bootstrap: [AppComponent], }) export class AppModule {}
NestJS 是構建高效,可擴展的 Node.js Web 應用程序的框架。 它使用現代的 JavaScript 或 TypeScript(保留與純 JavaScript 的兼容性),並結合 OOP(面向對象編程),FP(函數式編程)和FRP(函數響應式編程)的元素。
在底層,Nest 使用了 Express,但也提供了與其餘各類庫的兼容,例如 Fastify,能夠方便地使用各類可用的第三方插件。
近幾年,因爲 Node.js,JavaScript 已經成爲 Web 前端和後端應用程序的「通用語言」,從而產生了像 Angular、React、Vue 等使人耳目一新的項目,這些項目提升了開發人員的生產力,使得能夠快速構建可測試的且可擴展的前端應用程序。 然而,在服務器端,雖然有不少優秀的庫、helper 和 Node 工具,可是它們都沒有有效地解決主要問題 —— 架構。
NestJS 旨在提供一個開箱即用的應用程序體系結構,容許輕鬆建立高度可測試,可擴展,鬆散耦合且易於維護的應用程序。 在 NestJS 中也爲咱們開發者提供了依賴注入的功能,這裏咱們以官網的示例來演示一下依賴注入的功能。
import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { getHello(): string { return 'Hello World!'; } }
import { Get, Controller, Render } from '@nestjs/common'; import { AppService } from './app.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() @Render('index') render() { const message = this.appService.getHello(); return { message }; } }
在 AppController 中,咱們經過構造注入的方式注入了 AppService 對象,當用戶訪問首頁的時候,咱們會調用 AppService 對象的 getHello
方法來獲取 'Hello World!'
消息,並把消息返回給用戶。固然爲了保證依賴注入能夠正常工做,咱們還須要在 AppModule 中聲明 providers 和 controllers,具體操做以下:
import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @Module({ imports: [], controllers: [AppController], providers: [AppService], }) export class AppModule {}
其實 DI 並非 AngularJS/Angular 和 NestJS 所特有的,若是你想在其餘項目中使用 DI/IoC 的功能特性,阿寶哥推薦你使用 InversifyJS,它是一個可用於 JavaScript 和 Node.js 應用,功能強大、輕量的 IoC 容器。
對 InversifyJS 感興趣的小夥伴能夠自行了解一下,阿寶哥就不繼續展開介紹了。接下來,咱們將進入本文的重點,即介紹如何使用 TypeScript 實現一個簡單的 IoC 容器,該容器實現的功能以下圖所示:
爲了讓你們能更好地理解 IoC 容器的實現代碼,阿寶哥來介紹一些相關的前置知識。
若是你有使用過 Angular 或 NestJS,相信你對如下的代碼不會陌生。
@Injectable() export class HttpService { constructor( private httpClient: HttpClient ) {} }
在以上代碼中,咱們使用了 Injectable
裝飾器。該裝飾器用於表示此類能夠自動注入其依賴項。其中 @Injectable()
中的 @
符號屬於語法糖。
裝飾器是一個包裝類,函數或方法併爲其添加行爲的函數。這對於定義與對象關聯的元數據頗有用。裝飾器有如下四種分類:
前面示例中使用的 @Injectable()
裝飾器,屬於類裝飾器。在該類裝飾器修飾的 HttpService
類中,咱們經過構造注入的方式注入了用於處理 HTTP 請求的 HttpClient
依賴對象。
@Injectable() export class HttpService { constructor( private httpClient: HttpClient ) {} }
以上代碼若設置編譯的目標爲 ES5,則會生成如下代碼:
// 忽略__decorate函數等代碼 var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var HttpService = /** @class */ (function () { function HttpService(httpClient) { this.httpClient = httpClient; } var _a; HttpService = __decorate([ Injectable(), __metadata("design:paramtypes", [typeof (_a = typeof HttpClient !== "undefined" && HttpClient) === "function" ? _a : Object]) ], HttpService); return HttpService; }());
經過觀察上述代碼,你會發現 HttpService
構造函數中 httpClient
參數的類型被擦除了,這是由於 JavaScript 是弱類型語言。那麼如何在運行時,保證注入正確類型的依賴對象呢?這裏 TypeScript 使用 reflect-metadata 這個第三方庫來存儲額外的類型信息。
reflect-metadata 這個庫提供了不少 API 用於操做元信息,這裏咱們只簡單介紹幾個經常使用的 API:
// define metadata on an object or property Reflect.defineMetadata(metadataKey, metadataValue, target); Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey); // check for presence of a metadata key on the prototype chain of an object or property let result = Reflect.hasMetadata(metadataKey, target); let result = Reflect.hasMetadata(metadataKey, target, propertyKey); // get metadata value of a metadata key on the prototype chain of an object or property let result = Reflect.getMetadata(metadataKey, target); let result = Reflect.getMetadata(metadataKey, target, propertyKey); // delete metadata from an object or property let result = Reflect.deleteMetadata(metadataKey, target); let result = Reflect.deleteMetadata(metadataKey, target, propertyKey); // apply metadata via a decorator to a constructor @Reflect.metadata(metadataKey, metadataValue) class C { // apply metadata via a decorator to a method (property) @Reflect.metadata(metadataKey, metadataValue) method() { } }
對於上述的 API 只需簡單瞭解一下便可。在後續的內容中,咱們將介紹具體如何使用。這裏咱們須要注意如下兩個問題:
瞭解完裝飾器與反射相關的基礎知識,接下來咱們來開始實現 IoC 容器。咱們的 IoC 容器將使用兩個主要的概念:令牌(Token)和提供者(Provider)。令牌是 IoC 容器所要建立對象的標識符,而提供者用於描述如何建立這些對象。
IoC 容器最小的公共接口以下所示:
export class Container { addProvider<T>(provider: Provider<T>) {} // TODO inject<T>(type: Token<T>): T {} // TODO }
接下來咱們先來定義 Token:
// type.ts interface Type<T> extends Function { new (...args: any[]): T; } // provider.ts class InjectionToken { constructor(public injectionIdentifier: string) {} } type Token<T> = Type<T> | InjectionToken;
Token 類型是一個聯合類型,既能夠是一個函數類型也能夠是 InjectionToken 類型。AngularJS 中使用字符串做爲 Token,在某些狀況下,可能會致使衝突。所以,爲了解決這個問題,咱們定義了 InjectionToken 類,來避免出現命名衝突問題。
定義完 Token 類型,接下來咱們來定義三種不一樣類型的 Provider:
// provider.ts export type Factory<T> = () => T; export interface BaseProvider<T> { provide: Token<T>; } export interface ClassProvider<T> extends BaseProvider<T> { provide: Token<T>; useClass: Type<T>; } export interface ValueProvider<T> extends BaseProvider<T> { provide: Token<T>; useValue: T; } export interface FactoryProvider<T> extends BaseProvider<T> { provide: Token<T>; useFactory: Factory<T>; } export type Provider<T> = | ClassProvider<T> | ValueProvider<T> | FactoryProvider<T>;
爲了更方便的區分這三種不一樣類型的 Provider,咱們自定義了三個類型守衛函數:
// provider.ts export function isClassProvider<T>( provider: BaseProvider<T> ): provider is ClassProvider<T> { return (provider as any).useClass !== undefined; } export function isValueProvider<T>( provider: BaseProvider<T> ): provider is ValueProvider<T> { return (provider as any).useValue !== undefined; } export function isFactoryProvider<T>( provider: BaseProvider<T> ): provider is FactoryProvider<T> { return (provider as any).useFactory !== undefined; }
在前面咱們已經提過了,對於類或函數,咱們須要使用裝飾器來修飾它們,這樣才能保存元數據。所以,接下來咱們來分別建立 Injectable 和 Inject 裝飾器。
Injectable 裝飾器用於表示此類能夠自動注入其依賴項,該裝飾器屬於類裝飾器。在 TypeScript 中,類裝飾器的聲明以下:
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
類裝飾器顧名思義,就是用來裝飾類的。它接收一個參數:target: TFunction
,表示被裝飾的類。下面咱們來看一下 Injectable 裝飾器的具體實現:
// Injectable.ts import { Type } from "./type"; import "reflect-metadata"; const INJECTABLE_METADATA_KEY = Symbol("INJECTABLE_KEY"); export function Injectable() { return function(target: any) { Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target); return target; }; }
在以上代碼中,當調用完 Injectable 函數以後,會返回一個新的函數。在新的函數中,咱們使用 reflect-metadata 這個庫提供的 defineMetadata API 來保存元信息,其中 defineMetadata API 的使用方式以下所示:
// define metadata on an object or property Reflect.defineMetadata(metadataKey, metadataValue, target); Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);
Injectable 類裝飾器使用方式也簡單,只須要在被裝飾類的上方使用 @Injectable()
語法糖就能夠應用該裝飾器:
@Injectable() export class HttpService { constructor( private httpClient: HttpClient ) {} }
在以上示例中,咱們注入的是 Type 類型的 HttpClient 對象。但在實際的項目中,每每會比較複雜。除了須要注入 Type 類型的依賴對象以外,咱們還可能會注入其餘類型的依賴對象,好比咱們但願在 HttpService 服務中注入遠程服務器的 API 地址。針對這種情形,咱們須要使用 Inject 裝飾器。
接下來咱們來建立 Inject 裝飾器,該裝飾器屬於參數裝飾器。在 TypeScript 中,參數裝飾器的聲明以下:
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number ) => void
參數裝飾器顧名思義,是用來裝飾函數參數,它接收三個參數:
下面咱們來看一下 Inject 裝飾器的具體實現:
// Inject.ts import { Token } from './provider'; import 'reflect-metadata'; const INJECT_METADATA_KEY = Symbol('INJECT_KEY'); export function Inject(token: Token<any>) { return function(target: any, _: string | symbol, index: number) { Reflect.defineMetadata(INJECT_METADATA_KEY, token, target, `index-${index}`); return target; }; }
在以上代碼中,當調用完 Inject 函數以後,會返回一個新的函數。在新的函數中,咱們使用 reflect-metadata 這個庫提供的 defineMetadata API 來保存參數相關的元信息。這裏是保存 index 索引信息和 Token 信息。
定義完 Inject 裝飾器,咱們就能夠利用它來注入咱們前面所提到的遠程服務器的 API 地址,具體的使用方式以下:
const API_URL = new InjectionToken('apiUrl'); @Injectable() export class HttpService { constructor( private httpClient: HttpClient, @Inject(API_URL) private apiUrl: string ) {} }
目前爲止,咱們已經定義了 Token、Provider、Injectable 和 Inject 裝飾器。接下來咱們來實現前面所提到的 IoC 容器的 API:
export class Container { addProvider<T>(provider: Provider<T>) {} // TODO inject<T>(type: Token<T>): T {} // TODO }
addProvider() 方法的實現很簡單,咱們使用 Map 來存儲 Token 與 Provider 之間的關係:
export class Container { private providers = new Map<Token<any>, Provider<any>>(); addProvider<T>(provider: Provider<T>) { this.assertInjectableIfClassProvider(provider); this.providers.set(provider.provide, provider); } }
在 addProvider() 方法內部除了把 Token 與 Provider 的對應信息保存到 providers 對象中以外,咱們定義了一個 assertInjectableIfClassProvider 方法,用於確保添加的 ClassProvider 是可注入的。該方法的具體實現以下:
private assertInjectableIfClassProvider<T>(provider: Provider<T>) { if (isClassProvider(provider) && !isInjectable(provider.useClass)) { throw new Error( `Cannot provide ${this.getTokenName( provider.provide )} using class ${this.getTokenName( provider.useClass )}, ${this.getTokenName(provider.useClass)} isn't injectable` ); } }
在 assertInjectableIfClassProvider 方法體中,咱們使用了前面已經介紹的 isClassProvider
類型守衛函數來判斷是否爲 ClassProvider,若是是的話,會判斷該 ClassProvider 是否爲可注入的,具體使用的是 isInjectable 函數,該函數的定義以下:
export function isInjectable<T>(target: Type<T>) { return Reflect.getMetadata(INJECTABLE_METADATA_KEY, target) === true; }
在 isInjectable 函數中,咱們使用 reflect-metadata 這個庫提供的 getMetadata API 來獲取保存在類中的元信息。爲了更好地理解以上代碼,咱們來回顧一下前面 Injectable 裝飾器:
const INJECTABLE_METADATA_KEY = Symbol("INJECTABLE_KEY"); export function Injectable() { return function(target: any) { Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target); return target; }; }
若是添加的 Provider 是 ClassProvider,但 Provider 對應的類是不可注入的,則會拋出異常。爲了讓異常消息更加友好,也更加直觀。咱們定義了一個 getTokenName
方法來獲取 Token 對應的名稱:
private getTokenName<T>(token: Token<T>) { return token instanceof InjectionToken ? token.injectionIdentifier : token.name; }
如今咱們已經實現了 Container 類的 addProvider
方法,這時咱們就可使用它來添加三種不一樣類型的 Provider:
const container = new Container(); const input = { x: 200 }; class BasicClass {} // 註冊ClassProvider container.addProvider({ provide: BasicClass, useClass: BasicClass}); // 註冊ValueProvider container.addProvider({ provide: BasicClass, useValue: input }); // 註冊FactoryProvider container.addProvider({ provide: BasicClass, useFactory: () => input });
須要注意的是,以上示例中註冊三種不一樣類型的 Provider 使用的是同一個 Token 僅是爲了演示而已。下面咱們來實現 Container 類中核心的 inject 方法。
在看 inject 方法的具體實現以前,咱們先來看一下該方法所實現的功能:
const container = new Container(); const input = { x: 200 }; container.addProvider({ provide: BasicClass, useValue: input }); const output = container.inject(BasicClass); expect(input).toBe(output); // true
觀察以上的測試用例可知,Container 類中 inject 方法所實現的功能就是根據 Token 獲取與之對應的對象。在前面實現的 addProvider 方法中,咱們把 Token 和該 Token 對應的 Provider 保存在 providers Map 對象中。因此在 inject 方法中,咱們能夠先從 providers 對象中獲取該 Token 對應的 Provider 對象,而後在根據不一樣類型的 Provider 來獲取其對應的對象。
好的,下面咱們來看一下 inject 方法的具體實現:
inject<T>(type: Token<T>): T { let provider = this.providers.get(type); // 處理使用Injectable裝飾器修飾的類 if (provider === undefined && !(type instanceof InjectionToken)) { provider = { provide: type, useClass: type }; this.assertInjectableIfClassProvider(provider); } return this.injectWithProvider(type, provider); }
在以上代碼中,除了處理正常的流程以外。咱們還處理一個特殊的場景,即沒有使用 addProvider
方法註冊 Provider,而是使用 Injectable 裝飾器來裝飾某個類。對於這個特殊場景,咱們會根據傳入的 type 參數來建立一個 provider 對象,而後進一步調用 injectWithProvider
方法來建立對象,該方法的具體實現以下:
private injectWithProvider<T>(type: Token<T>, provider?: Provider<T>): T { if (provider === undefined) { throw new Error(`No provider for type ${this.getTokenName(type)}`); } if (isClassProvider(provider)) { return this.injectClass(provider as ClassProvider<T>); } else if (isValueProvider(provider)) { return this.injectValue(provider as ValueProvider<T>); } else { return this.injectFactory(provider as FactoryProvider<T>); } }
在 injectWithProvider
方法內部,咱們會使用前面定義的用於區分三種不一樣類型 Provider 的類型守衛函數來處理不一樣的 Provider。這裏咱們先來看一下最簡單 ValueProvider,當發現注入的是 ValueProvider 類型時,則會調用 injectValue
方法來獲取其對應的對象:
// { provide: API_URL, useValue: 'https://www.semlinker.com/' } private injectValue<T>(valueProvider: ValueProvider<T>): T { return valueProvider.useValue; }
接着咱們來看如何處理 FactoryProvider 類型的 Provider,若是發現是 FactoryProvider 類型時,則會調用 injectFactory
方法來獲取其對應的對象,該方法的實現也很簡單:
// const input = { x: 200 }; // container.addProvider({ provide: BasicClass, useFactory: () => input }); private injectFactory<T>(valueProvider: FactoryProvider<T>): T { return valueProvider.useFactory(); }
最後咱們來分析一下如何處理 ClassProvider,對於 ClassProvider 類說,經過 Provider 對象的 useClass 屬性,咱們就能夠直接獲取到類對應的構造函數。最簡單的情形是該類沒有依賴其餘對象,但在大多數場景下,即將實例化的服務類是會依賴其餘的對象的。因此在實例化服務類前,咱們須要構造其依賴的對象。
那麼如今問題來了,怎麼獲取類所依賴的對象呢?咱們先來分析一下如下代碼:
const API_URL = new InjectionToken('apiUrl'); @Injectable() export class HttpService { constructor( private httpClient: HttpClient, @Inject(API_URL) private apiUrl: string ) {} }
以上代碼若設置編譯的目標爲 ES5,則會生成如下代碼:
// 已省略__decorate函數的定義 var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var HttpService = /** @class */ (function () { function HttpService(httpClient, apiUrl) { this.httpClient = httpClient; this.apiUrl = apiUrl; } var _a; HttpService = __decorate([ Injectable(), __param(1, Inject(API_URL)), __metadata("design:paramtypes", [typeof (_a = typeof HttpClient !== "undefined" && HttpClient) === "function" ? _a : Object, String]) ], HttpService); return HttpService; }());
觀察以上的代碼會不會以爲有點暈?不要着急,阿寶哥會逐一分析 HttpService 中的兩個參數。首先咱們先來分析 apiUrl
參數:
在圖中咱們能夠很清楚地看到,API_URL
對應的 Token 最終會經過 Reflect.defineMetadata API 進行保存,所使用的 Key 是 Symbol('INJECT_KEY')
。而對於另外一個參數即 httpClient,它使用的 Key 是 "design:paramtypes"
,它用於修飾目標對象方法的參數類型。
除了 "design:paramtypes"
以外,還有其餘的 metadataKey
,好比 design:type
和 design:returntype
,它們分別用於修飾目標對象的類型和修飾目標對象方法返回值的類型。
由上圖可知,HttpService 構造函數的參數類型最終會使用 Reflect.metadata
API 進行存儲。瞭解完上述的知識,接下來咱們來定義一個 getInjectedParams
方法,用於獲取類構造函數中聲明的依賴對象,該方法的具體實現以下:
type InjectableParam = Type<any>; const REFLECT_PARAMS = "design:paramtypes"; private getInjectedParams<T>(target: Type<T>) { // 獲取參數的類型 const argTypes = Reflect.getMetadata(REFLECT_PARAMS, target) as ( | InjectableParam | undefined )[]; if (argTypes === undefined) { return []; } return argTypes.map((argType, index) => { // The reflect-metadata API fails on circular dependencies, and will return undefined // for the argument instead. if (argType === undefined) { throw new Error( `Injection error. Recursive dependency detected in constructor for type ${target.name} with parameter at index ${index}` ); } const overrideToken = getInjectionToken(target, index); const actualToken = overrideToken === undefined ? argType : overrideToken; let provider = this.providers.get(actualToken); return this.injectWithProvider(actualToken, provider); }); }
由於咱們的 Token 的類型是 Type<T> | InjectionToken
聯合類型,因此在 getInjectedParams
方法中咱們也要考慮 InjectionToken 的情形,所以咱們定義了一個 getInjectionToken
方法來獲取使用 @Inject
裝飾器註冊的 Token,該方法的實現很簡單:
export function getInjectionToken(target: any, index: number) { return Reflect.getMetadata(INJECT_METADATA_KEY, target, `index-${index}`) as Token<any> | undefined; }
如今咱們已經能夠獲取類構造函數中所依賴的對象,基於前面定義的 getInjectedParams
方法,咱們就來定義一個 injectClass
方法,用來實例化 ClassProvider 所註冊的類。
// { provide: HttpClient, useClass: HttpClient } private injectClass<T>(classProvider: ClassProvider<T>): T { const target = classProvider.useClass; const params = this.getInjectedParams(target); return Reflect.construct(target, params); }
這時 IoC 容器中定義的兩個方法都已經實現了,咱們來看一下 IoC 容器的完整代碼:
// container.ts type InjectableParam = Type<any>; const REFLECT_PARAMS = "design:paramtypes"; export class Container { private providers = new Map<Token<any>, Provider<any>>(); addProvider<T>(provider: Provider<T>) { this.assertInjectableIfClassProvider(provider); this.providers.set(provider.provide, provider); } inject<T>(type: Token<T>): T { let provider = this.providers.get(type); if (provider === undefined && !(type instanceof InjectionToken)) { provider = { provide: type, useClass: type }; this.assertInjectableIfClassProvider(provider); } return this.injectWithProvider(type, provider); } private injectWithProvider<T>(type: Token<T>, provider?: Provider<T>): T { if (provider === undefined) { throw new Error(`No provider for type ${this.getTokenName(type)}`); } if (isClassProvider(provider)) { return this.injectClass(provider as ClassProvider<T>); } else if (isValueProvider(provider)) { return this.injectValue(provider as ValueProvider<T>); } else { // Factory provider by process of elimination return this.injectFactory(provider as FactoryProvider<T>); } } private assertInjectableIfClassProvider<T>(provider: Provider<T>) { if (isClassProvider(provider) && !isInjectable(provider.useClass)) { throw new Error( `Cannot provide ${this.getTokenName( provider.provide )} using class ${this.getTokenName( provider.useClass )}, ${this.getTokenName(provider.useClass)} isn't injectable` ); } } private injectClass<T>(classProvider: ClassProvider<T>): T { const target = classProvider.useClass; const params = this.getInjectedParams(target); return Reflect.construct(target, params); } private injectValue<T>(valueProvider: ValueProvider<T>): T { return valueProvider.useValue; } private injectFactory<T>(valueProvider: FactoryProvider<T>): T { return valueProvider.useFactory(); } private getInjectedParams<T>(target: Type<T>) { const argTypes = Reflect.getMetadata(REFLECT_PARAMS, target) as ( | InjectableParam | undefined )[]; if (argTypes === undefined) { return []; } return argTypes.map((argType, index) => { // The reflect-metadata API fails on circular dependencies, and will return undefined // for the argument instead. if (argType === undefined) { throw new Error( `Injection error. Recursive dependency detected in constructor for type ${target.name} with parameter at index ${index}` ); } const overrideToken = getInjectionToken(target, index); const actualToken = overrideToken === undefined ? argType : overrideToken; let provider = this.providers.get(actualToken); return this.injectWithProvider(actualToken, provider); }); } private getTokenName<T>(token: Token<T>) { return token instanceof InjectionToken ? token.injectionIdentifier : token.name; } }
最後咱們來簡單測試一下咱們前面開發的 IoC 容器,具體的測試代碼以下所示:
// container.test.ts import { Container } from "./container"; import { Injectable } from "./injectable"; import { Inject } from "./inject"; import { InjectionToken } from "./provider"; const API_URL = new InjectionToken("apiUrl"); @Injectable() class HttpClient {} @Injectable() class HttpService { constructor( private httpClient: HttpClient, @Inject(API_URL) private apiUrl: string ) {} } const container = new Container(); container.addProvider({ provide: API_URL, useValue: "https://www.semlinker.com/", }); container.addProvider({ provide: HttpClient, useClass: HttpClient }); container.addProvider({ provide: HttpService, useClass: HttpService }); const httpService = container.inject(HttpService); console.dir(httpService);
以上代碼成功運行後,控制檯會輸出如下結果:
HttpService { httpClient: HttpClient {}, apiUrl: 'https://www.semlinker.com/' }
很明顯該結果正是咱們所指望的,這表示咱們 IoC 容器已經能夠正常工做了。固然在實際項目中,一個成熟的 IoC 容器還要考慮不少東西,若是小夥伴想在項目中使用的話,阿寶哥建議能夠考慮使用 InversifyJS 這個庫。
若須要獲取完整 IoC 容器源碼的話,可在 全棧修仙之路 公衆號回覆 ioc 關鍵字,便可獲取。