您好,登錄后才能下訂單哦!
這篇文章主要介紹“如何使用TypeScript實(shí)現(xiàn)一個(gè)IoC容器”,在日常操作中,相信很多人在如何使用TypeScript實(shí)現(xiàn)一個(gè)IoC容器問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”如何使用TypeScript實(shí)現(xiàn)一個(gè)IoC容器”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!
一、背景概述
在介紹什么是 IoC 容器之前,來(lái)舉一個(gè)日常工作中很常見(jiàn)的場(chǎng)景,即創(chuàng)建指定類(lèi)的實(shí)例。
最簡(jiǎn)單的情形是該類(lèi)沒(méi)有依賴(lài)其他類(lèi),但現(xiàn)實(shí)往往是殘酷的,我們?cè)趧?chuàng)建某個(gè)類(lèi)的實(shí)例時(shí),需要依賴(lài)不同類(lèi)對(duì)應(yīng)的實(shí)例。為了讓小伙伴們能夠更好地理解上述的內(nèi)容,阿寶哥來(lái)舉一個(gè)例子。
一輛小汽車(chē) ? 通常由 發(fā)動(dòng)機(jī)、底盤(pán)、車(chē)身和電氣設(shè)備 四大部分組成。汽車(chē)電氣設(shè)備的內(nèi)部構(gòu)造很復(fù)雜,簡(jiǎn)單起見(jiàn),我們只考慮三個(gè)部分:發(fā)動(dòng)機(jī)、底盤(pán)和車(chē)身。
在現(xiàn)實(shí)生活中,要造輛車(chē)還是很困難的。而在軟件的世界中,這可難不倒我們。
在開(kāi)始造車(chē)前,我們得先看一下 “圖紙”:
看完上面的 “圖紙”,我們馬上來(lái)開(kāi)啟造車(chē)之旅。第一步我們先來(lái)定義車(chē)身類(lèi):
1.定義車(chē)身類(lèi)
export default class Body { }
2.定義底盤(pán)類(lèi)
export default class Chassis { }
3.定義引擎類(lèi)
export default class Engine { start() { console.log("引擎發(fā)動(dòng)了"); } }
4.定義汽車(chē)類(lèi)
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)備就緒,我們馬上來(lái)造一輛車(chē):
const car = new Car(); // 阿寶哥造輛新車(chē) car.run(); // 控制臺(tái)輸出:引擎發(fā)動(dòng)了
現(xiàn)在雖然車(chē)已經(jīng)可以啟動(dòng)了,但卻存在以下問(wèn)題:
問(wèn)題一:在造車(chē)的時(shí)候,你不能選擇配置。比如你想更換汽車(chē)引擎的話,按照目前的方案,是實(shí)現(xiàn)不了的。
問(wèn)題二:在汽車(chē)類(lèi)內(nèi)部,你需要在構(gòu)造函數(shù)中手動(dòng)去創(chuàng)建汽車(chē)的各個(gè)部件。
為了解決第一個(gè)問(wèn)題,提供更靈活的方案,我們可以重構(gòu)一下已定義的汽車(chē)類(lèi),具體如下:
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)完汽車(chē)類(lèi),我們來(lái)重新造輛新車(chē):
const engine = new NewEngine(); const body = new Body(); const chassis = new Chassis(); const newCar = new Car(engine, body, chassis); newCar.run();
此時(shí)我們已經(jīng)解決了上面提到的第一個(gè)問(wèn)題,要解決第二個(gè)問(wèn)題我們要來(lái)了解一下 IoC(控制反轉(zhuǎn))的概念。
二、IoC 是什么
IoC(Inversion of Control),即 “控制反轉(zhuǎn)”。在開(kāi)發(fā)中, IoC 意味著你設(shè)計(jì)好的對(duì)象交給容器控制,而不是使用傳統(tǒng)的方式,在對(duì)象內(nèi)部直接控制。
如何理解好 IoC 呢?理解好 IoC 的關(guān)鍵是要明確 “誰(shuí)控制誰(shuí),控制什么,為何是反轉(zhuǎn),哪些方面反轉(zhuǎn)了”,我們來(lái)深入分析一下。
誰(shuí)控制誰(shuí),控制什么:在傳統(tǒng)的程序設(shè)計(jì)中,我們直接在對(duì)象內(nèi)部通過(guò) new 的方式創(chuàng)建對(duì)象,是程序主動(dòng)創(chuàng)建依賴(lài)對(duì)象;而 IoC 是有專(zhuān)門(mén)一個(gè)容器來(lái)創(chuàng)建這些對(duì)象,即由 IoC 容器控制對(duì)象的創(chuàng)建;
誰(shuí)控制誰(shuí)?當(dāng)然是 IoC 容器控制了對(duì)象;控制什么?主要是控制外部資源(依賴(lài)對(duì)象)獲取。
為何是反轉(zhuǎn)了,哪些方面反轉(zhuǎn)了:有反轉(zhuǎn)就有正轉(zhuǎn),傳統(tǒng)應(yīng)用程序是由我們自己在程序中主動(dòng)控制去獲取依賴(lài)對(duì)象,也就是正轉(zhuǎn);而反轉(zhuǎn)則是由容器來(lái)幫忙創(chuàng)建及注入依賴(lài)對(duì)象;
為何是反轉(zhuǎn)?因?yàn)橛扇萜鲙臀覀儾檎壹白⑷胍蕾?lài)對(duì)象,對(duì)象只是被動(dòng)的接受依賴(lài)對(duì)象,所以是反轉(zhuǎn)了;哪些方面反轉(zhuǎn)了?依賴(lài)對(duì)象的獲取被反轉(zhuǎn)了。
三、IoC 能做什么
IoC 不是一種技術(shù),只是一種思想,是面向?qū)ο缶幊讨械囊环N設(shè)計(jì)原則,可以用來(lái)減低計(jì)算機(jī)代碼之間的耦合度。
傳統(tǒng)應(yīng)用程序都是由我們?cè)陬?lèi)內(nèi)部主動(dòng)創(chuàng)建依賴(lài)對(duì)象,從而導(dǎo)致類(lèi)與類(lèi)之間高耦合,難于測(cè)試;有了 IoC 容器后,把創(chuàng)建和查找依賴(lài)對(duì)象的控制權(quán)交給了容器,由容器注入組合對(duì)象,所以對(duì)象之間是松散耦合。 這樣也便于測(cè)試,利于功能復(fù)用,更重要的是使得程序的整個(gè)體系結(jié)構(gòu)變得非常靈活。
其實(shí) IoC 對(duì)編程帶來(lái)的最大改變不是從代碼上,而是思想上,發(fā)生了 “主從換位” 的變化。應(yīng)用程序本來(lái)是老大,要獲取什么資源都是主動(dòng)出擊,但在 IoC 思想中,應(yīng)用程序就變成被動(dòng)了,被動(dòng)的等待 IoC 容器來(lái)創(chuàng)建并注入它所需的資源了。
四、IoC 與 DI 之間的關(guān)系
對(duì)于控制反轉(zhuǎn)來(lái)說(shuō),其中最常見(jiàn)的方式叫做 依賴(lài)注入,簡(jiǎn)稱(chēng)為 DI(Dependency Injection)。
組件之間的依賴(lài)關(guān)系由容器在運(yùn)行期決定,形象的說(shuō),即由容器動(dòng)態(tài)的將某個(gè)依賴(lài)關(guān)系注入到組件之中。依賴(lài)注入的目的并非為軟件系統(tǒng)帶來(lái)更多功能,而是為了提升組件重用的頻率,并為系統(tǒng)搭建一個(gè)靈活、可擴(kuò)展的平臺(tái)。
通過(guò)依賴(lài)注入機(jī)制,我們只需要通過(guò)簡(jiǎn)單的配置,而無(wú)需任何代碼就可指定目標(biāo)需要的資源,完成自身的業(yè)務(wù)邏輯,而不需要關(guān)心具體的資源來(lái)自何處,由誰(shuí)實(shí)現(xiàn)。
理解 DI 的關(guān)鍵是 “誰(shuí)依賴(lài)了誰(shuí),為什么需要依賴(lài),誰(shuí)注入了誰(shuí),注入了什么”:
誰(shuí)依賴(lài)了誰(shuí):當(dāng)然是應(yīng)用程序依賴(lài) IoC 容器;
為什么需要依賴(lài):應(yīng)用程序需要 IoC 容器來(lái)提供對(duì)象需要的外部資源(包括對(duì)象、資源、常量數(shù)據(jù));
誰(shuí)注入誰(shuí):很明顯是 IoC 容器注入應(yīng)用程序依賴(lài)的對(duì)象;
注入了什么:注入某個(gè)對(duì)象所需的外部資源(包括對(duì)象、資源、常量數(shù)據(jù))。
那么 IoC 和 DI 有什么關(guān)系?其實(shí)它們是同一個(gè)概念的不同角度描述,由于控制反轉(zhuǎn)的概念比較含糊(可能只是理解為容器控制對(duì)象這一個(gè)層面,很難讓人想到誰(shuí)來(lái)維護(hù)依賴(lài)關(guān)系),所以 2004 年大師級(jí)人物 Martin Fowler 又給出了一個(gè)新的名字:“依賴(lài)注入”,相對(duì) IoC 而言,“依賴(lài)注入” 明確描述了被注入對(duì)象依賴(lài) IoC 容器配置依賴(lài)對(duì)象。
總的來(lái)說(shuō), 控制反轉(zhuǎn)(Inversion of Control)是說(shuō)創(chuàng)建對(duì)象的控制權(quán)發(fā)生轉(zhuǎn)移,以前創(chuàng)建對(duì)象的主動(dòng)權(quán)和創(chuàng)建時(shí)機(jī)由應(yīng)用程序把控,而現(xiàn)在這種權(quán)利轉(zhuǎn)交給 IoC 容器,它就是一個(gè)專(zhuān)門(mén)用來(lái)創(chuàng)建對(duì)象的工廠,你需要什么對(duì)象,它就給你什么對(duì)象。
有了 IoC 容器,依賴(lài)關(guān)系就改變了,原先的依賴(lài)關(guān)系就沒(méi)了,它們都依賴(lài) IoC 容器了,通過(guò) IoC 容器來(lái)建立它們之間的關(guān)系。
前面介紹了那么多的概念,現(xiàn)在我們來(lái)看一下未使用依賴(lài)注入框架和使用依賴(lài)注入框架之間有什么明顯的區(qū)別。
4.1 未使用依賴(lài)注入框架
假設(shè)我們的服務(wù) A 依賴(lài)于服務(wù) B,即要使用服務(wù) A 前,我們需要先創(chuàng)建服務(wù) B。具體的流程如下圖所示:
從上圖可知,未使用依賴(lài)注入框架時(shí),服務(wù)的使用者需要關(guān)心服務(wù)本身和其依賴(lài)的對(duì)象是如何創(chuàng)建的,且需要手動(dòng)維護(hù)依賴(lài)關(guān)系。若服務(wù)本身需要依賴(lài)多個(gè)對(duì)象,這樣就會(huì)增加使用難度和后期的維護(hù)成本。
對(duì)于上述的問(wèn)題,我們可以考慮引入依賴(lài)注入框架。下面我們來(lái)看一下引入依賴(lài)注入框架,整體流程會(huì)發(fā)生什么變化。
4.2 使用依賴(lài)注入框架
使用依賴(lài)注入框架之后,系統(tǒng)中的服務(wù)會(huì)統(tǒng)一注冊(cè)到 IoC 容器中,如果服務(wù)有依賴(lài)其他服務(wù)時(shí),也需要對(duì)依賴(lài)進(jìn)行聲明。當(dāng)用戶需要使用特定的服務(wù)時(shí),IoC 容器會(huì)負(fù)責(zé)該服務(wù)及其依賴(lài)對(duì)象的創(chuàng)建與管理工作。具體的流程如下圖所示:
到這里我們已經(jīng)介紹了 IoC 與 DI 的概念及特點(diǎn),接下來(lái)我們來(lái)介紹 DI 的應(yīng)用。
五、DI 的應(yīng)用
DI 在前端和服務(wù)端都有相應(yīng)的應(yīng)用,比如在前端領(lǐng)域的代表是 AngularJS 和 Angular,而在服務(wù)端領(lǐng)域是 Node.js 生態(tài)中比較出名的 NestJS。接下來(lái)將簡(jiǎn)單介紹一下 DI 在 AngularJS/Angular 和 NestJS 中的應(yīng)用。
5.1 DI 在 AngularJS 中的應(yīng)用
在 AngularJS 中,依賴(lài)注入是其核心的特性之一。在 AngularJS 中聲明依賴(lài)項(xiàng)有 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) {}; // 不推薦
對(duì)于以上的代碼,相信使用過(guò) AngularJS 的小伙們都不會(huì)陌生。作為 AngularJS 核心功能特性的 DI 還是蠻強(qiáng)大的,但隨著 AngularJS 的普及和應(yīng)用的復(fù)雜度不斷提高,AngularJS DI 系統(tǒng)的問(wèn)題就暴露出來(lái)了。
這里阿寶哥簡(jiǎn)單介紹一下 AngularJS DI 系統(tǒng)存在的幾個(gè)問(wèn)題:
內(nèi)部緩存:AngularJS 應(yīng)用程序中所有的依賴(lài)項(xiàng)都是單例,我們不能控制是否使用新的實(shí)例;
命名空間沖突:在系統(tǒng)中我們使用字符串來(lái)標(biāo)識(shí)服務(wù)的名稱(chēng),假設(shè)我們?cè)陧?xiàng)目中已有一個(gè) CarService,然而第三方庫(kù)中也引入了同樣的服務(wù),這樣的話就容易出現(xiàn)混淆。
由于 AngularJS DI 存在以上的問(wèn)題,所以在后續(xù)的 Angular 重新設(shè)計(jì)了新的 DI 系統(tǒng)。
5.2 DI 在 Angular 中的應(yīng)用
以前面汽車(chē)的例子為例,我們可以把汽車(chē)、發(fā)動(dòng)機(jī)、底盤(pán)和車(chē)身這些認(rèn)為是一種 “服務(wù)”,所以它們會(huì)以服務(wù)提供者的形式注冊(cè)到 DI 系統(tǒng)中。為了能區(qū)分不同服務(wù),我們需要使用不同的令牌(Token)來(lái)標(biāo)識(shí)它們。接著我們會(huì)基于已注冊(cè)的服務(wù)提供者創(chuàng)建注入器對(duì)象。
之后,當(dāng)我們需要獲取指定服務(wù)時(shí),我們就可以通過(guò)該服務(wù)對(duì)應(yīng)的令牌,從注入器對(duì)象中獲取令牌對(duì)應(yīng)的依賴(lài)對(duì)象。上述的流程的具體如下圖所示:
好的,了解完上述的流程。下面我們來(lái)看一下如何使用 Angular 內(nèi)置的 DI 系統(tǒng)來(lái) “造車(chē)”。
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ā)動(dòng)了'); } } @Injectable() export default class Car { // 使用構(gòu)造注入方式注入依賴(lài)對(duì)象 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 對(duì)象的 resolveAndCreate 方法手動(dòng)創(chuàng)建注入器,然后根據(jù)車(chē)輛對(duì)應(yīng)的 Token 來(lái)獲取對(duì)應(yīng)的依賴(lài)對(duì)象。通過(guò)觀察上述代碼,你可以發(fā)現(xiàn),我們已經(jīng)不需要手動(dòng)地管理和維護(hù)依賴(lài)對(duì)象了,這些 “臟活”、“累活” 已經(jīng)交給注入器來(lái)處理了。
此外,如果要能正常獲取汽車(chē)對(duì)象,我們還需要在 app.module.ts 文件中聲明 Car 對(duì)應(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)建高效,可擴(kuò)展的 Node.js Web 應(yīng)用程序的框架。它使用現(xiàn)代的 JavaScript 或 TypeScript(保留與純 JavaScript 的兼容性),并結(jié)合 OOP(面向?qū)ο缶幊?,F(xiàn)P(函數(shù)式編程)和FRP(函數(shù)響應(yīng)式編程)的元素。
在底層,Nest 使用了 Express,但也提供了與其他各種庫(kù)的兼容,例如 Fastify,可以方便地使用各種可用的第三方插件。
近幾年,由于 Node.js,JavaScript 已經(jīng)成為 Web 前端和后端應(yīng)用程序的「通用語(yǔ)言」,從而產(chǎn)生了像 Angular、React、Vue 等令人耳目一新的項(xiàng)目,這些項(xiàng)目提高了開(kāi)發(fā)人員的生產(chǎn)力,使得可以快速構(gòu)建可測(cè)試的且可擴(kuò)展的前端應(yīng)用程序。然而,在服務(wù)器端,雖然有很多優(yōu)秀的庫(kù)、helper 和 Node 工具,但是它們都沒(méi)有有效地解決主要問(wèn)題 —— 架構(gòu)。
NestJS 旨在提供一個(gè)開(kāi)箱即用的應(yīng)用程序體系結(jié)構(gòu),允許輕松創(chuàng)建高度可測(cè)試,可擴(kuò)展,松散耦合且易于維護(hù)的應(yīng)用程序。 在 NestJS 中也為我們開(kāi)發(fā)者提供了依賴(lài)注入的功能,這里我們以官網(wǎng)的示例來(lái)演示一下依賴(lài)注入的功能。
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 中,我們通過(guò)構(gòu)造注入的方式注入了 AppService 對(duì)象,當(dāng)用戶訪問(wèn)首頁(yè)的時(shí)候,我們會(huì)調(diào)用 AppService 對(duì)象的 getHello 方法來(lái)獲取 'Hello World!' 消息,并把消息返回給用戶。當(dāng)然為了保證依賴(lài)注入可以正常工作,我們還需要在 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 {}
其實(shí) DI 并不是 AngularJS/Angular 和 NestJS 所特有的,如果你想在其他項(xiàng)目中使用 DI/IoC 的功能特性,阿寶哥推薦你使用 InversifyJS,它是一個(gè)可用于 JavaScript 和 Node.js 應(yīng)用,功能強(qiáng)大、輕量的 IoC 容器。
對(duì) InversifyJS 感興趣的小伙伴可以自行了解一下,阿寶哥就不繼續(xù)展開(kāi)介紹了。接下來(lái),我們將進(jìn)入本文的重點(diǎn),即介紹如何使用 TypeScript 實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 IoC 容器,該容器實(shí)現(xiàn)的功能如下圖所示:
六、手寫(xiě) IoC 容器
為了讓大家能更好地理解 IoC 容器的實(shí)現(xiàn)代碼,小編先介紹一些相關(guān)的前置知識(shí)。
6.1 裝飾器
如果你有使用過(guò) Angular 或 NestJS,相信你對(duì)以下的代碼不會(huì)陌生。
@Injectable() export class HttpService { constructor( private httpClient: HttpClient ) {} }
在以上代碼中,我們使用了 Injectable 裝飾器。該裝飾器用于表示此類(lèi)可以自動(dòng)注入其依賴(lài)項(xiàng)。其中 @Injectable() 中的 @ 符號(hào)屬于語(yǔ)法糖。
裝飾器是一個(gè)包裝類(lèi),函數(shù)或方法并為其添加行為的函數(shù)。這對(duì)于定義與對(duì)象關(guān)聯(lián)的元數(shù)據(jù)很有用。裝飾器有以下四種分類(lèi):
類(lèi)裝飾器(Class decorators)
屬性裝飾器(Property decorators)
方法裝飾器(Method decorators)
參數(shù)裝飾器(Parameter decorators)
前面示例中使用的 @Injectable() 裝飾器,屬于類(lèi)裝飾器。在該類(lèi)裝飾器修飾的 HttpService 類(lèi)中,我們通過(guò)構(gòu)造注入的方式注入了用于處理 HTTP 請(qǐng)求的 HttpClient 依賴(lài)對(duì)象。
6.2 反射
@Injectable() export class HttpService { constructor( private httpClient: HttpClient ) {} }
以上代碼若設(shè)置編譯的目標(biāo)為 ES5,則會(huì)生成以下代碼:
// 忽略__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; }());
通過(guò)觀察上述代碼,你會(huì)發(fā)現(xiàn) HttpService 構(gòu)造函數(shù)中 httpClient 參數(shù)的類(lèi)型被擦除了,這是因?yàn)?JavaScript 是弱類(lèi)型語(yǔ)言。那么如何在運(yùn)行時(shí),保證注入正確類(lèi)型的依賴(lài)對(duì)象呢?這里 TypeScript 使用 reflect-metadata 這個(gè)第三方庫(kù)來(lái)存儲(chǔ)額外的類(lèi)型信息。
reflect-metadata 這個(gè)庫(kù)提供了很多 API 用于操作元信息,這里我們只簡(jiǎn)單介紹幾個(gè)常用的 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() { } }
對(duì)于上述的 API 只需簡(jiǎn)單了解一下即可。在后續(xù)的內(nèi)容中,我們將介紹具體如何使用。這里我們需要注意以下兩個(gè)問(wèn)題:
對(duì)于類(lèi)或函數(shù),我們需要使用裝飾器來(lái)修飾它們,這樣才能保存元數(shù)據(jù)。
只有類(lèi)、枚舉或原始數(shù)據(jù)類(lèi)型能被記錄。接口和聯(lián)合類(lèi)型作為 “對(duì)象” 出現(xiàn)。這是因?yàn)檫@些類(lèi)型在編譯后完全消失,而類(lèi)卻一直存在。
6.3 定義 Token 和 Provider
了解完裝飾器與反射相關(guān)的基礎(chǔ)知識(shí),接下來(lái)我們來(lái)開(kāi)始實(shí)現(xiàn) IoC 容器。我們的 IoC 容器將使用兩個(gè)主要的概念:令牌(Token)和提供者(Provider)。令牌是 IoC 容器所要?jiǎng)?chuàng)建對(duì)象的標(biāo)識(shí)符,而提供者用于描述如何創(chuàng)建這些對(duì)象。
IoC 容器最小的公共接口如下所示:
export class Container { addProvider<T>(provider: Provider<T>) {} // TODO inject<T>(type: Token<T>): T {} // TODO }
接下來(lái)我們先來(lái)定義 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 類(lèi)型是一個(gè)聯(lián)合類(lèi)型,既可以是一個(gè)函數(shù)類(lèi)型也可以是 InjectionToken 類(lèi)型。AngularJS 中使用字符串作為 Token,在某些情況下,可能會(huì)導(dǎo)致沖突。因此,為了解決這個(gè)問(wèn)題,我們定義了 InjectionToken 類(lèi),來(lái)避免出現(xiàn)命名沖突問(wèn)題。
定義完 Token 類(lèi)型,接下來(lái)我們來(lái)定義三種不同類(lèi)型的 Provider:
ClassProvider:提供一個(gè)類(lèi),用于創(chuàng)建依賴(lài)對(duì)象;
ValueProvider:提供一個(gè)已存在的值,作為依賴(lài)對(duì)象;
FactoryProvider:提供一個(gè)工廠方法,用于創(chuàng)建依賴(lài)對(duì)象。
// 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ū)分這三種不同類(lèi)型的 Provider,我們自定義了三個(gè)類(lèi)型守衛(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)提過(guò)了,對(duì)于類(lèi)或函數(shù),我們需要使用裝飾器來(lái)修飾它們,這樣才能保存元數(shù)據(jù)。因此,接下來(lái)我們來(lái)分別創(chuàng)建 Injectable 和 Inject 裝飾器。
6.4.1 Injectable 裝飾器
Injectable 裝飾器用于表示此類(lèi)可以自動(dòng)注入其依賴(lài)項(xiàng),該裝飾器屬于類(lèi)裝飾器。在 TypeScript 中,類(lèi)裝飾器的聲明如下:
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
類(lèi)裝飾器顧名思義,就是用來(lái)裝飾類(lèi)的。它接收一個(gè)參數(shù):target: TFunction,表示被裝飾的類(lèi)。下面我們來(lái)看一下 Injectable 裝飾器的具體實(shí)現(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ù)之后,會(huì)返回一個(gè)新的函數(shù)。在新的函數(shù)中,我們使用 reflect-metadata 這個(gè)庫(kù)提供的 defineMetadata API 來(lái)保存元信息,其中 defineMetadata API 的使用方式如下所示:
// define metadata on an object or property Reflect.defineMetadata(metadataKey, metadataValue, target); Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);
Injectable 類(lèi)裝飾器使用方式也簡(jiǎn)單,只需要在被裝飾類(lèi)的上方使用 @Injectable() 語(yǔ)法糖就可以應(yīng)用該裝飾器:
@Injectable() export class HttpService { constructor( private httpClient: HttpClient ) {} }
在以上示例中,我們注入的是 Type 類(lèi)型的 HttpClient 對(duì)象。但在實(shí)際的項(xiàng)目中,往往會(huì)比較復(fù)雜。除了需要注入 Type 類(lèi)型的依賴(lài)對(duì)象之外,我們還可能會(huì)注入其他類(lèi)型的依賴(lài)對(duì)象,比如我們希望在 HttpService 服務(wù)中注入遠(yuǎn)程服務(wù)器的 API 地址。針對(duì)這種情形,我們需要使用 Inject 裝飾器。
6.4.2 Inject 裝飾器
接下來(lái)我們來(lái)創(chuàng)建 Inject 裝飾器,該裝飾器屬于參數(shù)裝飾器。在 TypeScript 中,參數(shù)裝飾器的聲明如下:
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number ) => void
參數(shù)裝飾器顧名思義,是用來(lái)裝飾函數(shù)參數(shù),它接收三個(gè)參數(shù):
target: Object —— 被裝飾的類(lèi);
propertyKey: string | symbol —— 方法名;
parameterIndex: number —— 方法中參數(shù)的索引值。
下面我們來(lái)看一下 Inject 裝飾器的具體實(shí)現(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ù)之后,會(huì)返回一個(gè)新的函數(shù)。在新的函數(shù)中,我們使用 reflect-metadata 這個(gè)庫(kù)提供的 defineMetadata API 來(lái)保存參數(shù)相關(guān)的元信息。這里是保存 index 索引信息和 Token 信息。
定義完 Inject 裝飾器,我們就可以利用它來(lái)注入我們前面所提到的遠(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 實(shí)現(xiàn) IoC 容器
目前為止,我們已經(jīng)定義了 Token、Provider、Injectable 和 Inject 裝飾器。接下來(lái)我們來(lái)實(shí)現(xiàn)前面所提到的 IoC 容器的 API:
export class Container { addProvider<T>(provider: Provider<T>) {} // TODO inject<T>(type: Token<T>): T {} // TODO }
6.5.1 實(shí)現(xiàn) addProvider 方法
addProvider() 方法的實(shí)現(xiàn)很簡(jiǎn)單,我們使用 Map 來(lái)存儲(chǔ) 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 的對(duì)應(yīng)信息保存到 providers 對(duì)象中之外,我們定義了一個(gè) assertInjectableIfClassProvider 方法,用于確保添加的 ClassProvider 是可注入的。該方法的具體實(shí)現(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 類(lèi)型守衛(wèi)函數(shù)來(lái)判斷是否為 ClassProvider,如果是的話,會(huì)判斷該 ClassProvider 是否為可注入的,具體使用的是 isInjectable 函數(shù),該函數(shù)的定義如下:
export function isInjectable<T>(target: Type<T>) { return Reflect.getMetadata(INJECTABLE_METADATA_KEY, target) === true; }
在 isInjectable 函數(shù)中,我們使用 reflect-metadata 這個(gè)庫(kù)提供的 getMetadata API 來(lái)獲取保存在類(lèi)中的元信息。為了更好地理解以上代碼,我們來(lái)回顧一下前面 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 對(duì)應(yīng)的類(lèi)是不可注入的,則會(huì)拋出異常。為了讓異常消息更加友好,也更加直觀。我們定義了一個(gè) getTokenName 方法來(lái)獲取 Token 對(duì)應(yīng)的名稱(chēng):
private getTokenName<T>(token: Token<T>) { return token instanceof InjectionToken ? token.injectionIdentifier : token.name; }
現(xiàn)在我們已經(jīng)實(shí)現(xiàn)了 Container 類(lèi)的 addProvider 方法,這時(shí)我們就可以使用它來(lái)添加三種不同類(lèi)型的 Provider:
const container = new Container(); const input = { x: 200 }; class BasicClass {} // 注冊(cè)ClassProvider container.addProvider({ provide: BasicClass, useClass: BasicClass}); // 注冊(cè)ValueProvider container.addProvider({ provide: BasicClass, useValue: input }); // 注冊(cè)FactoryProvider container.addProvider({ provide: BasicClass, useFactory: () => input });
需要注意的是,以上示例中注冊(cè)三種不同類(lèi)型的 Provider 使用的是同一個(gè) Token 僅是為了演示而已。下面我們來(lái)實(shí)現(xiàn) Container 類(lèi)中核心的 inject 方法。
6.5.2 實(shí)現(xiàn) inject 方法
在看 inject 方法的具體實(shí)現(xiàn)之前,我們先來(lái)看一下該方法所實(shí)現(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
觀察以上的測(cè)試用例可知,Container 類(lèi)中 inject 方法所實(shí)現(xiàn)的功能就是根據(jù) Token 獲取與之對(duì)應(yīng)的對(duì)象。在前面實(shí)現(xiàn)的 addProvider 方法中,我們把 Token 和該 Token 對(duì)應(yīng)的 Provider 保存在 providers Map 對(duì)象中。所以在 inject 方法中,我們可以先從 providers 對(duì)象中獲取該 Token 對(duì)應(yīng)的 Provider 對(duì)象,然后在根據(jù)不同類(lèi)型的 Provider 來(lái)獲取其對(duì)應(yīng)的對(duì)象。
好的,下面我們來(lái)看一下 inject 方法的具體實(shí)現(xiàn):
inject<T>(type: Token<T>): T { let provider = this.providers.get(type); // 處理使用Injectable裝飾器修飾的類(lèi) if (provider === undefined && !(type instanceof InjectionToken)) { provider = { provide: type, useClass: type }; this.assertInjectableIfClassProvider(provider); } return this.injectWithProvider(type, provider); }
在以上代碼中,除了處理正常的流程之外。我們還處理一個(gè)特殊的場(chǎng)景,即沒(méi)有使用 addProvider 方法注冊(cè) Provider,而是使用 Injectable 裝飾器來(lái)裝飾某個(gè)類(lèi)。對(duì)于這個(gè)特殊場(chǎng)景,我們會(huì)根據(jù)傳入的 type 參數(shù)來(lái)創(chuàng)建一個(gè) provider 對(duì)象,然后進(jìn)一步調(diào)用 injectWithProvider 方法來(lái)創(chuàng)建對(duì)象,該方法的具體實(shí)現(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)部,我們會(huì)使用前面定義的用于區(qū)分三種不同類(lèi)型 Provider 的類(lèi)型守衛(wèi)函數(shù)來(lái)處理不同的 Provider。這里我們先來(lái)看一下最簡(jiǎn)單 ValueProvider,當(dāng)發(fā)現(xiàn)注入的是 ValueProvider 類(lèi)型時(shí),則會(huì)調(diào)用 injectValue 方法來(lái)獲取其對(duì)應(yīng)的對(duì)象:
// { provide: API_URL, useValue: 'https://www.semlinker.com/' } private injectValue<T>(valueProvider: ValueProvider<T>): T { return valueProvider.useValue; }
接著我們來(lái)看如何處理 FactoryProvider 類(lèi)型的 Provider,如果發(fā)現(xiàn)是 FactoryProvider 類(lèi)型時(shí),則會(huì)調(diào)用 injectFactory 方法來(lái)獲取其對(duì)應(yīng)的對(duì)象,該方法的實(shí)現(xiàn)也很簡(jiǎn)單:
// const input = { x: 200 }; // container.addProvider({ provide: BasicClass, useFactory: () => input }); private injectFactory<T>(valueProvider: FactoryProvider<T>): T { return valueProvider.useFactory(); }
最后我們來(lái)分析一下如何處理 ClassProvider,對(duì)于 ClassProvider 類(lèi)說(shuō),通過(guò) Provider 對(duì)象的 useClass 屬性,我們就可以直接獲取到類(lèi)對(duì)應(yīng)的構(gòu)造函數(shù)。最簡(jiǎn)單的情形是該類(lèi)沒(méi)有依賴(lài)其他對(duì)象,但在大多數(shù)場(chǎng)景下,即將實(shí)例化的服務(wù)類(lèi)是會(huì)依賴(lài)其他的對(duì)象的。所以在實(shí)例化服務(wù)類(lèi)前,我們需要構(gòu)造其依賴(lài)的對(duì)象。
那么現(xiàn)在問(wèn)題來(lái)了,怎么獲取類(lèi)所依賴(lài)的對(duì)象呢?我們先來(lái)分析一下以下代碼:
const API_URL = new InjectionToken('apiUrl'); @Injectable() export class HttpService { constructor( private httpClient: HttpClient, @Inject(API_URL) private apiUrl: string ) {} }
以上代碼若設(shè)置編譯的目標(biāo)為 ES5,則會(huì)生成以下代碼:
// 已省略__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; }());
觀察以上的代碼會(huì)不會(huì)覺(jué)得有點(diǎn)暈?不要著急,阿寶哥會(huì)逐一分析 HttpService 中的兩個(gè)參數(shù)。首先我們先來(lái)分析 apiUrl 參數(shù):
在圖中我們可以很清楚地看到,API_URL 對(duì)應(yīng)的 Token 最終會(huì)通過(guò) Reflect.defineMetadata API 進(jìn)行保存,所使用的 Key 是 Symbol('INJECT_KEY')。而對(duì)于另一個(gè)參數(shù)即 httpClient,它使用的 Key 是 "design:paramtypes",它用于修飾目標(biāo)對(duì)象方法的參數(shù)類(lèi)型。
除了 "design:paramtypes" 之外,還有其他的 metadataKey,比如 design:type 和design:returntype,它們分別用于修飾目標(biāo)對(duì)象的類(lèi)型和修飾目標(biāo)對(duì)象方法返回值的類(lèi)型。
由上圖可知,HttpService 構(gòu)造函數(shù)的參數(shù)類(lèi)型最終會(huì)使用 Reflect.metadata API 進(jìn)行存儲(chǔ)。了解完上述的知識(shí),接下來(lái)我們來(lái)定義一個(gè) getInjectedParams 方法,用于獲取類(lèi)構(gòu)造函數(shù)中聲明的依賴(lài)對(duì)象,該方法的具體實(shí)現(xiàn)如下:
type InjectableParam = Type<any>; const REFLECT_PARAMS = "design:paramtypes"; private getInjectedParams<T>(target: Type<T>) { // 獲取參數(shù)的類(lèi)型 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); }); }
因?yàn)槲覀兊?Token 的類(lèi)型是 Type
export function getInjectionToken(target: any, index: number) { return Reflect.getMetadata(INJECT_METADATA_KEY, target, `index-${index}`) as Token<any> | undefined; }
現(xiàn)在我們已經(jīng)可以獲取類(lèi)構(gòu)造函數(shù)中所依賴(lài)的對(duì)象,基于前面定義的 getInjectedParams 方法,我們就來(lái)定義一個(gè) injectClass 方法,用來(lái)實(shí)例化 ClassProvider 所注冊(cè)的類(lèi)。
// { 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); }
這時(shí) IoC 容器中定義的兩個(gè)方法都已經(jīng)實(shí)現(xiàn)了,我們來(lái)看一下 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; } }
最后我們來(lái)簡(jiǎn)單測(cè)試一下我們前面開(kāi)發(fā)的 IoC 容器,具體的測(cè)試代碼如下所示:
// 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);
以上代碼成功運(yùn)行后,控制臺(tái)會(huì)輸出以下結(jié)果:
HttpService { httpClient: HttpClient {}, apiUrl: 'https://www.semlinker.com/' }
很明顯該結(jié)果正是我們所期望的,這表示我們 IoC 容器已經(jīng)可以正常工作了。當(dāng)然在實(shí)際項(xiàng)目中,一個(gè)成熟的 IoC 容器還要考慮很多東西,如果小伙伴想在項(xiàng)目中使用的話,建議可以考慮使用 InversifyJS 這個(gè)庫(kù)。
到此,關(guān)于“如何使用TypeScript實(shí)現(xiàn)一個(gè)IoC容器”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。