JavaScript 中的依賴注入
依賴注入 DI (Dependency Injection) 是編程領(lǐng)域中一個(gè)非常常見的設(shè)計(jì)模式,它指的是將應(yīng)用程序所需的依賴關(guān)系(如服務(wù)或其他組件)通過構(gòu)造函數(shù)參數(shù)或?qū)傩宰詣?dòng)注入的過程。這樣做的好處是可以減少組件之間的耦合,更容易測試和維護(hù)。
我們先舉個(gè)簡單的例子,我們有兩個(gè)簡單的 A 類和 B 類,在 B 類中依賴了 A 類,我們?cè)?nbsp;B 類中對(duì)它進(jìn)行實(shí)例化,并調(diào)用它的方法:
但是這種寫法是非常不靈活的, A 類作為一個(gè)依賴項(xiàng),它的初始化的邏輯被硬編碼到了 B 類中,如果我們想添加或修改其他的依賴項(xiàng),必須要不斷修改 B 類。
借助依賴注入的設(shè)計(jì)思想,我們可以將代碼改寫成下面這樣:
代碼只做了很小的改動(dòng),最核心的變化就是我們將 A 類和 B 的實(shí)現(xiàn)完全分離開來了,他們無需再關(guān)心依賴的實(shí)例化,因?yàn)槲覀儗⒁蕾嚨淖⑷胩岬降淖钔鈧?cè)。
這也就是為什么我們常常將依賴注入和控制反轉(zhuǎn) IoC (Inversion of Control) 放在一起講,控制反轉(zhuǎn)即將創(chuàng)建對(duì)象的控制權(quán)進(jìn)行轉(zhuǎn)移,以前創(chuàng)建對(duì)象的主動(dòng)權(quán)和創(chuàng)建時(shí)機(jī)是由自己把控的,而現(xiàn)在這種權(quán)力轉(zhuǎn)移到第三方。
可能在這樣簡單的代碼中我們還看不出來什么好處,但是在大型的代碼庫中,這種設(shè)計(jì)可以顯著幫助我們減少樣板代碼,創(chuàng)建和連接依賴項(xiàng)的工作由一段程序統(tǒng)一處理,我們無需擔(dān)心創(chuàng)建特定類所需的類的實(shí)例。
在 JavaScript 的各大框架中,依賴注入的設(shè)計(jì)模式也發(fā)揮著非常重要的作用,在 Angular、Vue.js、Next.js 等框架中都用到了依賴注入的設(shè)計(jì)模式。
JavaScript 框架中的依賴注入
Angular
在 Angular 中大量應(yīng)用了依賴注入的設(shè)計(jì)思想。Angular 使用依賴注入來管理應(yīng)用的各個(gè)部分之間的依賴關(guān)系,以及如何將這些依賴關(guān)系注入到應(yīng)用中,例如你可以使用依賴注入來注入服務(wù)、組件、指令、管道等。
比如我們現(xiàn)在有個(gè)日志打點(diǎn)的工具類,我們可以使用 Injectable 將其指定為可注入對(duì)象。
然后在組件中使用時(shí),無需進(jìn)行實(shí)例化,直接在 constructor 的參數(shù)中就可以取出自動(dòng)注入好的對(duì)象:
Vue.js
在 Vue.js 中,provide 和 inject 其實(shí)也使用了依賴注入的設(shè)計(jì)模式。
provide 屬性可以用來在父組件中提供一個(gè)值,這個(gè)值可以在父組件的所有子組件中注入。
inject 屬性可以用來在子組件中注入父組件提供的值。
React.js
在 React.js 中,并沒有直接使用依賴注入的地方,不過我們依然可以借助一些第三方庫來實(shí)現(xiàn), 比如我們可以通過 InversifyJS 提供的 injectable decorator 標(biāo)記 class 是可被注入的。
在組件中,我們可以直接調(diào)用注入的 provide 方法,而組件內(nèi)部不用關(guān)心它的實(shí)現(xiàn)。
手動(dòng)實(shí)現(xiàn)依賴注入
前面我們提到的 InversifyJS 實(shí)際上就是一個(gè)專門用來實(shí)現(xiàn)依賴注入的工具庫,它主要就由 injectable 、inject 等幾個(gè)裝飾器組成的,這么神奇的功能究竟是咋實(shí)現(xiàn)的呢,下面我們手動(dòng)來實(shí)現(xiàn)一下。
首先我們來明確一個(gè)需求場景,假設(shè)我們要使用 Koa 框架開發(fā)一個(gè)簡單的 Node.js 服務(wù)。
在 Koa 中,Controller 用來處理用戶請(qǐng)求和響應(yīng),它負(fù)責(zé)接收用戶的請(qǐng)求,然后調(diào)用相應(yīng)的服務(wù)或業(yè)務(wù)邏輯進(jìn)行處理,最后將處理結(jié)果返回給用戶。Service 用來封裝業(yè)務(wù)邏輯和數(shù)據(jù)處理,它負(fù)責(zé)實(shí)現(xiàn)應(yīng)用程序的核心功能。
Service 通常會(huì)被多個(gè) Controller 所調(diào)用,它們之間是松散耦合的關(guān)系,我們希望用兩裝飾器來實(shí)現(xiàn) Service 的自動(dòng)依賴注入:
在實(shí)現(xiàn)過程中我們可能會(huì)用到兩個(gè)非常重要的 API,Metadata Reflection API 以及 Decorator API,我們先分別來回顧一下它們的基礎(chǔ)知識(shí)。
Decorator API
裝飾器模式是一種經(jīng)典的設(shè)計(jì)模式,其目的是在不修改被裝飾者(如某個(gè)函數(shù)、某個(gè)類等)源碼的前提下,為被裝飾者增加 / 移除某些功能。一些現(xiàn)代編程語言在語法層面提供了對(duì)裝飾器模式的支持,并且各語言中的現(xiàn)代框架都大量應(yīng)用了裝飾器。主要用處分為兩大類:
- 收集用戶定義的類/函數(shù)的信息(例如,用于生成路由表,用于實(shí)現(xiàn)依賴注入,等等)
- 對(duì)用戶定義的類/函數(shù)進(jìn)行增強(qiáng),增加額外功能
我們目前用的比較多的裝飾器就是 TypeScript 的實(shí)驗(yàn)性裝飾器,以及 ECMAScript 中還處于 legacy 階段的 Decorator API,下面是它的用法:
裝飾類的時(shí)候,裝飾器方法一般會(huì)接收一個(gè)目標(biāo)類作為參數(shù),下面是一個(gè)示例,給類增加靜態(tài)屬性、原型方法:
類屬性裝飾器可以用在類的屬性、方法、get/set 函數(shù)中,一般會(huì)接收三個(gè)參數(shù):
- target:被修飾的類
- name:類成員的名字
- descriptor:屬性描述符,對(duì)象會(huì)將這個(gè)參數(shù)傳給 Object.defineProperty
下面是一個(gè)示例,可以修改類屬性為只讀:
Metadata Reflection API
Reflect 是 JavaScript 中的一個(gè)內(nèi)置對(duì)象,它提供了一組用于操作對(duì)象的方法。它與其他內(nèi)置對(duì)象類似,但是它的目的是為了提供一組用于操作對(duì)象的通用方法。
Reflect Metadata 是 ES7 的一個(gè)提案,它主要用來在聲明的時(shí)候添加和讀取元數(shù)據(jù)。
Reflect.getMetadata('design:type', target, key) 可以用來獲取類 target 中屬性 key 的類型信息:
Reflect.getMetadata('design:paramtypes', target, key) 可以用來獲取類 target 中屬性 key 的函數(shù)參數(shù)類型;
Reflect.getMetadata('design:returntype', target, key) 可以用來獲取類 target 中屬性 key 的函數(shù)返回值類型。
除能獲取固定的類型信息之外,也可以自定義 MetaData,并在合適的時(shí)機(jī)獲取它的值,示例如下:
好了,有了這些知識(shí),我們就可以手動(dòng)來實(shí)現(xiàn)一個(gè)依賴注入裝飾器了。
實(shí)現(xiàn)依賴注入
再明確一下我們的需求:在不同服務(wù)的 Controller 中共用 Service,使用 Service 時(shí)可以自動(dòng)獲取已注入的 Service 實(shí)例,同時(shí) Service 里可以獲取到請(qǐng)求的 Context 信息。
首先我們來實(shí)現(xiàn),Inject 裝飾器:
- 在Controller 中注冊(cè)需要用到哪些 Service
- 通過design:type 獲取 Service 的類型信息
- 通過自定義metadata 存儲(chǔ) Controller 中用到哪些 Service
然后是 UseService 裝飾器:
- 在請(qǐng)求過來時(shí)取出metadata 中存儲(chǔ)的 Controller 和 Service 對(duì)應(yīng)信息
- 將Service 實(shí)例化,并將 Context 傳入 Service
好了,接下來就可以愉快的使用了~