本篇文章帶大家了解一下Node 后端框架Nest.js,介紹一下Nestjs模塊機(jī)制的概念和實(shí)現(xiàn)原理,希望對(duì)大家有所幫助!
Nest 提供了模塊機(jī)制,通過(guò)在模塊裝飾器中定義提供者、導(dǎo)入、導(dǎo)出和提供者構(gòu)造函數(shù)便完成了依賴注入,通過(guò)模塊樹(shù)組織整個(gè)應(yīng)用程序的開(kāi)發(fā)。按照框架本身的約定直接擼一個(gè)應(yīng)用程序,是完全沒(méi)有問(wèn)題的??墒?,于我而言對(duì)于框架宣稱的依賴注入、控制反轉(zhuǎn)、模塊、提供者、元數(shù)據(jù)、相關(guān)裝飾器等等,覺(jué)得缺乏一個(gè)更清晰系統(tǒng)的認(rèn)識(shí)。
- 為什么需要控制反轉(zhuǎn)?
- 什么是依賴注入?
- 裝飾器做了啥?
- 模塊 (@Module) 中的提供者(providers),導(dǎo)入(imports)、導(dǎo)出(exports)是什么實(shí)現(xiàn)原理?
好像能夠理解,能夠意會(huì),但是讓我自己從頭說(shuō)清楚,我說(shuō)不清楚。于是進(jìn)行了一番探索,便有了這篇文章。從現(xiàn)在起,我們從新出發(fā),進(jìn)入正文。
1 兩個(gè)階段
1.1 Express、Koa
一個(gè)語(yǔ)言和其技術(shù)社區(qū)的發(fā)展過(guò)程,一定是從底層功能逐漸往上豐富發(fā)展的,就像是樹(shù)根慢慢生長(zhǎng)為樹(shù)枝再長(zhǎng)滿樹(shù)葉的過(guò)程。在較早,Nodejs 出現(xiàn)了 Express 和 Koa 這樣的基本 Web 服務(wù)框架。能夠提供一個(gè)非常基礎(chǔ)的服務(wù)能力?;谶@樣的框架,大量的中間件、插件開(kāi)始在社區(qū)誕生,為框架提供更加豐富的服務(wù)。我們需要自己去組織應(yīng)用依賴,搭建應(yīng)用腳手架,靈活又繁瑣,也具有一定工作量。
發(fā)展到后面,一些生產(chǎn)更高效、規(guī)則更統(tǒng)一的框架便誕生了,開(kāi)啟了一個(gè)更新的階段。
1.2 EggJs、Nestjs
為了更加適應(yīng)快速生產(chǎn)應(yīng)用,統(tǒng)一規(guī)范,開(kāi)箱即用,便發(fā)展出了 EggJs、NestJs、Midway等框架。此類框架,通過(guò)實(shí)現(xiàn)底層生命周期,將一個(gè)應(yīng)用的實(shí)現(xiàn)抽象為一個(gè)通用可擴(kuò)展的過(guò)程,我們只需要按照框架提供的配置方式,便可以更簡(jiǎn)單的實(shí)現(xiàn)應(yīng)用程序??蚣軐?shí)現(xiàn)了程序的過(guò)程控制,而我們只需要在合適位置組裝我們的零件就行,這看起來(lái)更像是流水線工作,每個(gè)流程被分割的很清楚,也省去了很多實(shí)現(xiàn)成本。
1.3 小結(jié)
上面的兩個(gè)階段只是一個(gè)鋪墊,我們可以大致了解到,框架的升級(jí)是提高了生產(chǎn)效率,而要實(shí)現(xiàn)框架的升級(jí),就會(huì)引入一些設(shè)計(jì)思路和模式,Nest 中就出現(xiàn)了控制反轉(zhuǎn)、依賴注入、元編程的概念,下面我們來(lái)聊聊。
2 控制反轉(zhuǎn)和依賴注入
2.1 依賴注入
一個(gè)應(yīng)用程序?qū)嶋H就是非常多的抽象類,通過(guò)互相調(diào)用實(shí)現(xiàn)應(yīng)用的所有功能。隨著應(yīng)用代碼和功能復(fù)雜度的增加,項(xiàng)目一定會(huì)越來(lái)越難以維護(hù),因?yàn)轭愒絹?lái)越多,相互之間的關(guān)系越來(lái)越復(fù)雜。
舉個(gè)例子,假如我們使用 Koa 開(kāi)發(fā)我們的應(yīng)用,Koa 本身主要實(shí)現(xiàn)了一套基礎(chǔ)的 Web 服務(wù)能力,我們?cè)趯?shí)現(xiàn)應(yīng)用的過(guò)程中,會(huì)定義很多類,這些類的實(shí)例化方式、相互依賴關(guān)系,都會(huì)由我們?cè)诖a邏輯自由組織和控制。每個(gè)類的實(shí)例化都是由我們手動(dòng) new,并且我們可以控制某個(gè)類是只實(shí)例化一次然后被共享,還是每次都實(shí)例化。下面的 B 類依賴 A,每次實(shí)例化 B 的時(shí)候,A 都會(huì)被實(shí)例化一次,所以對(duì)于每個(gè)實(shí)例 B 來(lái)說(shuō),A 是不被共享的實(shí)例。
class A{} // B class B{ contructor(){ this.a = new A(); } }
下面的 C 是獲取的外部實(shí)例,所以多個(gè) C 實(shí)例是共享的 app.a 這個(gè)實(shí)例。
class A{} // C const app = {}; app.a = new A(); class C{ contructor(){ this.a = app.a; } }
下面的 D 是通過(guò)構(gòu)造函數(shù)參數(shù)傳入,可以每次傳入一個(gè)非共享實(shí)例,也可以傳入共享的 app.a 這個(gè)實(shí)例(D 和 F 共享 app.a),并且由于現(xiàn)在是參數(shù)的方式傳入,我也可以傳入一個(gè) X 類實(shí)例。
class A{} class X{} // D const app = {}; app.a = new A(); class D{ contructor(a){ this.a = a; } } class F{ contructor(a){ this.a = a; } } new D(app.a) new F(app.a) new D(new X())
這種方式就是依賴注入,把 B 所依賴的 A,通過(guò)傳值的方式注入到 B 中。通過(guò)構(gòu)造函數(shù)注入(傳值)只是一種實(shí)現(xiàn)方式,也可以通過(guò)實(shí)現(xiàn) set 方法調(diào)用傳入,或者是其他任何方式,只要能把外部的一個(gè)依賴,傳入到內(nèi)部就行。其實(shí)就這么簡(jiǎn)單。
class A{} // D class D{ setDep(a){ this.a = a; } } const d = new D() d.setDep(new A())
2.2 All in 依賴注入?
隨著迭代進(jìn)行,出現(xiàn)了 B 根據(jù)不同的前置條件依賴會(huì)發(fā)生變化。比如,前置條件一 this.a
需要傳入 A 的實(shí)例,前置條件二this.a
需要傳入 X 的實(shí)例。這個(gè)時(shí)候,我們就會(huì)開(kāi)始做實(shí)際的抽象了。我們就會(huì)改造成上面 D 這樣依賴注入的方式。
初期,我們?cè)趯?shí)現(xiàn)應(yīng)用的時(shí)候,在滿足當(dāng)時(shí)需求的情況下,就會(huì)實(shí)現(xiàn)出 B 和 C 類的寫法,這本身也沒(méi)有什么問(wèn)題,項(xiàng)目迭代了幾年之后,都不一定會(huì)動(dòng)這部分代碼。我們要是去考慮后期擴(kuò)展什么的,是會(huì)影響開(kāi)發(fā)效率的,而且不一定派的上用場(chǎng)。所以大部分時(shí)候,我們都是遇到需要抽象的場(chǎng)景,再對(duì)部分代碼做抽象改造。
// 改造前 class B{ contructor(){ this.a = new A(); } } new B() // 改造后 class D{ contructor(a){ this.a = a; } } new D(new A()) new D(new X())
按照目前的開(kāi)發(fā)模式,CBD三種類都會(huì)存在,B 和 C有一定的幾率發(fā)展成為 D,每次升級(jí) D 的抽象過(guò)程,我們會(huì)需要重構(gòu)代碼,這是一種實(shí)現(xiàn)成本。
這里舉這個(gè)例子是想說(shuō)明,在一個(gè)沒(méi)有任何約束或者規(guī)定的開(kāi)發(fā)模式下。我們是可以自由的寫代碼來(lái)達(dá)到各種類與類之間依賴控制。在一個(gè)完全開(kāi)放的環(huán)境里,是非常自由的,這是一個(gè)刀耕火種的原始時(shí)代。由于沒(méi)有一個(gè)固定的代碼開(kāi)發(fā)模式,沒(méi)有一個(gè)最高行動(dòng)綱領(lǐng),隨著不同開(kāi)發(fā)人員的介入或者說(shuō)同一個(gè)開(kāi)發(fā)者不同時(shí)間段寫代碼的差別,代碼在增長(zhǎng)的過(guò)程中,依賴關(guān)系會(huì)變得非常不清晰,該共享的實(shí)例可能被多次實(shí)例化,浪費(fèi)內(nèi)存。從代碼中,很難看清楚一個(gè)完整的依賴關(guān)系結(jié)構(gòu),代碼可能會(huì)變得非常難以維護(hù)。
那我們每定義一個(gè)類,都按照依賴注入的方式來(lái)寫,都寫成 D 這樣的,那 C 和 B 的抽象過(guò)程就被提前了,這樣后期擴(kuò)展也比較方便,減少了改造成本。所以把這叫All in 依賴注入
,也就是我們所有依賴都通過(guò)依賴注入的方式實(shí)現(xiàn)。
可這樣前期的實(shí)現(xiàn)成本又變高了,很難在團(tuán)隊(duì)協(xié)作中達(dá)到統(tǒng)一并且堅(jiān)持下去,最終可能會(huì)落地失敗,這也可以被定義為是一種過(guò)度設(shè)計(jì),因?yàn)轭~外的實(shí)現(xiàn)成本,不一定能帶來(lái)收益。
2.3 控制反轉(zhuǎn)
既然已經(jīng)約定好了統(tǒng)一使用依賴注入的方式,那是否可以通過(guò)框架的底層封裝,實(shí)現(xiàn)一個(gè)底層控制器,約定一個(gè)依賴配置規(guī)則,控制器根據(jù)我們定義的依賴配置來(lái)控制實(shí)例化過(guò)程和依賴共享,幫助我們實(shí)現(xiàn)類管理。這樣的設(shè)計(jì)模式就叫控制反轉(zhuǎn)。
控制反轉(zhuǎn)可能第一次聽(tīng)說(shuō)的時(shí)候會(huì)很難理解,控制指的什么?反轉(zhuǎn)了啥?
猜測(cè)是由于開(kāi)發(fā)者一開(kāi)始就用此類框架,并沒(méi)有體驗(yàn)過(guò)上個(gè)“Express、Koa時(shí)代”,缺乏舊社會(huì)毒打。加上這反轉(zhuǎn)的用詞,在程序中顯得非常的抽象,難以望文生義。
前文我們說(shuō)的實(shí)現(xiàn) Koa 應(yīng)用,所有的類完全由我們自由控制的,所以可以看作是一個(gè)常規(guī)的程序控制方式,那就叫它:控制正轉(zhuǎn)。而我們使用 Nest,它底層實(shí)現(xiàn)一套控制器,我們只需要在實(shí)際開(kāi)發(fā)過(guò)程中,按照約定寫配置代碼,框架程序就會(huì)幫我們管理類的依賴注入,所以就把它叫作:控制反轉(zhuǎn)。
本質(zhì)就是把程序的實(shí)現(xiàn)過(guò)程交給框架程序去統(tǒng)一管理,控制權(quán)從開(kāi)發(fā)者,交給了框架程序。
控制正轉(zhuǎn):開(kāi)發(fā)者純手動(dòng)控制程序
控制反轉(zhuǎn):框架程序控制
舉個(gè)現(xiàn)實(shí)的例子,一個(gè)人本來(lái)是自己開(kāi)車去上班的,他的目的就是到達(dá)公司。它自己開(kāi)車,自己控制路線。而如果交出開(kāi)車的控制權(quán),就是去趕公交,他只需要選擇一個(gè)對(duì)應(yīng)的班車就可以到達(dá)公司了。單從控制來(lái)說(shuō),人就是被解放出來(lái)了,只需要記住坐那趟公交就行了,犯錯(cuò)的幾率也小了,人也輕松了不少。公交系統(tǒng)就是控制器,公交線路就是約定配置。
通過(guò)如上的實(shí)際對(duì)比,我想應(yīng)該有點(diǎn)能理解控制反轉(zhuǎn)了。
2.4 小結(jié)
從 Koa 到 Nest,從前端的 JQuery 到 Vue React。其實(shí)都是一步步通過(guò)框架封裝,去解決上個(gè)時(shí)代低效率的問(wèn)題。
上面的 Koa 應(yīng)用開(kāi)發(fā),通過(guò)非常原始的方式去控制依賴和實(shí)例化,就類似于前端中的 JQuery 操作 dom ,這種很原始的方式就把它叫控制正轉(zhuǎn),而 Vue React 就好似 Nest 提供了一層程序控制器,他們可以都叫控制反轉(zhuǎn)。這也是個(gè)人理解,如果有問(wèn)題期望大神指出。
下面再來(lái)說(shuō)說(shuō) Nest 中的模塊 @Module,依賴注入、控制反轉(zhuǎn)需要它作為媒介。
3 Nestjs的模塊(@Module)
Nestjs實(shí)現(xiàn)了控制反轉(zhuǎn),約定配置模塊(@module)的 imports、exports、providers 管理提供者也就是類的依賴注入。
providers 可以理解是在當(dāng)前模塊注冊(cè)和實(shí)例化類,下面的 A 和 B 就在當(dāng)前模塊被實(shí)例化,如果B在構(gòu)造函數(shù)中引用 A,就是引用的當(dāng)前 ModuleD 的 A 實(shí)例。
import { Module } from '@nestjs/common'; import { ModuleX } from './moduleX'; import { A } from './A'; import { B } from './B'; @Module({ imports: [ModuleX], providers: [A,B], exports: [A] }) export class ModuleD {} // B class B{ constructor(a:A){ this.a = a; } }
exports
就是把當(dāng)前模塊中的 providers
中實(shí)例化的類,作為可被外部模塊共享的類。比如現(xiàn)在 ModuleF 的 C 類實(shí)例化的時(shí)候,想直接注入 ModuleD 的 A 類實(shí)例。就在 ModuleD 中設(shè)置導(dǎo)出(exports)A,在 ModuleF 中通過(guò) imports
導(dǎo)入 ModuleD。
按照下面的寫法,控制反轉(zhuǎn)程序會(huì)自動(dòng)掃描依賴,首先看自己模塊的 providers 中,有沒(méi)有提供者 A,如果沒(méi)有就去尋找導(dǎo)入的 ModuleD 中是否有 A 實(shí)例,發(fā)現(xiàn)存在,就取得 ModuleD 的 A 實(shí)例注入到 C 實(shí)例之中。
import { Module } from '@nestjs/common'; import { ModuleD} from './moduleD'; import { C } from './C'; @Module({ imports: [ModuleD], providers: [C], }) export class ModuleF {} // C class C { constructor(a:A){ this.a = a; } }
因此想要讓外部模塊使用當(dāng)前模塊的類實(shí)例,必須先在當(dāng)前模塊的providers
里定義實(shí)例化類,再定義導(dǎo)出這個(gè)類,否則就會(huì)報(bào)錯(cuò)。
//正確 @Module({ providers: [A], exports: [A] }) //錯(cuò)誤 @Module({ providers: [], exports: [A] })
后期補(bǔ)充
模塊查找實(shí)例的過(guò)程回看了一下,確實(shí)有點(diǎn)不清晰。核心點(diǎn)就是providers里的類會(huì)被實(shí)例化,實(shí)例化后就是提供者,模塊里只有providers里的類會(huì)被實(shí)例化,而導(dǎo)出和導(dǎo)入只是一個(gè)組織關(guān)系配置。模塊會(huì)優(yōu)先使用自己的提供者,如果沒(méi)有,再去找導(dǎo)入的模塊是否有對(duì)應(yīng)提供者
這里還是提一嘴ts的知識(shí)點(diǎn)
export class C { constructor(private a: A) { } }
由于 TypeScript 支持 constructor 參數(shù)(private、protected、public、readonly)隱式自動(dòng)定義為 class 屬性 (Parameter Property),因此無(wú)需使用 this.a = a
。Nest 中都是這樣的寫法。
4 Nest 元編程
元編程的概念在 Nest 框架中得到了體現(xiàn),它其中的控制反轉(zhuǎn)、裝飾器,就是元編程的實(shí)現(xiàn)。大概可以理解為,元編程本質(zhì)還是編程,只是中間多了一些抽象的程序,這個(gè)抽象程序能夠識(shí)別元數(shù)據(jù)(如@Module中的對(duì)象數(shù)據(jù)),其實(shí)就是一種擴(kuò)展能力,能夠?qū)⑵渌绦蜃鳛閿?shù)據(jù)來(lái)處理。我們?cè)诰帉戇@樣的抽象程序,就是在元編程了。
4.1 元數(shù)據(jù)
Nest 文檔中也常提到了元數(shù)據(jù),元數(shù)據(jù)這個(gè)概念第一次看到的話,也會(huì)比較費(fèi)解,需要隨著接觸時(shí)間增長(zhǎng)習(xí)慣成理解,可以不用太過(guò)糾結(jié)。
元數(shù)據(jù)的定義是:描述數(shù)據(jù)的數(shù)據(jù),主要是描述數(shù)據(jù)屬性的信息,也可以理解為描述程序的數(shù)據(jù)。
Nest 中 @Module 配置的exports、providers、imports、controllers
都是元數(shù)據(jù),因?yàn)樗怯脕?lái)描述程序關(guān)系的數(shù)據(jù),這個(gè)數(shù)據(jù)信息不是展示給終端用戶的實(shí)際數(shù)據(jù),而是給框架程序讀取識(shí)別的。
4.2 Nest 裝飾器
如果看看 Nest 中的裝飾器源碼,會(huì)發(fā)現(xiàn),幾乎每一個(gè)裝飾器本身只是通過(guò) reflect-metadata 定義了一個(gè)元數(shù)據(jù)。
@Injectable裝飾器
export function Injectable(options?: InjectableOptions): ClassDecorator { return (target: object) => { Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target); Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target); }; }
這里存在反射的概念,反射也比較好理解,拿 @Module 裝飾器舉例,定義元數(shù)據(jù) providers
,只是往providers
數(shù)組里傳入了類,在程序?qū)嶋H運(yùn)行時(shí)providers
里的類,會(huì)被框架程序自動(dòng)實(shí)例化變?yōu)樘峁┱?,不需要開(kāi)發(fā)者顯示的去執(zhí)行實(shí)例化和依賴注入。類只有在模塊中實(shí)例化了之后才變成了提供者。providers
中的類被反射了成了提供者,控制反轉(zhuǎn)就是利用的反射技術(shù)。
換個(gè)例子的話,就是數(shù)據(jù)庫(kù)中的 ORM(對(duì)象關(guān)系映射),使用 ORM 只需要定義表字段,ORM 庫(kù)會(huì)自動(dòng)把對(duì)象數(shù)據(jù)轉(zhuǎn)換為 SQL 語(yǔ)句。
const data = TableModel.build(); data.time = 1; data.browser = 'chrome'; data.save(); // SQL: INSERT INTO tableName (time,browser) [{"time":1,"browser":"chrome"}]
ORM 庫(kù)就是利用了反射技術(shù),讓使用者只需要關(guān)注字段數(shù)據(jù)本身,對(duì)象被 ORM 庫(kù)反射成為了 SQL 執(zhí)行語(yǔ)句,開(kāi)發(fā)者只需要關(guān)注數(shù)據(jù)字段,而不需要去寫 SQL 了。
4.3 reflect-metadata
reflect-metadata 是一個(gè)反射庫(kù),Nest 用它來(lái)管理元數(shù)據(jù)。reflect-metadata 使用 WeakMap,創(chuàng)建一個(gè)全局單實(shí)例,通過(guò) set 和 get 方法設(shè)置和獲取被裝飾對(duì)象(類、方法等)的元數(shù)據(jù)。
// 隨便看看即可 var _WeakMap = !usePolyfill && typeof WeakMap === "function" ? WeakMap : CreateWeakMapPolyfill(); var Metadata = new _WeakMap(); function defineMetadata(){ OrdinaryDefineOwnMetadata(){ GetOrCreateMetadataMap(){ var targetMetadata = Metadata.get(O); if (IsUndefined(targetMetadata)) { if (!Create) return undefined; targetMetadata = new _Map(); Metadata.set(O, targetMetadata); } var metadataMap = targetMetadata.get(P); if (IsUndefined(metadataMap)) { if (!Create) return undefined; metadataMap = new _Map(); targetMetadata.set(P, metadataMap); } return metadataMap; } } }
reflect-metadata 把被裝飾者的元數(shù)據(jù)存在了全局單例對(duì)象中,進(jìn)行統(tǒng)一管理。reflect-metadata 并不是實(shí)現(xiàn)具體的反射,而是提供了一個(gè)輔助反射實(shí)現(xiàn)的工具庫(kù)。
5 最后
現(xiàn)在再來(lái)看看前面的幾個(gè)疑問(wèn)。
為什么需要控制反轉(zhuǎn)?
什么是依賴注入?
裝飾器做了啥?
模塊 (@Module) 中的提供者(providers),導(dǎo)入(imports)、導(dǎo)出(exports)是什么實(shí)現(xiàn)原理?
1 和 2 我想前面已經(jīng)說(shuō)清楚了,如果還有點(diǎn)模糊,建議再回去看一遍并查閱一些其它文章資料,通過(guò)不同作者的思維來(lái)幫助理解知識(shí)。
5.1 問(wèn)題 [3 4] 總述:
Nest 利用反射技術(shù)、實(shí)現(xiàn)了控制反轉(zhuǎn),提供了元編程能力,開(kāi)發(fā)者使用 @Module 裝飾器修飾類并定義元數(shù)據(jù)(providersimportsexports),元數(shù)據(jù)被存儲(chǔ)在全局對(duì)象中(使用 reflect-metadata 庫(kù))。程序運(yùn)行后,Nest 框架內(nèi)部的控制程序讀取和注冊(cè)模塊樹(shù),掃描元數(shù)據(jù)并實(shí)例化類,使其成為提供者,并根據(jù)模塊元數(shù)據(jù)中的 providersimportsexports 定義,在所有模塊的提供者中尋找當(dāng)前類的其它依賴類的實(shí)例(提供者),找到后通過(guò)構(gòu)造函數(shù)注入。
本文概念較多,也并沒(méi)有做太詳細(xì)的解析,概念需要時(shí)間慢慢理解,如果一時(shí)理解不透徹,也不必太過(guò)著急。好吧,就到這里,這篇文章還是花費(fèi)不少精力,喜歡的朋友期望你能一鍵三連~