您好,登錄后才能下訂單哦!
這篇文章主要介紹“如何使用TypeScript實現一個IoC容器”,在日常操作中,相信很多人在如何使用TypeScript實現一個IoC容器問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”如何使用TypeScript實現一個IoC容器”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
一、背景概述
在介紹什么是 IoC 容器之前,來舉一個日常工作中很常見的場景,即創建指定類的實例。
最簡單的情形是該類沒有依賴其他類,但現實往往是殘酷的,我們在創建某個類的實例時,需要依賴不同類對應的實例。為了讓小伙伴們能夠更好地理解上述的內容,阿寶哥來舉一個例子。
一輛小汽車 ? 通常由 發動機、底盤、車身和電氣設備 四大部分組成。汽車電氣設備的內部構造很復雜,簡單起見,我們只考慮三個部分:發動機、底盤和車身。
在現實生活中,要造輛車還是很困難的。而在軟件的世界中,這可難不倒我們。
在開始造車前,我們得先看一下 “圖紙”:
看完上面的 “圖紙”,我們馬上來開啟造車之旅。第一步我們先來定義車身類:
1.定義車身類
export default class Body { }
2.定義底盤類
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 是什么
IoC(Inversion of Control),即 “控制反轉”。在開發中, IoC 意味著你設計好的對象交給容器控制,而不是使用傳統的方式,在對象內部直接控制。
如何理解好 IoC 呢?理解好 IoC 的關鍵是要明確 “誰控制誰,控制什么,為何是反轉,哪些方面反轉了”,我們來深入分析一下。
誰控制誰,控制什么:在傳統的程序設計中,我們直接在對象內部通過 new 的方式創建對象,是程序主動創建依賴對象;而 IoC 是有專門一個容器來創建這些對象,即由 IoC 容器控制對象的創建;
誰控制誰?當然是 IoC 容器控制了對象;控制什么?主要是控制外部資源(依賴對象)獲取。
為何是反轉了,哪些方面反轉了:有反轉就有正轉,傳統應用程序是由我們自己在程序中主動控制去獲取依賴對象,也就是正轉;而反轉則是由容器來幫忙創建及注入依賴對象;
為何是反轉?因為由容器幫我們查找及注入依賴對象,對象只是被動的接受依賴對象,所以是反轉了;哪些方面反轉了?依賴對象的獲取被反轉了。
三、IoC 能做什么
IoC 不是一種技術,只是一種思想,是面向對象編程中的一種設計原則,可以用來減低計算機代碼之間的耦合度。
傳統應用程序都是由我們在類內部主動創建依賴對象,從而導致類與類之間高耦合,難于測試;有了 IoC 容器后,把創建和查找依賴對象的控制權交給了容器,由容器注入組合對象,所以對象之間是松散耦合。 這樣也便于測試,利于功能復用,更重要的是使得程序的整個體系結構變得非常靈活。
其實 IoC 對編程帶來的最大改變不是從代碼上,而是思想上,發生了 “主從換位” 的變化。應用程序本來是老大,要獲取什么資源都是主動出擊,但在 IoC 思想中,應用程序就變成被動了,被動的等待 IoC 容器來創建并注入它所需的資源了。
四、IoC 與 DI 之間的關系
對于控制反轉來說,其中最常見的方式叫做 依賴注入,簡稱為 DI(Dependency Injection)。
組件之間的依賴關系由容器在運行期決定,形象的說,即由容器動態的將某個依賴關系注入到組件之中。依賴注入的目的并非為軟件系統帶來更多功能,而是為了提升組件重用的頻率,并為系統搭建一個靈活、可擴展的平臺。
通過依賴注入機制,我們只需要通過簡單的配置,而無需任何代碼就可指定目標需要的資源,完成自身的業務邏輯,而不需要關心具體的資源來自何處,由誰實現。
理解 DI 的關鍵是 “誰依賴了誰,為什么需要依賴,誰注入了誰,注入了什么”:
誰依賴了誰:當然是應用程序依賴 IoC 容器;
為什么需要依賴:應用程序需要 IoC 容器來提供對象需要的外部資源(包括對象、資源、常量數據);
誰注入誰:很明顯是 IoC 容器注入應用程序依賴的對象;
注入了什么:注入某個對象所需的外部資源(包括對象、資源、常量數據)。
那么 IoC 和 DI 有什么關系?其實它們是同一個概念的不同角度描述,由于控制反轉的概念比較含糊(可能只是理解為容器控制對象這一個層面,很難讓人想到誰來維護依賴關系),所以 2004 年大師級人物 Martin Fowler 又給出了一個新的名字:“依賴注入”,相對 IoC 而言,“依賴注入” 明確描述了被注入對象依賴 IoC 容器配置依賴對象。
總的來說, 控制反轉(Inversion of Control)是說創建對象的控制權發生轉移,以前創建對象的主動權和創建時機由應用程序把控,而現在這種權利轉交給 IoC 容器,它就是一個專門用來創建對象的工廠,你需要什么對象,它就給你什么對象。
有了 IoC 容器,依賴關系就改變了,原先的依賴關系就沒了,它們都依賴 IoC 容器了,通過 IoC 容器來建立它們之間的關系。
前面介紹了那么多的概念,現在我們來看一下未使用依賴注入框架和使用依賴注入框架之間有什么明顯的區別。
4.1 未使用依賴注入框架
假設我們的服務 A 依賴于服務 B,即要使用服務 A 前,我們需要先創建服務 B。具體的流程如下圖所示:
從上圖可知,未使用依賴注入框架時,服務的使用者需要關心服務本身和其依賴的對象是如何創建的,且需要手動維護依賴關系。若服務本身需要依賴多個對象,這樣就會增加使用難度和后期的維護成本。
對于上述的問題,我們可以考慮引入依賴注入框架。下面我們來看一下引入依賴注入框架,整體流程會發生什么變化。
4.2 使用依賴注入框架
使用依賴注入框架之后,系統中的服務會統一注冊到 IoC 容器中,如果服務有依賴其他服務時,也需要對依賴進行聲明。當用戶需要使用特定的服務時,IoC 容器會負責該服務及其依賴對象的創建與管理工作。具體的流程如下圖所示:
到這里我們已經介紹了 IoC 與 DI 的概念及特點,接下來我們來介紹 DI 的應用。
五、DI 的應用
DI 在前端和服務端都有相應的應用,比如在前端領域的代表是 AngularJS 和 Angular,而在服務端領域是 Node.js 生態中比較出名的 NestJS。接下來將簡單介紹一下 DI 在 AngularJS/Angular 和 NestJS 中的應用。
5.1 DI 在 AngularJS 中的應用
在 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 應用程序中所有的依賴項都是單例,我們不能控制是否使用新的實例;
命名空間沖突:在系統中我們使用字符串來標識服務的名稱,假設我們在項目中已有一個 CarService,然而第三方庫中也引入了同樣的服務,這樣的話就容易出現混淆。
由于 AngularJS DI 存在以上的問題,所以在后續的 Angular 重新設計了新的 DI 系統。
5.2 DI 在 Angular 中的應用
以前面汽車的例子為例,我們可以把汽車、發動機、底盤和車身這些認為是一種 “服務”,所以它們會以服務提供者的形式注冊到 DI 系統中。為了能區分不同服務,我們需要使用不同的令牌(Token)來標識它們。接著我們會基于已注冊的服務提供者創建注入器對象。
之后,當我們需要獲取指定服務時,我們就可以通過該服務對應的令牌,從注入器對象中獲取令牌對應的依賴對象。上述的流程的具體如下圖所示:
好的,了解完上述的流程。下面我們來看一下如何使用 Angular 內置的 DI 系統來 “造車”。
5.2.1 car.ts
// 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,具體如下所示:
5.2.2 app.module.ts
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 {}
5.3 DI 在 NestJS 中的應用
NestJS 是構建高效,可擴展的 Node.js Web 應用程序的框架。它使用現代的 JavaScript 或 TypeScript(保留與純 JavaScript 的兼容性),并結合 OOP(面向對象編程),FP(函數式編程)和FRP(函數響應式編程)的元素。
在底層,Nest 使用了 Express,但也提供了與其他各種庫的兼容,例如 Fastify,可以方便地使用各種可用的第三方插件。
近幾年,由于 Node.js,JavaScript 已經成為 Web 前端和后端應用程序的「通用語言」,從而產生了像 Angular、React、Vue 等令人耳目一新的項目,這些項目提高了開發人員的生產力,使得可以快速構建可測試的且可擴展的前端應用程序。然而,在服務器端,雖然有很多優秀的庫、helper 和 Node 工具,但是它們都沒有有效地解決主要問題 —— 架構。
NestJS 旨在提供一個開箱即用的應用程序體系結構,允許輕松創建高度可測試,可擴展,松散耦合且易于維護的應用程序。 在 NestJS 中也為我們開發者提供了依賴注入的功能,這里我們以官網的示例來演示一下依賴注入的功能。
5.3.1 app.service.ts
import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { getHello(): string { return 'Hello World!'; } }
5.3.2 app.controller.ts
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 容器
為了讓大家能更好地理解 IoC 容器的實現代碼,小編先介紹一些相關的前置知識。
6.1 裝飾器
如果你有使用過 Angular 或 NestJS,相信你對以下的代碼不會陌生。
@Injectable() export class HttpService { constructor( private httpClient: HttpClient ) {} }
在以上代碼中,我們使用了 Injectable 裝飾器。該裝飾器用于表示此類可以自動注入其依賴項。其中 @Injectable() 中的 @ 符號屬于語法糖。
裝飾器是一個包裝類,函數或方法并為其添加行為的函數。這對于定義與對象關聯的元數據很有用。裝飾器有以下四種分類:
類裝飾器(Class decorators)
屬性裝飾器(Property decorators)
方法裝飾器(Method decorators)
參數裝飾器(Parameter decorators)
前面示例中使用的 @Injectable() 裝飾器,屬于類裝飾器。在該類裝飾器修飾的 HttpService 類中,我們通過構造注入的方式注入了用于處理 HTTP 請求的 HttpClient 依賴對象。
6.2 反射
@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 只需簡單了解一下即可。在后續的內容中,我們將介紹具體如何使用。這里我們需要注意以下兩個問題:
對于類或函數,我們需要使用裝飾器來修飾它們,這樣才能保存元數據。
只有類、枚舉或原始數據類型能被記錄。接口和聯合類型作為 “對象” 出現。這是因為這些類型在編譯后完全消失,而類卻一直存在。
6.3 定義 Token 和 Provider
了解完裝飾器與反射相關的基礎知識,接下來我們來開始實現 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:
ClassProvider:提供一個類,用于創建依賴對象;
ValueProvider:提供一個已存在的值,作為依賴對象;
FactoryProvider:提供一個工廠方法,用于創建依賴對象。
// 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; }
6.4 定義裝飾器
在前面我們已經提過了,對于類或函數,我們需要使用裝飾器來修飾它們,這樣才能保存元數據。因此,接下來我們來分別創建 Injectable 和 Inject 裝飾器。
6.4.1 Injectable 裝飾器
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 裝飾器。
6.4.2 Inject 裝飾器
接下來我們來創建 Inject 裝飾器,該裝飾器屬于參數裝飾器。在 TypeScript 中,參數裝飾器的聲明如下:
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number ) => void
參數裝飾器顧名思義,是用來裝飾函數參數,它接收三個參數:
target: Object —— 被裝飾的類;
propertyKey: string | symbol —— 方法名;
parameterIndex: number —— 方法中參數的索引值。
下面我們來看一下 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 ) {} }
6.5 實現 IoC 容器
目前為止,我們已經定義了 Token、Provider、Injectable 和 Inject 裝飾器。接下來我們來實現前面所提到的 IoC 容器的 API:
export class Container { addProvider<T>(provider: Provider<T>) {} // TODO inject<T>(type: Token<T>): T {} // TODO }
6.5.1 實現 addProvider 方法
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 方法。
6.5.2 實現 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
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 這個庫。
到此,關于“如何使用TypeScript實現一個IoC容器”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。