<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          使用IoC來管理你的Vue應(yīng)用

          共 8572字,需瀏覽 18分鐘

           ·

          2020-10-26 18:14


          本文來自于曉黑板前端技術(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.vuelistService.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



          別的小朋友都有人
          點(diǎn)贊
          了,我的呢?



          瀏覽 66
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  操逼二区| 麻豆精品福利 | 大鸡吧在线观看视频 | 三区在线观看视频 | 黄色A片一级 |