使用IoC來管理你的Vue應(yīng)用
本文來自于曉黑板前端技術(shù)的投稿
原文鏈接:https://juejin.im/post/6881883342623473677
伴隨著現(xiàn)代應(yīng)用功能越來越多,各個(gè)模塊不可避免的相互依賴、引用,如果沒有任何策略的堆代碼,應(yīng)用的維護(hù)會(huì)變成一種災(zāi)難。因此,有效的管理和解耦依賴變得很重要。本文從依賴注入的角度切入,嘗試?yán)孟嚓P(guān)的理念來解決這個(gè)問題。
先看一個(gè)例子
假設(shè)我們有兩個(gè)模塊:一個(gè)實(shí)現(xiàn) http 請求,另一個(gè)實(shí)現(xiàn)路由跳轉(zhuǎn)。
//?httpService.ts
export?class?HttpService?{
????name?=?'HttpService'
}
//?routerService.ts
export?class?RouterService?{
????name?=?'RouterService'
}
現(xiàn)在有一個(gè)登錄功能使用了上述兩個(gè)模塊:
//?login.ts
export?class?Login?{
????constructor()?{
????????this.httpService?=?new?HttpService()
????????this.routerService?=?new?RouterService()
????}
}
在上面的代碼中,為了實(shí)現(xiàn)登錄功能,Login 類內(nèi)部分別實(shí)例化了 HttpService和 RouterService。雖然上述代碼可以正常工作,但是不是很靈活。假如修改HttpService 需要增加 token 信息:
//?httpService.ts
export?class?HttpService?{
????name?=?'HttpService'
????constructor(token:string)?{}
}
此時(shí)我們就需要編輯 Login 類,在 HttpService 實(shí)例化時(shí)增加 token 參數(shù)。假如我們想要給 RouterService 增加操作或者再次更新 HttpService,就不可避免每次都要重新編輯 Login 類。
為了解決現(xiàn)狀,首先我們把依賴作為參數(shù)傳遞給模塊:
//?login.ts
export?class?Login?{
????constructor(httpService:?HttpService,?routerService:?RouterService)?{
????????this.httpService?=?httpService
????????this.routerService?=?routerService
????}
}
這樣就完成了 Login 和 HttpService、RouterService 的解耦,Login 不再親自創(chuàng)建 httpService 和 routerService,而是使用他們。
然而這樣還有新的問題:想象一下假如 HttpService 和 RouterService 在很多地方被調(diào)用,如果增加他們的依賴條件,我們就不得不改變所有調(diào)用他們的地方。
//?routerService.ts
export?class?RouterService?{
????name?=?'RouterService'
????constructor(authService:AuthService)?{}
}
// RouterService的依賴條件發(fā)生了改變,這時(shí)候就需要在下面的不同文件中修改,如果文件過多,這種方法就顯得很不合適了。
//?并且我們發(fā)現(xiàn)出現(xiàn)了多個(gè)HttpService和RouterService實(shí)例。
//?login.vue
const?httpService?=?new?HttpService(token)
const?authService?=?new?AuthService()
const?routerService?=?new?RouterService(authService)?//?增加依賴條件
const?login?=?new?Login(httpService,?routerService)
//?list.vue
const?httpService?=?new?HttpService(token)
const?authService?=?new?AuthService()
const?routerService?=?new?RouterService(authService)?//?增加依賴條件
const?login?=?new?Login(httpService,?routerService)
因此,我們需要一個(gè)來幫助我們管理依賴的工具。
這就是依賴注入要解決的問題,先列一下我們要實(shí)現(xiàn)的目標(biāo):
依賴的創(chuàng)建和查找交給第三方 依賴本身的依賴可以自動(dòng)被創(chuàng)建 可以手動(dòng)注冊依賴 依賴的類型不僅限于類,也可以是值,或者函數(shù) 為了共享數(shù)據(jù),保持依賴單例 語法簡潔,不需要寫太多的代碼
什么是IoC、DI
在實(shí)現(xiàn)我們的目標(biāo)之前,我們需要先弄明白依賴注入涉及的相關(guān)概念。
控制反轉(zhuǎn)
控制反轉(zhuǎn)(Inversion of Control,縮寫IoC),是面向?qū)ο缶幊讨械囊环N設(shè)計(jì)原則,可以用來降低計(jì)算機(jī)代碼之間的耦合度。其中最常見的方式叫做依賴注入(Dependency Injection,簡稱DI),還要一種方式叫“依賴查找”(Dependency Lookup)。通過控制反轉(zhuǎn),對象在被創(chuàng)建的時(shí)候,由一個(gè)調(diào)控系統(tǒng)內(nèi)所有對象的外界實(shí)體,將其所依賴的對象的引用傳遞(注入)給它?!S基百科
依賴注入
在軟件工程中,依賴注入(Dependency Injection)的意思為,給予調(diào)用方它所需要的事物。“依賴”是指可被方法調(diào)用的事物。依賴注入形式下,調(diào)用方不再直接使用“依賴”,取而代之是“注入”?!白⑷搿笔侵笇ⅰ耙蕾嚒眰鬟f給調(diào)用方的過程。在“注入”之后,調(diào)用方才會(huì)調(diào)用該“依賴”。傳遞依賴給調(diào)用方,而不是讓調(diào)用方直接獲得依賴,這個(gè)是該設(shè)計(jì)的根本需求?!S基百科
控制反轉(zhuǎn)概念中提到的調(diào)控系統(tǒng)就是我們要實(shí)現(xiàn)的目標(biāo)中提到的第三方,即 IoC 容器。我們通過容器將A對象中用到的 B 對象在外部 new 出來并注入到A中,取代在 A 中顯式的 new 一個(gè) B 對象。從而達(dá)到設(shè)計(jì)的目的:解耦調(diào)用方和依賴,提高代碼可讀性以及代碼重用性。
實(shí)現(xiàn)一個(gè)IoC容器
在了解完依賴注入相關(guān)的概念之后,我們來手動(dòng)實(shí)現(xiàn)一個(gè)簡單的 IoC 容器
1.定義容器接口
//?interface.ts
export?interface?ContainerInterface?{
??addProvider(token:?Token,provider:?any):?void;
??getProvider(token:?Token):?T;
}
Container 類至少需要實(shí)現(xiàn) addProvider() 和 getProvider() 兩個(gè)方法。接著我們定義參數(shù)類型
2.定義 Token 和 Provider
//?interface.ts
export?interface?Type?extends?Function?{
??[INJECTED]?:?Type<any>[]?//?在3.3實(shí)現(xiàn)@Injectable()用到
??new?(...args:?any[]):?T;
}
export?type?Token?=?string?|?Type
Token:DI令牌,它關(guān)聯(lián)到一個(gè)依賴提供者,用來查找依賴。我們定義它的類型為聯(lián)合類型,即可以是字符串也可以是函數(shù)類型。Provider:一個(gè)提供者對象,定義了如何獲取與 DI 令牌(token) 相關(guān)聯(lián)的可注入依賴。這里我們不限制提供者的類型,可以是任意類型的值(any)
3.實(shí)現(xiàn)裝飾器@Injectable()
3.1 裝飾器
一個(gè)函數(shù),用來修飾緊隨其后的類或?qū)傩远x。裝飾器(也叫注解)是 JavaScript 的一種語言特性,是一項(xiàng)位于 stage 2 的試驗(yàn)特性。
我們這里使用類裝飾器 @Injectable() 來標(biāo)記對象為可注入對象。
3.2 元數(shù)據(jù)反射
Reflect Metadata是ES7的一個(gè)提案,它主要用來在聲明的時(shí)候添加和讀取元數(shù)據(jù)。TypeScript在1.5+的版本結(jié)合refelct-metadata庫已經(jīng)支持,使用方法:
npm i refelct-metadata --save在 tsconfig.json里配置emitDecoratorMetadata選項(xiàng)。
然后在項(xiàng)目中引入reflect-metadata后,就可以使用Reflect.getMetadata的API了。我們這里主要使用Reflect.getMetadata("design:paramtypes", target, key)方法來獲取函數(shù)參數(shù),記錄依賴信息。
3.3 實(shí)現(xiàn)@Injectable()
了解了裝飾器和元數(shù)據(jù)反射這兩個(gè)前置條件的相關(guān)概念后,我們就可以來實(shí)現(xiàn)Injectable 函數(shù)了
//?injectable.ts
import?'reflect-metadata'
import?{?Type?}?from?'./interface'
export?const?INJECTED?=?'__INJECTED_TYPES'
export?function?Injectable()?{
??return?function(target:?any)?{
????//?記錄前置依賴
????const?outInjected?=?Reflect.getMetadata('design:paramtypes',?target)?as?(Type<any>?|?undefined)[]
????const?innerInjected?=?target[INJECTED]
????if(!innerInjected)?{
??????target[INJECTED]?=?outInjected
????}?else?{
??????outInjected.forEach((argType,?index)?=>?{
????????if(!innerInjected[index])?{
??????????target[INJECTED][index]?=?argType
????????}
??????})
????}
????return?target
??}
}4.實(shí)現(xiàn)Container
我們在前面定義好了ContainerInterface和兩個(gè)關(guān)鍵的方法addProvider()和getProvider(),下面我們就來分別實(shí)現(xiàn)
4.1 addProvider()
//?container.ts
import?{?ContainerInterface,?Token?}?from?"./interface";
export?class?Container?implements?ContainerInterface?{
??private?_providers?=?new?Map();
??
??addProvider(token:?Token<any>,?provider:?any)?{
????this._providers.set(token,?provider);
??}
}Container類具有一個(gè)私有變量_providers,類型為Map,保存所有的提供者。提供者類型為any,所以我們可以注冊任意類型值的provider。addProvider()注冊依賴。
4.2 getProvider()
getProvider(token:?Token):?T?{
????if?(this._providers.has(token))?{
??????return?this._providers.get(token);
????}?else?{
??????if?(isClassProvider(token))?{
????????const?instance?=?this.getInstanceFromClass(token?as?Type<any>);
????????this.addProvider(token,?instance);
????????return?instance;
??????}?else?{
????????throw?new?Error(`${token}?is?a?normal?string?that?cannot?be?instantiated`);
??????}
????}
??}
通過getProvider()方法,傳入token就可以拿到注冊過的provider。這里我們增加了getInstanceFromClass方法,用來自動(dòng)實(shí)例化class類型的依賴。
說明:在Angular DI的實(shí)現(xiàn)中,Provider為聯(lián)合類型
TypeProvider|ValueProvider|ClassProvider|ConstructorProvider| ExistingProvider|FactoryProvider|any[],考慮的場景比較全面,實(shí)現(xiàn)起來也很復(fù)雜。我們這里只是演示思路,所以只考慮class這一種類型,并且簡化Provider的類型為any
4.3 getInstanceFromClass()
private?getInstanceFromClass(provider:?Type):?T?{
????const?target?=?provider;
????if?(target[INJECTED])?{
??????const?injects?=?target[INJECTED]!.map(childToken?=>?this.getProvider(childToken));
??????return?new?target(...injects);
????}?else?{
??????if?(target.length)?{
????????throw?new?Error(
??????????`Injection?error.${target.name}?has?dependancy?injection?but,but?no?@Injectable()?decorate?it`
????????);
??????}
??????return?new?target();
????}
??}
還記得我們在實(shí)現(xiàn)@Injectable()時(shí)通過元數(shù)據(jù)反射拿到的參數(shù)信息嗎,當(dāng)時(shí)被我們記錄在了對象的[INJECTED]屬性上面。target[INJECTED]類型為Type,使用map()方法讓數(shù)組中的每一個(gè)元素都調(diào)用getProvider()方法,遞歸獲取所有的依賴,然后返回目標(biāo)類的實(shí)例。
至此,一個(gè)簡易版的 IoC 容器就制作完成了,讓我們來測試一下是否可行吧。
//?test.ts
import?{?Container?}?from?'./container'
import?{?Injectable?}?from?"./injectable";
@Injectable()
class?AuthService?{
??name?=?'authService'
}
@Injectable()
class?RouterService?{
??name?=?'routerService'
??constructor(private?authService:?AuthService){}
}
const?container?=?new?Container()
const?routerService?=?container.getProvider(RouterService)
console.log(routerService)
在瀏覽器中運(yùn)行測試代碼,可以在控制臺(tái)中看到成功后的打印信息
在Vue中使用
在前文中我們實(shí)現(xiàn)了一個(gè)簡易版的 IoC 容器,現(xiàn)在回到我們的主題“使用 IoC 來管理你的 Vue 應(yīng)用”。
眾所周知,.vue文件使用三段式代碼分別實(shí)現(xiàn)template,script,style,并基于component來拆分組合代碼。但是當(dāng)組件中js邏輯過多時(shí),.vue文件就會(huì)變得比較臃腫,不利于維護(hù)。我相信很多人都碰到過這種情況,也都有各種的解決方案。
本文不討論哪種方案更好,旨在通過依賴注入這個(gè)點(diǎn)來切入 Vue 項(xiàng)目,提供一種維護(hù)項(xiàng)目的思路。我們先來看一張圖片
這張圖片形象的說明了 IoC 容器扮演的角色,通過控制反轉(zhuǎn)將業(yè)務(wù)模塊中各個(gè)容易變化的部件抽象解耦,不同的模塊去實(shí)現(xiàn)自己的定制需求,而通用代碼不要重復(fù)開發(fā)。
這里我們使用插件來為 Vue 提供 IoC 容器的功能。
//?plugin.ts
import?{?VueConstructor?}?from?"vue";
import?{?Container?}?from?"./index";
import?{?Type?}?from?"./interface";
export?default?{
??install(Vue:?VueConstructor,?rootContainer:?Container)?{
????Vue.mixin({
??????beforeCreate()?{
????????const?{?viewInject?}?=?this.$options;
????????if?(viewInject)?{
??????????const?injects?=?viewInject;
??????????for?(const?name?in?injects)?{
????????????this[name]?=?rootContainer.getProvider(injects[name]?as?Type<any>);
??????????}
????????}
??????}
????});
??}
};
我們將依賴的注入位置放在 Vue 實(shí)例的初始化選項(xiàng)里,類型定義為對象viewInject?: Object。之后在 rootContainer 里查找依賴,如果存在就返回,沒有就自動(dòng)實(shí)例化。
插件完成之后,我們就可以在 Vue 項(xiàng)目中使用 IoC 了。
首先在入口文件main.ts中安裝插件
//?main.ts
import?Vue?from?'vue'
import?IocPlugin?from?'./plugin'
import?{?Container??}?from?"./container"
Vue.use(IocPlugin,?new?Container())
然后我們創(chuàng)建兩個(gè)文件list.vue和listService.ts
//?list.vue
import?ListService?from?'./listService'
export?default?{
??name:?'list',
??viewInject:?{
????listService:?ListService
??},
??mounted()?{
????console.log(this.listService.getList())
??}
}
//?listService.ts
import?{?Injectable?}?from?"./injectable";
@Injectable()
export?default?class?ListService?{
??getList():?number[]?{
????const?data?=?[1,2,3,4,5,6]
????return?data;
??}
}
復(fù)制代碼
在瀏覽器中運(yùn)行上述代碼,控制臺(tái)會(huì)打印出來getList()的返回值
不局限于Vue
至此,我們已經(jīng)實(shí)現(xiàn)了一個(gè)在 Vue 應(yīng)用中使用 IoC 的 MVP 了。雖然還有很多功能沒有實(shí)現(xiàn),比如 provider scope,@Inject(),依賴的生命周期等,但這不妨礙我們理解 IoC 和 DI 的理念,解耦我們的項(xiàng)目。不局限于 Vue,你也可以在其他框架中使用 DI 。
參考資料:
Inversion of Control Containers and the Dependency Injection pattern Dependency injection in JavaScript Angular 文檔
?
推薦閱讀? webpack 5 正式發(fā)布! 面試會(huì)遇到的手寫 Pollyfill 都在這里了 一份9年工作經(jīng)驗(yàn)大佬推薦的前端書單評測 React17新特性:啟發(fā)式更新算法 React 狀態(tài)管理庫的battle (setState/useState vs Redux vs Mobx)

覺得本文對你有幫助?請分享給更多人
關(guān)注「前端之露」加星標(biāo),跟露姐學(xué)前端
商務(wù)合作請?zhí)砑游⑿?/span>:FE-ROAD-ON

