如何使用TypeScript實現(xiàn)一個IoC容器

這篇文章主要介紹“如何使用TypeScript實現(xiàn)一個IoC容器”,在日常操作中,相信很多人在如何使用TypeScript實現(xiàn)一個IoC容器問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”如何使用TypeScript實現(xiàn)一個IoC容器”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

成都創(chuàng)新互聯(lián)公司主要從事成都做網(wǎng)站、網(wǎng)站設(shè)計、網(wǎng)頁設(shè)計、企業(yè)做網(wǎng)站、公司建網(wǎng)站等業(yè)務(wù)。立足成都服務(wù)樺南,十年網(wǎng)站建設(shè)經(jīng)驗,價格優(yōu)惠、服務(wù)專業(yè),歡迎來電咨詢建站服務(wù):18982081108

一、背景概述

在介紹什么是 IoC 容器之前,來舉一個日常工作中很常見的場景,即創(chuàng)建指定類的實例。

最簡單的情形是該類沒有依賴其他類,但現(xiàn)實往往是殘酷的,我們在創(chuàng)建某個類的實例時,需要依賴不同類對應(yīng)的實例。為了讓小伙伴們能夠更好地理解上述的內(nèi)容,阿寶哥來舉一個例子。

一輛小汽車 ? 通常由 發(fā)動機、底盤、車身和電氣設(shè)備  四大部分組成。汽車電氣設(shè)備的內(nèi)部構(gòu)造很復(fù)雜,簡單起見,我們只考慮三個部分:發(fā)動機、底盤和車身。

如何使用TypeScript實現(xiàn)一個IoC容器

在現(xiàn)實生活中,要造輛車還是很困難的。而在軟件的世界中,這可難不倒我們。

如何使用TypeScript實現(xiàn)一個IoC容器

在開始造車前,我們得先看一下 “圖紙”:

如何使用TypeScript實現(xiàn)一個IoC容器

看完上面的 “圖紙”,我們馬上來開啟造車之旅。第一步我們先來定義車身類:

1.定義車身類

export default class Body { }

2.定義底盤類

export default class Chassis { }

3.定義引擎類

export default class Engine {   start() {     console.log("引擎發(fā)動了");   } }

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();     } }

一切已準(zhǔn)備就緒,我們馬上來造一輛車:

const car = new Car(); // 阿寶哥造輛新車 car.run(); // 控制臺輸出:引擎發(fā)動了

現(xiàn)在雖然車已經(jīng)可以啟動了,但卻存在以下問題:

問題一:在造車的時候,你不能選擇配置。比如你想更換汽車引擎的話,按照目前的方案,是實現(xiàn)不了的。

問題二:在汽車類內(nèi)部,你需要在構(gòu)造函數(shù)中手動去創(chuàng)建汽車的各個部件。

為了解決第一個問題,提供更靈活的方案,我們可以重構(gòu)一下已定義的汽車類,具體如下:

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();     } }

重構(gòu)完汽車類,我們來重新造輛新車:

const engine = new NewEngine(); const body = new Body(); const chassis = new Chassis();  const newCar = new Car(engine, body, chassis); newCar.run();

此時我們已經(jīng)解決了上面提到的第一個問題,要解決第二個問題我們要來了解一下 IoC(控制反轉(zhuǎn))的概念。

二、IoC 是什么

IoC(Inversion of Control),即 “控制反轉(zhuǎn)”。在開發(fā)中, IoC  意味著你設(shè)計好的對象交給容器控制,而不是使用傳統(tǒng)的方式,在對象內(nèi)部直接控制。

如何理解好 IoC 呢?理解好 IoC 的關(guān)鍵是要明確 “誰控制誰,控制什么,為何是反轉(zhuǎn),哪些方面反轉(zhuǎn)了”,我們來深入分析一下。

  • 誰控制誰,控制什么:在傳統(tǒng)的程序設(shè)計中,我們直接在對象內(nèi)部通過 new 的方式創(chuàng)建對象,是程序主動創(chuàng)建依賴對象;而 IoC  是有專門一個容器來創(chuàng)建這些對象,即由 IoC 容器控制對象的創(chuàng)建;

誰控制誰?當(dāng)然是 IoC 容器控制了對象;控制什么?主要是控制外部資源(依賴對象)獲取。

  • 為何是反轉(zhuǎn)了,哪些方面反轉(zhuǎn)了:有反轉(zhuǎn)就有正轉(zhuǎn),傳統(tǒng)應(yīng)用程序是由我們自己在程序中主動控制去獲取依賴對象,也就是正轉(zhuǎn);而反轉(zhuǎn)則是由容器來幫忙創(chuàng)建及注入依賴對象;

為何是反轉(zhuǎn)?因為由容器幫我們查找及注入依賴對象,對象只是被動的接受依賴對象,所以是反轉(zhuǎn)了;哪些方面反轉(zhuǎn)了?依賴對象的獲取被反轉(zhuǎn)了。

三、IoC 能做什么

IoC 不是一種技術(shù),只是一種思想,是面向?qū)ο缶幊讨械囊环N設(shè)計原則,可以用來減低計算機代碼之間的耦合度。

傳統(tǒng)應(yīng)用程序都是由我們在類內(nèi)部主動創(chuàng)建依賴對象,從而導(dǎo)致類與類之間高耦合,難于測試;有了 IoC  容器后,把創(chuàng)建和查找依賴對象的控制權(quán)交給了容器,由容器注入組合對象,所以對象之間是松散耦合。  這樣也便于測試,利于功能復(fù)用,更重要的是使得程序的整個體系結(jié)構(gòu)變得非常靈活。

其實 IoC 對編程帶來的最大改變不是從代碼上,而是思想上,發(fā)生了 “主從換位” 的變化。應(yīng)用程序本來是老大,要獲取什么資源都是主動出擊,但在 IoC  思想中,應(yīng)用程序就變成被動了,被動的等待 IoC 容器來創(chuàng)建并注入它所需的資源了。

四、IoC 與 DI 之間的關(guān)系

對于控制反轉(zhuǎn)來說,其中最常見的方式叫做 依賴注入,簡稱為 DI(Dependency Injection)。

組件之間的依賴關(guān)系由容器在運行期決定,形象的說,即由容器動態(tài)的將某個依賴關(guān)系注入到組件之中。依賴注入的目的并非為軟件系統(tǒng)帶來更多功能,而是為了提升組件重用的頻率,并為系統(tǒng)搭建一個靈活、可擴展的平臺。

通過依賴注入機制,我們只需要通過簡單的配置,而無需任何代碼就可指定目標(biāo)需要的資源,完成自身的業(yè)務(wù)邏輯,而不需要關(guān)心具體的資源來自何處,由誰實現(xiàn)。

理解 DI 的關(guān)鍵是 “誰依賴了誰,為什么需要依賴,誰注入了誰,注入了什么”:

  • 誰依賴了誰:當(dāng)然是應(yīng)用程序依賴 IoC 容器;

  • 為什么需要依賴:應(yīng)用程序需要 IoC 容器來提供對象需要的外部資源(包括對象、資源、常量數(shù)據(jù));

  • 誰注入誰:很明顯是 IoC 容器注入應(yīng)用程序依賴的對象;

  • 注入了什么:注入某個對象所需的外部資源(包括對象、資源、常量數(shù)據(jù))。

那么 IoC 和 DI  有什么關(guān)系?其實它們是同一個概念的不同角度描述,由于控制反轉(zhuǎn)的概念比較含糊(可能只是理解為容器控制對象這一個層面,很難讓人想到誰來維護依賴關(guān)系),所以 2004  年大師級人物 Martin Fowler 又給出了一個新的名字:“依賴注入”,相對 IoC 而言,“依賴注入” 明確描述了被注入對象依賴 IoC  容器配置依賴對象。

總的來說, 控制反轉(zhuǎn)(Inversion of  Control)是說創(chuàng)建對象的控制權(quán)發(fā)生轉(zhuǎn)移,以前創(chuàng)建對象的主動權(quán)和創(chuàng)建時機由應(yīng)用程序把控,而現(xiàn)在這種權(quán)利轉(zhuǎn)交給 IoC  容器,它就是一個專門用來創(chuàng)建對象的工廠,你需要什么對象,它就給你什么對象。

有了 IoC 容器,依賴關(guān)系就改變了,原先的依賴關(guān)系就沒了,它們都依賴 IoC 容器了,通過 IoC 容器來建立它們之間的關(guān)系。

前面介紹了那么多的概念,現(xiàn)在我們來看一下未使用依賴注入框架和使用依賴注入框架之間有什么明顯的區(qū)別。

4.1 未使用依賴注入框架

假設(shè)我們的服務(wù) A 依賴于服務(wù) B,即要使用服務(wù) A 前,我們需要先創(chuàng)建服務(wù) B。具體的流程如下圖所示:

如何使用TypeScript實現(xiàn)一個IoC容器

從上圖可知,未使用依賴注入框架時,服務(wù)的使用者需要關(guān)心服務(wù)本身和其依賴的對象是如何創(chuàng)建的,且需要手動維護依賴關(guān)系。若服務(wù)本身需要依賴多個對象,這樣就會增加使用難度和后期的維護成本。

對于上述的問題,我們可以考慮引入依賴注入框架。下面我們來看一下引入依賴注入框架,整體流程會發(fā)生什么變化。

4.2 使用依賴注入框架

使用依賴注入框架之后,系統(tǒng)中的服務(wù)會統(tǒng)一注冊到 IoC 容器中,如果服務(wù)有依賴其他服務(wù)時,也需要對依賴進行聲明。當(dāng)用戶需要使用特定的服務(wù)時,IoC  容器會負(fù)責(zé)該服務(wù)及其依賴對象的創(chuàng)建與管理工作。具體的流程如下圖所示:

如何使用TypeScript實現(xiàn)一個IoC容器

到這里我們已經(jīng)介紹了 IoC 與 DI 的概念及特點,接下來我們來介紹 DI 的應(yīng)用。

五、DI 的應(yīng)用

DI 在前端和服務(wù)端都有相應(yīng)的應(yīng)用,比如在前端領(lǐng)域的代表是 AngularJS 和 Angular,而在服務(wù)端領(lǐng)域是 Node.js 生態(tài)中比較出名的  NestJS。接下來將簡單介紹一下 DI 在 AngularJS/Angular 和 NestJS 中的應(yīng)用。

5.1 DI 在 AngularJS 中的應(yīng)用

在 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 的普及和應(yīng)用的復(fù)雜度不斷提高,AngularJS DI 系統(tǒng)的問題就暴露出來了。

這里阿寶哥簡單介紹一下 AngularJS DI 系統(tǒng)存在的幾個問題:

  • 內(nèi)部緩存:AngularJS 應(yīng)用程序中所有的依賴項都是單例,我們不能控制是否使用新的實例;

  • 命名空間沖突:在系統(tǒng)中我們使用字符串來標(biāo)識服務(wù)的名稱,假設(shè)我們在項目中已有一個  CarService,然而第三方庫中也引入了同樣的服務(wù),這樣的話就容易出現(xiàn)混淆。

由于 AngularJS DI 存在以上的問題,所以在后續(xù)的 Angular 重新設(shè)計了新的 DI 系統(tǒng)。

5.2 DI 在 Angular 中的應(yīng)用

以前面汽車的例子為例,我們可以把汽車、發(fā)動機、底盤和車身這些認(rèn)為是一種 “服務(wù)”,所以它們會以服務(wù)提供者的形式注冊到 DI  系統(tǒng)中。為了能區(qū)分不同服務(wù),我們需要使用不同的令牌(Token)來標(biāo)識它們。接著我們會基于已注冊的服務(wù)提供者創(chuàng)建注入器對象。

之后,當(dāng)我們需要獲取指定服務(wù)時,我們就可以通過該服務(wù)對應(yīng)的令牌,從注入器對象中獲取令牌對應(yīng)的依賴對象。上述的流程的具體如下圖所示:

如何使用TypeScript實現(xiàn)一個IoC容器

好的,了解完上述的流程。下面我們來看一下如何使用 Angular 內(nèi)置的 DI 系統(tǒng)來 “造車”。

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('引擎發(fā)動了');   } }  @Injectable() export default class Car {   // 使用構(gòu)造注入方式注入依賴對象   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();

在以上代碼中我們調(diào)用 ReflectiveInjector 對象的 resolveAndCreate 方法手動創(chuàng)建注入器,然后根據(jù)車輛對應(yīng)的 Token  來獲取對應(yīng)的依賴對象。通過觀察上述代碼,你可以發(fā)現(xiàn),我們已經(jīng)不需要手動地管理和維護依賴對象了,這些 “臟活”、“累活” 已經(jīng)交給注入器來處理了。

此外,如果要能正常獲取汽車對象,我們還需要在 app.module.ts 文件中聲明 Car 對應(yīng) 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 中的應(yīng)用

NestJS 是構(gòu)建高效,可擴展的 Node.js Web 應(yīng)用程序的框架。它使用現(xiàn)代的 JavaScript 或 TypeScript(保留與純  JavaScript 的兼容性),并結(jié)合 OOP(面向?qū)ο缶幊?,F(xiàn)P(函數(shù)式編程)和FRP(函數(shù)響應(yīng)式編程)的元素。

在底層,Nest 使用了 Express,但也提供了與其他各種庫的兼容,例如 Fastify,可以方便地使用各種可用的第三方插件。

近幾年,由于 Node.js,JavaScript 已經(jīng)成為 Web 前端和后端應(yīng)用程序的「通用語言」,從而產(chǎn)生了像 Angular、React、Vue  等令人耳目一新的項目,這些項目提高了開發(fā)人員的生產(chǎn)力,使得可以快速構(gòu)建可測試的且可擴展的前端應(yīng)用程序。然而,在服務(wù)器端,雖然有很多優(yōu)秀的庫、helper 和  Node 工具,但是它們都沒有有效地解決主要問題 —— 架構(gòu)。

NestJS 旨在提供一個開箱即用的應(yīng)用程序體系結(jié)構(gòu),允許輕松創(chuàng)建高度可測試,可擴展,松散耦合且易于維護的應(yīng)用程序。 在 NestJS  中也為我們開發(fā)者提供了依賴注入的功能,這里我們以官網(wǎng)的示例來演示一下依賴注入的功能。

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 中,我們通過構(gòu)造注入的方式注入了 AppService 對象,當(dāng)用戶訪問首頁的時候,我們會調(diào)用 AppService  對象的 getHello 方法來獲取 'Hello World!' 消息,并把消息返回給用戶。當(dāng)然為了保證依賴注入可以正常工作,我們還需要在 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 應(yīng)用,功能強大、輕量的 IoC 容器。

對 InversifyJS 感興趣的小伙伴可以自行了解一下,阿寶哥就不繼續(xù)展開介紹了。接下來,我們將進入本文的重點,即介紹如何使用 TypeScript  實現(xiàn)一個簡單的 IoC 容器,該容器實現(xiàn)的功能如下圖所示:

如何使用TypeScript實現(xiàn)一個IoC容器

六、手寫 IoC 容器

為了讓大家能更好地理解 IoC 容器的實現(xiàn)代碼,小編先介紹一些相關(guān)的前置知識。

6.1 裝飾器

如果你有使用過 Angular 或 NestJS,相信你對以下的代碼不會陌生。

@Injectable() export class HttpService {   constructor(     private httpClient: HttpClient   ) {} }

在以上代碼中,我們使用了 Injectable 裝飾器。該裝飾器用于表示此類可以自動注入其依賴項。其中 @Injectable() 中的 @  符號屬于語法糖。

裝飾器是一個包裝類,函數(shù)或方法并為其添加行為的函數(shù)。這對于定義與對象關(guān)聯(lián)的元數(shù)據(jù)很有用。裝飾器有以下四種分類:

  • 類裝飾器(Class decorators)

  • 屬性裝飾器(Property decorators)

  • 方法裝飾器(Method decorators)

  • 參數(shù)裝飾器(Parameter decorators)

前面示例中使用的 @Injectable() 裝飾器,屬于類裝飾器。在該類裝飾器修飾的 HttpService 類中,我們通過構(gòu)造注入的方式注入了用于處理  HTTP 請求的 HttpClient 依賴對象。

6.2 反射

@Injectable() export class HttpService {   constructor(     private httpClient: HttpClient   ) {} }

以上代碼若設(shè)置編譯的目標(biāo)為 ES5,則會生成以下代碼:

// 忽略__decorate函數(shù)等代碼 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; }());

通過觀察上述代碼,你會發(fā)現(xiàn) HttpService 構(gòu)造函數(shù)中 httpClient 參數(shù)的類型被擦除了,這是因為 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 只需簡單了解一下即可。在后續(xù)的內(nèi)容中,我們將介紹具體如何使用。這里我們需要注意以下兩個問題:

  • 對于類或函數(shù),我們需要使用裝飾器來修飾它們,這樣才能保存元數(shù)據(jù)。

  • 只有類、枚舉或原始數(shù)據(jù)類型能被記錄。接口和聯(lián)合類型作為 “對象” 出現(xiàn)。這是因為這些類型在編譯后完全消失,而類卻一直存在。

6.3 定義 Token 和 Provider

了解完裝飾器與反射相關(guān)的基礎(chǔ)知識,接下來我們來開始實現(xiàn) IoC 容器。我們的 IoC  容器將使用兩個主要的概念:令牌(Token)和提供者(Provider)。令牌是 IoC 容器所要創(chuàng)建對象的標(biāo)識符,而提供者用于描述如何創(chuàng)建這些對象。

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 類型是一個聯(lián)合類型,既可以是一個函數(shù)類型也可以是 InjectionToken 類型。AngularJS 中使用字符串作為  Token,在某些情況下,可能會導(dǎo)致沖突。因此,為了解決這個問題,我們定義了 InjectionToken 類,來避免出現(xiàn)命名沖突問題。

定義完 Token 類型,接下來我們來定義三種不同類型的 Provider:

  • ClassProvider:提供一個類,用于創(chuàng)建依賴對象;

  • ValueProvider:提供一個已存在的值,作為依賴對象;

  • FactoryProvider:提供一個工廠方法,用于創(chuàng)建依賴對象。

// 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>;

為了更方便的區(qū)分這三種不同類型的 Provider,我們自定義了三個類型守衛(wèi)函數(shù):

// 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 定義裝飾器

在前面我們已經(jīng)提過了,對于類或函數(shù),我們需要使用裝飾器來修飾它們,這樣才能保存元數(shù)據(jù)。因此,接下來我們來分別創(chuàng)建 Injectable 和 Inject  裝飾器。

6.4.1 Injectable 裝飾器

Injectable 裝飾器用于表示此類可以自動注入其依賴項,該裝飾器屬于類裝飾器。在 TypeScript 中,類裝飾器的聲明如下:

declare type ClassDecorator = <TFunction extends Function>(target: TFunction)    => TFunction | void;

類裝飾器顧名思義,就是用來裝飾類的。它接收一個參數(shù):target: TFunction,表示被裝飾的類。下面我們來看一下 Injectable  裝飾器的具體實現(xiàn):

// 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;   }; }

在以上代碼中,當(dāng)調(diào)用完 Injectable 函數(shù)之后,會返回一個新的函數(shù)。在新的函數(shù)中,我們使用 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() 語法糖就可以應(yīng)用該裝飾器:

@Injectable() export class HttpService {   constructor(     private httpClient: HttpClient   ) {} }

在以上示例中,我們注入的是 Type 類型的 HttpClient 對象。但在實際的項目中,往往會比較復(fù)雜。除了需要注入 Type  類型的依賴對象之外,我們還可能會注入其他類型的依賴對象,比如我們希望在 HttpService 服務(wù)中注入遠(yuǎn)程服務(wù)器的 API 地址。針對這種情形,我們需要使用  Inject 裝飾器。

6.4.2 Inject 裝飾器

接下來我們來創(chuàng)建 Inject 裝飾器,該裝飾器屬于參數(shù)裝飾器。在 TypeScript 中,參數(shù)裝飾器的聲明如下:

declare type ParameterDecorator = (target: Object,    propertyKey: string | symbol, parameterIndex: number ) => void

參數(shù)裝飾器顧名思義,是用來裝飾函數(shù)參數(shù),它接收三個參數(shù):

  • target: Object &mdash;&mdash; 被裝飾的類;

  • propertyKey: string | symbol &mdash;&mdash; 方法名;

  • parameterIndex: number &mdash;&mdash; 方法中參數(shù)的索引值。

下面我們來看一下 Inject 裝飾器的具體實現(xiàn):

// 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;   }; }

在以上代碼中,當(dāng)調(diào)用完 Inject 函數(shù)之后,會返回一個新的函數(shù)。在新的函數(shù)中,我們使用 reflect-metadata 這個庫提供的  defineMetadata API 來保存參數(shù)相關(guān)的元信息。這里是保存 index 索引信息和 Token 信息。

定義完 Inject 裝飾器,我們就可以利用它來注入我們前面所提到的遠(yuǎn)程服務(wù)器的 API 地址,具體的使用方式如下:

const API_URL = new InjectionToken('apiUrl');  @Injectable() export class HttpService {   constructor(     private httpClient: HttpClient,     @Inject(API_URL) private apiUrl: string   ) {} }

6.5 實現(xiàn) IoC 容器

目前為止,我們已經(jīng)定義了 Token、Provider、Injectable 和 Inject 裝飾器。接下來我們來實現(xiàn)前面所提到的 IoC 容器的  API:

export class Container {   addProvider<T>(provider: Provider<T>) {} // TODO   inject<T>(type: Token<T>): T {} // TODO }

6.5.1 實現(xiàn) addProvider 方法

addProvider() 方法的實現(xiàn)很簡單,我們使用 Map 來存儲 Token 與 Provider 之間的關(guān)系:

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() 方法內(nèi)部除了把 Token 與 Provider 的對應(yīng)信息保存到 providers 對象中之外,我們定義了一個  assertInjectableIfClassProvider 方法,用于確保添加的 ClassProvider 是可注入的。該方法的具體實現(xiàn)如下:

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 方法體中,我們使用了前面已經(jīng)介紹的 isClassProvider  類型守衛(wèi)函數(shù)來判斷是否為 ClassProvider,如果是的話,會判斷該 ClassProvider 是否為可注入的,具體使用的是 isInjectable  函數(shù),該函數(shù)的定義如下:

export function isInjectable<T>(target: Type<T>) {   return Reflect.getMetadata(INJECTABLE_METADATA_KEY, target) === true; }

在 isInjectable 函數(shù)中,我們使用 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  對應(yīng)的類是不可注入的,則會拋出異常。為了讓異常消息更加友好,也更加直觀。我們定義了一個 getTokenName 方法來獲取 Token 對應(yīng)的名稱:

private getTokenName<T>(token: Token<T>) {   return token instanceof InjectionToken     ? token.injectionIdentifier     : token.name; }

現(xiàn)在我們已經(jīng)實現(xiàn)了 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 僅是為了演示而已。下面我們來實現(xiàn) Container 類中核心的  inject 方法。

6.5.2 實現(xiàn) inject 方法

在看 inject 方法的具體實現(xiàn)之前,我們先來看一下該方法所實現(xiàn)的功能:

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 方法所實現(xiàn)的功能就是根據(jù) Token 獲取與之對應(yīng)的對象。在前面實現(xiàn)的  addProvider 方法中,我們把 Token 和該 Token 對應(yīng)的 Provider 保存在 providers Map 對象中。所以在 inject  方法中,我們可以先從 providers 對象中獲取該 Token 對應(yīng)的 Provider 對象,然后在根據(jù)不同類型的 Provider  來獲取其對應(yīng)的對象。

好的,下面我們來看一下 inject 方法的具體實現(xiàn):

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 裝飾器來裝飾某個類。對于這個特殊場景,我們會根據(jù)傳入的 type 參數(shù)來創(chuàng)建一個 provider 對象,然后進一步調(diào)用  injectWithProvider 方法來創(chuàng)建對象,該方法的具體實現(xiàn)如下:

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 方法內(nèi)部,我們會使用前面定義的用于區(qū)分三種不同類型 Provider 的類型守衛(wèi)函數(shù)來處理不同的  Provider。這里我們先來看一下最簡單 ValueProvider,當(dāng)發(fā)現(xiàn)注入的是 ValueProvider 類型時,則會調(diào)用 injectValue  方法來獲取其對應(yīng)的對象:

// { provide: API_URL, useValue: 'https://www.semlinker.com/' } private injectValue<T>(valueProvider: ValueProvider<T>): T {   return valueProvider.useValue; }

接著我們來看如何處理 FactoryProvider 類型的 Provider,如果發(fā)現(xiàn)是 FactoryProvider 類型時,則會調(diào)用  injectFactory 方法來獲取其對應(yīng)的對象,該方法的實現(xiàn)也很簡單:

// const input = { x: 200 }; // container.addProvider({ provide: BasicClass, useFactory: () => input }); private injectFactory<T>(valueProvider: FactoryProvider<T>): T {   return valueProvider.useFactory(); }

最后我們來分析一下如何處理 ClassProvider,對于 ClassProvider 類說,通過 Provider 對象的 useClass  屬性,我們就可以直接獲取到類對應(yīng)的構(gòu)造函數(shù)。最簡單的情形是該類沒有依賴其他對象,但在大多數(shù)場景下,即將實例化的服務(wù)類是會依賴其他的對象的。所以在實例化服務(wù)類前,我們需要構(gòu)造其依賴的對象。

那么現(xiàn)在問題來了,怎么獲取類所依賴的對象呢?我們先來分析一下以下代碼:

const API_URL = new InjectionToken('apiUrl');  @Injectable() export class HttpService {   constructor(     private httpClient: HttpClient,     @Inject(API_URL) private apiUrl: string   ) {} }

以上代碼若設(shè)置編譯的目標(biāo)為 ES5,則會生成以下代碼:

// 已省略__decorate函數(shù)的定義 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 中的兩個參數(shù)。首先我們先來分析 apiUrl 參數(shù):

如何使用TypeScript實現(xiàn)一個IoC容器

在圖中我們可以很清楚地看到,API_URL 對應(yīng)的 Token 最終會通過 Reflect.defineMetadata API 進行保存,所使用的  Key 是 Symbol('INJECT_KEY')。而對于另一個參數(shù)即 httpClient,它使用的 Key 是  "design:paramtypes",它用于修飾目標(biāo)對象方法的參數(shù)類型。

除了 "design:paramtypes" 之外,還有其他的 metadataKey,比如 design:type  和design:returntype,它們分別用于修飾目標(biāo)對象的類型和修飾目標(biāo)對象方法返回值的類型。

如何使用TypeScript實現(xiàn)一個IoC容器

由上圖可知,HttpService 構(gòu)造函數(shù)的參數(shù)類型最終會使用 Reflect.metadata API  進行存儲。了解完上述的知識,接下來我們來定義一個 getInjectedParams 方法,用于獲取類構(gòu)造函數(shù)中聲明的依賴對象,該方法的具體實現(xiàn)如下:

type InjectableParam = Type<any>; const REFLECT_PARAMS = "design:paramtypes";  private getInjectedParams<T>(target: Type<T>) {   // 獲取參數(shù)的類型   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| InjectionToken 聯(lián)合類型,所以在 getInjectedParams  方法中我們也要考慮 InjectionToken 的情形,因此我們定義了一個 getInjectionToken 方法來獲取使用 @Inject 裝飾器注冊的  Token,該方法的實現(xiàn)很簡單:

export function getInjectionToken(target: any, index: number) {   return Reflect.getMetadata(INJECT_METADATA_KEY, target, `index-${index}`) as Token<any> | undefined; }

現(xiàn)在我們已經(jīng)可以獲取類構(gòu)造函數(shù)中所依賴的對象,基于前面定義的 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 容器中定義的兩個方法都已經(jīng)實現(xiàn)了,我們來看一下 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;   } }

最后我們來簡單測試一下我們前面開發(fā)的 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);

以上代碼成功運行后,控制臺會輸出以下結(jié)果:

HttpService {   httpClient: HttpClient {},   apiUrl: 'https://www.semlinker.com/' }

很明顯該結(jié)果正是我們所期望的,這表示我們 IoC 容器已經(jīng)可以正常工作了。當(dāng)然在實際項目中,一個成熟的 IoC  容器還要考慮很多東西,如果小伙伴想在項目中使用的話,建議可以考慮使用 InversifyJS 這個庫。

到此,關(guān)于“如何使用TypeScript實現(xiàn)一個IoC容器”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

當(dāng)前文章:如何使用TypeScript實現(xiàn)一個IoC容器
分享網(wǎng)址:http://muchs.cn/article20/gedcco.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供用戶體驗、手機網(wǎng)站建設(shè)、品牌網(wǎng)站設(shè)計、服務(wù)器托管、面包屑導(dǎo)航網(wǎng)站營銷

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

搜索引擎優(yōu)化