一統江湖的大前端(10)——inversify.js控制反轉

      網友投稿 972 2025-04-02

      一統江湖的大前端(10)——inversify.js控制反轉

      《大史住在大前端》前端技術博文集可在下列地址訪問:

      【github總基地】【博客園】【華為云社區】【掘金】

      Angular是由Google推出的前端框架,曾經與React和Vue一起被開發者稱為“前端三駕馬車”,但從隨著技術的迭代發展,它在國內前端技術圈中的存在感變得越來越低,通常只有Java技術棧的后端工程師在考慮轉型全棧工程師時才會優先考慮使用。Angular沒落的原因并不是因為它不夠好,反而是因為它過于優秀,還有點高冷,忽略了國內前端開發者的學習意愿和接受能力,就好像一個學霸,明明成績已經很好了,但還是不斷尋求挑戰來實現自我突破,盡管他從不吝嗇分享自己的所思所想,但他所接觸的領域令廣大學渣望塵莫及,而學渣們感興趣的事物在他看來又有些無聊,最終的結果通常都只能是大家各玩各的。

      了解過前端框架發展歷史的讀者可能會知道在2014年時Angular1.x版本有多火,盡管它并不是第一個將MVC思想引入前端的框架,但的確可以算作第一個真正撼動jQuery江湖地位的黑馬,由于在升級Angular2.0版本的過程中強制使用Typescript作為開發語言,使它失去了大量用戶,Vue和React也開始趁勢崛起,很快便形成“三足鼎立”之勢。但Angular似乎并沒有回頭的意思,而是保持著半年一個大版本的迭代速度將更多的新概念帶給前端,從而推動前端領域的技術演進,也推動著前端向正規的軟件工程方向逐步靠攏。我常說Angular是一個孤傲的變革者,它喜歡引入和傳播思想層面的概念,將那些被公認為正確優雅且有助于工程實踐的事物帶給前端,它似乎總是在說“這個是好的,那我們就在Angular里實現它吧”,從早期的模塊化和雙向數據綁定的引入,到后來的組件化、Typescript、Cli、RxJS、DI、AOT等等,一個個特性的引入都引導著開發者從不同的角度去思考,擴展著前端領域的邊界,也對團隊的整體素養提出更高的要求。如果你看看今天Typescript在前端開發領域的江湖地位,回顧一下早期的Vue和Angular1.x之間的差異性,看看RxJS和React Hooks出現的時間差,就不難明白Angular的思想有多前衛。

      “如果一件事情是軟件工程師應該懂的,那么你就應該弄懂它”,這在筆者看來是Angular帶給前端開發者最有價值的思想,精細化分工對企業而言是非常有利的,但卻非常容易限制技術人員本身的視野和職業發展,就好像流水線上從事體力勞動的工人,就算對自己負責的環節再熟悉,也無法僅僅憑此來保障整個零件加工最終的質量。我們應該在協作中對自己的產出負責,但只有摘掉職位頭銜帶來的思維枷鎖,你才能成為一個更全面的軟件工程師,它并不是關于技能的,而是關于思維方式的,那些源于內心深處的認知和定位會決定一個人未來所能達到的高度。

      無論你是否會在自己的項目中使用Angular,都希望你能夠花一點時間了解它的理念,它能夠擴展你對于編程的認知,領略軟件技術思想層面的美。本章中我們就一起來學習Angular框架中最具特色的技術——DI(依賴注入),了解相關的IOC設計模式、AOP編程思想以及實現層面的裝飾器語法,最后再看看如何使用Inversify.js來在自己的代碼中實現“依賴注入”。如果你對此感興趣,可以通過Java的Spring框架進行更深入的研究。

      依賴為什么需要注入

      依賴注入(Dependency Injection,簡稱DI)并不算一個復雜的概念,但想要真正理解它背后的原理卻不是一件容易的事情,它的上游有更加抽象的IOC設計思想,下游有更加具體的AOP編程思想和裝飾器語法,只有搞清楚整個知識脈絡中各個術語之間的聯系,才能夠建立相對完整的認知,從而在適合的場景使用它,核心概念的關系如下圖所示:

      面向對象的編程是基于“類”和“實例”來運作的,當你希望使用一個類的功能時,通常需要先對它進行實例化,然后才能調用相關的實例方法。由于遵循“單一職責”的設計原則,開發者在實現復雜的功能時并不會將代碼都寫在一起,而是依賴于多個子模塊協作來實現相應的功能,如何將這些模塊組合在一起對于面向對象編程而言是非常關鍵的,這也是設計模式相關的知識需要解決的主要問題,代碼結構設計不僅影響團隊協作的開發效率,也關系著代碼在未來的維護和擴展成本。畢竟在真實的開發中,不同的模塊可能由不同的團隊來開發維護,如果模塊之間耦合度太高,那么偏底層的模塊一旦發生變更,整個程序在代碼層面的修改可能會非常多,這對于軟件可靠性的影響是非常嚴重的。

      在普通的編程模式中,開發者需要引入自己所依賴的類或者相關類的工廠方法(工廠方法是指運行后會得到實例的方法)并手動完成子模塊的實例化和綁定,如下所示:

      import B from ‘../def/B’;

      import createC from ‘../def/C’;

      class A{

      constructor(paramB, paramC){

      this.b = new B(paramB);

      this.c = createC(paramC);

      }

      actionA(){

      this.b.actionB();

      }

      }

      從功能實現的角度而言,這樣做并沒有什么問題,但從代碼結構設計的角度來看卻存在著一些潛在的風險。首先,在生成A的實例時所接受的構造參數實際上并不是由A自身來消費的,而是將其透傳分發給它所依賴的B類和C類,換句話說,A除了需要承擔其本身的職責之外,還額外承擔了B和C的實例化任務,這與面向對象編程中的SOLID基本設計原則中的“單一職責”原則是相悖的;其次,A類的實例a僅僅依賴于B類實例的actionB方法,如果對actionA方法進行單元測試,理論上只要actionB方法執行正確,那么單元測試就應該能夠通過,但在前文的示例代碼中,這樣的單元測試實際上已經變成了包含B實例化過程、C實例化過程以及actionB方法調用的小范圍集成測試,任何一個環節發生異常都會導致單元測試無法通過;最后,對于C模塊而言,它對外暴露的工廠方法createC可以對實例化的過程進行控制,例如維護一個全局單例對象,但對于直接導出類定義的B模塊而言,每個依賴它的模塊都需要自己完成對它的實例化,如果未來B類的構造方法發生了變化,恐怕開發者只能利用IDE全局搜索所有對B類進行實例化的代碼然后手動進行修改。

      “依賴注入”的模式就是為了解決以上的問題而出現的,在這種編程模式中,我們不再接收構造參數然后手動完成子模塊的實例化,而是直接在構造函數中接受一個已經完成實例化的對象,在代碼層面的基本實現形式變成了下面的樣子:

      class A{

      constructor(bInstance, cInstance){

      this.b = bInstance;

      this.c = cInstance;

      }

      actionA(){

      this.b.actionB();

      }

      }

      對于A類而言,它所依賴的b實例和c實例都是在構造時從外部注入進來的,這意味著它不再需要關心子模塊實例化的過程,而只需要以形參的方式聲明對這個實例的依賴,然后專注于實現自己所負責的功能即可,對子模塊實例化的工作交給A類外部的其他模塊來完成,這個外部模塊通常被稱為“IOC容器”,它本質上就是“類注冊表+工廠方法”,開發者通過“key-value”的形式將各個類注冊到IOC容器中,然后由IOC容器來控制類的實例化過程,當構造函數需要使用其他類的實例時,IOC容器會自動完成對依賴的分析,生成需要的實例并將它們注入到構造函數中,當然需要以單例模式來使用的實例都會保存在緩存中。

      另一方面,在“依賴注入”的模式下,上層的A類對下層模塊B和C的強制依賴已經消失了,它和你在JavaScript基礎中了解到的“鴨式辨形”機制非常類似,只要實際傳入的bInstance參數也實現一個actionB方法,且在函數簽名(或者說類型聲明)上和B類的actionB方法保持一致,對于A模塊而言它們就是一樣的,這可以極大地降低對A模塊進行單元測試的難度,而且方便開發者在開發環境、測試環境和生產環境等不同的場景中對特定的模塊提供完全不同的實現,而不是被B模塊的具體實現所限制,如果你了解過面向對象編程的SOLID設計原則就會明白,“依賴注入”實際上就是對“依賴倒置原則”的一種體現。

      依賴倒置原則(Dependency Inversion):

      上層模塊不應該依賴底層模塊,它們應該依賴于共同的抽象。

      抽象不應該依賴于細節,細節應該依賴于抽象。

      這就是“依賴注入”和“控制反轉”的基本知識,依賴的實例由原本手動生成的方式轉變為由IOC容器自動分析并以注入的方式提供,原本由上層模塊控制的實例化過程被轉移給IOC容器來完成,本質上它們都是對面向對象基本設計原則的實現手段,目的就是為了降低模塊之間的耦合性,隱藏更多細節。很多時候,設計模式的應用的確會讓本來直觀清晰的代碼變得晦澀難懂,但換來的卻是整個軟件對于需求不確定性的抵御能力。初級開發者在編程時千萬不要只滿足于實現眼前的需求,而是應該多思考如何降低需求變動可能給自己造成的影響,甚至直接“控制反轉”將細節定制的環節以配置文件的形式提供給產品人員。請時刻記得,軟件工程師的任務是設計軟件,讓軟件和可復用的模塊去幫助自己實現需求,而不是把自己變成一個擅長搬磚的工具。

      IOC容器的實現

      基于前文的分析,你可以先自己來思考一下基本的IOC容器應該實現的功能,然后再繼續下面的內容。IOC容器的主要職責是接管所有實例化的過程,那么它肯定能夠訪問到所有的類定義,并且知道每個類的依賴,但類的定義可能編寫在多個不同的文件中,IOC容器要如何來完成依賴收集呢?比較容易想到的方法就是為IOC容器實現一個注冊方法,開發者在每個類定義完成后調用注冊方法將自己的構造函數和依賴模塊的名稱注冊到IOC容器中,IOC容器以閉包的形式維護一個私有的類注冊表,其中以鍵值對的形式記錄了每個類的相關信息,例如工廠方法、依賴列表、是否使用單例以及指向單例的指針屬性等等,你可以根據實際需要去添加更多的配置信息,這樣一來IOC容器就擁有了訪問所有類并進行實例化的能力;除了收集信息外,IOC容器還需要實現一個獲取所需實例的調用方法,當調用方法執行時,它可以根據傳入的鍵值去找到對應的配置對象,根據配置信息返回正確的實例給調用者。這樣一來,IOC容器就可以完成實例化的基本職能。

      IOC容器的使用對于模塊之間耦合關系的影響是非常明顯的,在原來的手動實例化模型中,模塊之間的關系時相互耦合的,模塊的變動很容易直接導致依賴它的模塊發生修改,因為上層模塊對底層模塊本身產生了依賴;在引入IOC容器后,每個類只需要調用容器的注冊方法將自己的信息登記進去,其他模塊如果對它有依賴,通過調用IOC容器提供的方法就可以獲取到所需要的實例,這樣一來,子模塊實例化的過程和主模塊之間就不再是強依賴關系,子模塊發生變化時也不需要再去修改主模塊,這樣的處理模式對于保障大型應用的穩定性非常有幫助。現在我們再回過頭看看那張經典的控制反轉示意圖,就比較容易理解其背后完成的工作了:

      IOC的機制其實和招聘是非常類似的,公司的項目要落地實施,需要項目經理、產品、設計、研發、測試等多個不同崗位的員工協作來完成,對公司而言,更加關注每個崗位需要多少人,低中高不同級別的人員比例大概是多少,從而可以從宏觀的角度來評估人力配置是否足以保障項目落地,至于具體招聘到的人是誰,公司并不需要在意;而HR的角色就像是IOC容器,只需要按照公司的標準去市場上搜尋符合條件的候選人,并用面試來檢驗他是否符合用人要求就可以了。

      手動實現IOC容器

      下面我們使用Typescript來手動實現一個簡單的IOC容器類,你可以先體會一下它的基本用法,因為強類型的特點,它更容易幫助你在抽象層面了解自己所寫的代碼,另外它的面向對象特性也更加完備,語法特征和Java非常相似,是學習收益率很高的選擇。相比于JavaScript的靈活,Typescript的代碼增加了非常多的限制,最開始你可能會被類型系統搞的暈頭轉向,但當你熟悉后,就會慢慢開始享受這種代碼層面的約束和自律帶來的工程層面的清晰。我們先來編寫基本的結構和必要的類型限制:

      // IOC成員屬性

      interface iIOCMember {

      factory: Function;

      singleton: boolean;

      instance?: {}

      }

      // 定義IOC容器

      Class IOC {

      private container: Map;

      constructor() {

      this.container = new Map();

      }

      }

      在上面的代碼中我們定義了2個接口和1個類,IOC容器類中有一個私有的map實例,它的鍵是PropertyKey類型,這是Typescript中預設的類型,指string | number | symbol的聯合類型,也就我們平時用作鍵的類型,而值的類型是iIOCMember,從接口的定義中可以看到,它需要一個工廠方法、一個標記是否為單例的屬性以及指向單例的指針,接下來我們在IOC容器類上添加用于注冊構造函數的方法bind:

      // 構造函數泛型

      interface iClass {

      new(...args: any[]): T

      }

      // 定義IOC容器

      class IOC {

      private container: Map;

      constructor() {

      this.container = new Map();

      }

      bind(key: string, Fn: iClass) {

      const factory = () => new Fn();

      this.container.set(key, { factory, singleton: true });

      }

      }

      bind方法的邏輯并不難理解,初學者可能會對iClass接口的聲明比較陌生,它是指實現了這個接口的實體在被new時需要返回預設類型T的實例,換句話說就是這里接收的是一個構造函數,new( )作為接口的屬性時也被稱為“構造器字面量”。但IOC容器是延遲實例化的,想要讓構造函數延遲執行,最簡單的方式就是定義一個簡單的工廠方法(如前文示例中的factory方法所做的那樣)并將它保存起來,等需要時在進行實例化。最后我們再來實現一個調用方法use:

      use(namespace: string) {

      let item = this.container.get(namespace);

      if (item !== undefined) {

      if (item.singleton && !item.instance) {

      item.instance = item.factory();

      }

      return item.singleton ? item.instance : item.factory();

      } else {

      throw new Error('未找到構造方法');

      }

      }

      use方法接收一個字符串并根據它從容器中找出對應的值,這里的值就會符合iIOCMember接口定義的結構,為了方便演示,如果沒有找到對應的記錄就直接報錯,如果需要單例且還沒有生成過相應的對象,就調用工廠方法來生成單例,最終根據配置信息來判斷是返回單例還是創建新的實例。現在我們就可以來使用這個IOC容器了:

      class UserService {

      constructor() {}

      test(name: string) {

      console.log(`my name is ${name}`);

      }

      }

      const container = new IOC();

      container.bind('UserService', UserService);

      const userService = container.use('UserService');

      userService.test('大史不說話');

      使用ts-node直接運行Typescript代碼后,就可以在控制臺看到打印的信息。前文的IOC容器僅僅實現了最核心的流程,它還不具備依賴管理和加載的功能,希望你可以自己嘗試來進行實現,需要做的工作就是在注冊信息時提供依賴模塊鍵的列表,然后在實例化時通過遞歸的方式將依賴模塊都映射為對應的實例,當你學習webpack模塊加載原理時也會接觸到類似的模式,下一小節中我們來看看Angular1.x版本如何完成對依賴的自動分析和注入。

      AngularJS中的依賴注入

      AngularJS在業內特指Angular2以前的版本(更高的版本中統一稱為Angular),它提倡使用模塊化的方式來分解代碼,將不同層面的邏輯拆分為Controller、Service、Directive、Filter等類型的模塊,從而提高整個代碼的結構性,其中Controller模塊是用來連接頁面和數據模型的,通常每個頁面會對應一個Controller,典型的代碼片段如下所示:

      var app = angular.module(“myApp”, []);

      //編寫頁面控制器

      app.controller(“mainPageCtrl”,function($scope,userService) {

      // 控制器函數操作部分 ,主要進行數據的初始化操作和事件函數的定義

      $scope.title = ‘大史住在大前端’;

      userService.showUserInfo();

      });

      // 編寫自定義服務

      app.service(‘userService’,function(){

      this.showUserInfo = function(){

      Console.log(‘call the method to show user information’);

      }

      })

      示例代碼中先通過module方法定義了一個全局的模塊實例,接著在實例上定義了一個控制器模塊(Controller)和一個服務模塊(Service),$scope對象用于和頁面之間產生關聯,通過模板語法綁定的變量或事件處理函數都需要掛載在頁面的$scope對象上才能夠被訪問,上面這段簡單的代碼在運行時,AngularJS就會將頁面模板上帶有ng-bind=“title”標記的元素內容替換為自定義的內容,并執行userService服務上的showUserInfo方法。

      如果你仔細觀察上面的代碼,很容易就會發現依賴注入的痕跡,Controller在定義時接收了一個字符串key和一個函數,這個函數通過形參userService來接收外部傳入的同名服務,用戶要做的僅僅是使用AngularJS提供的方法來定義對應的模塊,而框架在執行工廠方法來實例化時就會自動找到它依賴的模塊實例并將其注入進來,對于Controller而言,它只需要在工廠函數的形參中聲明自己依賴的模塊就可以了。有了前文中IOC相關知識的鋪墊,我們不難想象,app.controller方法的本質其實就是IOC容器中的bind方法,用于將一個工廠方法登記到注冊表中,它僅僅是依賴收集的過程,app.service方法也是類似的。這種實現方式被稱為“推斷注入”,也就是從傳入的工廠方法形參的名稱中推斷出依賴的模塊并將其注入,函數體的字符串形式可以調用toString方法得到,接著使用正則就可以提取出形參的字符,也就是依賴模塊的名稱。“推斷注入”屬于一種隱式推斷的方式,它要求形參的名稱和模塊注冊時使用的鍵名保持一致,例如前文示例中的userService對應著使用app.service方法所定義的userService服務。這種方式雖然簡潔,但代碼在利用工具進行壓縮混淆時通常會將形參使用的名稱修改為更短的名稱,這時再用形參的名稱去尋找依賴項就會導致錯誤,于是AngularJS又提供了另外兩種依賴注入的實現方式——“內聯聲明”和“聲明注入”,它們的基本語法如下所示:

      // 內聯注入

      app.controller(“mainPageCtrl”,[‘$scope’, ’userService’, function($scope,userService) {

      // 控制器函數操作部分 ,主要進行數據的初始化操作和事件函數的定義

      $scope.title = ‘大史住在大前端’;

      userService.showUserInfo();

      }]);

      // 聲明注入

      var mainPageCtrl = ?function($scope,userService) {

      // 控制器函數操作部分 ,主要進行數據的初始化操作和事件函數的定義

      $scope.title = ‘大史住在大前端’;

      userService.showUserInfo();

      };

      mainPageCtrl.$inject = [‘$scope’,’userService’];

      app.controller(“mainPageCtrl”, mainPageCtrl);

      內聯注入是在原本傳入工廠方法的位置傳入一個數組,默認數組的最后一項為工廠方法,而前置項是依賴模塊的鍵名,字符串常量并不像函數定義那樣會被壓縮混淆工具影響,這樣AngularJS的依賴注入系統就能夠找到需要的模塊了;聲明注入的目的也是一樣的,只不過它將依賴列表掛載在工廠函數的$inject屬性上而已(JavaScript中的函數本質上也是對象類型,可以添加屬性),在程序的實現上想要兼容上述的幾種不同的依賴聲明方式并不困難,只需要判斷app.controller方法接收到的第二個參數是數組還是函數,如果是函數的話是否有$inject屬性,然后將依賴數組提取出來并遍歷加載模塊就可以了。

      AngularJS的依賴注入模塊源代碼可以在官方代碼倉的src/auto/injector.js中找到,從文件夾的命名就可以看到它是用來實現自動化依賴注入的,其中包含大量官方文檔的注釋,會對閱讀理解源代碼的思路有很大幫助,你可以在其中找到annotate方法的定義,就可以看到AngularJS中對于上述幾種不同的依賴聲明方式的兼容處理,感興趣的讀者可以自行完成其他部分的學習。

      AOP和裝飾器

      “面向切面編程”并不是什么顛覆性的技術,它帶來的是一種新的代碼組織思路。假設你在系統中使用著名的axios庫來處理網絡請求,后端在用戶登錄成功后返回一個token,需要你每次發送請求時都將它添加在請求頭中以便進行鑒權,常規的思路是編寫一個通用的getToken方法,然后在每次發請求時通過自定義headers將其傳入(假設自定義頭字段為X-Token):

      import { getToken } from ‘./utils’;

      axios.get(‘/api/report/get_list’,{

      headers:{

      ‘X-Token’:getToken()

      }

      });

      從功能實現角度而言,上面的做法是可行的,但我們不得不在每個需要發送請求的模塊中引用公共方法getToken,這樣顯得非常繁瑣,畢竟在不同的請求中添加token信息的動作都是一樣的;相比之下,axios提供的interceptors-機制就非常適合用來處理類似的場景,它就是非常典型的“面向切面”實踐:

      axios.interceptors.request.use(function (config) {

      // 在config配置中添加自定義信息

      config.headers = {

      ...config.headers,

      ‘X-Token’:getToken()

      }

      return config;

      }, function (error) {

      // 請求發生錯誤時的處理函數

      return Promise.reject(error);

      });

      如果你了解過express和koa框架中所使用的中間件模型,就很容易意識到這里的-機制本質上和它們是一樣的,用戶自定義的處理函數被依次添加進-數組,在請求發送前或者響應返回后的特定“時間切面”上依次執行,這樣一來,每個具體的請求就不需要再自行處理向請求頭中添加Token之類的非業務邏輯了,功能層面的代碼就這樣被剝離并隱藏起來,業務邏輯的代碼自然就變得更加簡潔。

      除了利用編程技巧,高級語言也提供了更加簡潔的語法來方便開發者實踐“面向切面編程”,JavaScript從ES7標準開始支持裝飾器語法,但由于當前前端工程中有Babel編譯工具的存在,所以對于開發者而言并不需要考慮瀏覽器對新語法支持度的問題,如果使用Typescript,開發者就可以通過配置tsconfig.json中的參數來啟用裝飾器(在Spring框架中被稱為annotation,也就是注解)語法來實現相關的邏輯,它的本質只是一種語法糖。常見的裝飾器包括類裝飾器、方法裝飾器、屬性裝飾器、參數裝飾器,類定義中幾乎所有的部分都可以被裝飾器包裝。以類裝飾器為例,它接收的參數是需要被修飾的類,下面的示例中使用@testable修飾符在已經定義的類的原型對象上增加一個名為_testable的屬性:

      function testable(target){

      target.prototype._testable = false;

      }

      // 在類名上一行編寫裝飾器

      @testable

      Class Person{

      constructor(name){

      this.name = name;

      }

      }

      從上面的代碼中你會發現,即使沒有裝飾器語法,我們自己在JavaScript中執行testable函數也可以完成對類的擴展,它們的區別在于手動執行包裝的語句是命令式風格的,而裝飾器語法是聲明式風格的,后者通常被認為更適合在面向對象編程中使用,因為它可以保持業務邏輯層代碼的簡潔,而把無關緊要的細節移交給專門的模塊去處理。Angular中提供的裝飾器通常都可以接收參數,我們只需要借助高階函數來實現一個“裝飾器工廠”,返回一個裝飾器生成函數就可以了:

      // Angular中的組件定義

      @Component({

      selector: ‘hero-detail’,

      templateUrl: ‘hero-detail.html’,

      styleUrls: [‘style.css’]

      })

      Class MyComponent{

      //......

      }

      //@Component裝飾器的定義大致符合如下的形式

      function Component(params){

      return function(target){

      // target可以訪問到params中的內容

      target.prototype._meta = params;

      }

      }

      這樣組件在被實例化時,就可以獲得從裝飾器工廠傳入的配置信息,這些配置信息通常也被稱為類的元信息。其他類型裝飾器的基本工作原理也是一樣的,只是函數簽名中的參數不同,例如方法裝飾器被調用時會傳入3個參數:

      第1個參數裝飾靜態方法時為構造函數,裝飾類方法時為類的原型對象

      第2個參數是成員名

      第3個參數是成員屬性描述符

      你可能一下子就發現了,它和JavaScript中的Object.defineProperty的函數簽名是一樣的,這也意味著方法裝飾器和它一樣屬于抽象度較高但通用性更強的方法。在方法裝飾器的函數體中,我們可以從構造函數或原型對象上獲取到需要被裝飾的方法,接著用代理模式生成一個帶有附加功能的新方法,并在恰當的時機執行原方法,最后通過直接賦值或是利用屬性描述符中的getter返回包裝后的新方法,從而完成對原方法功能的擴展,你可以在Vue2源碼中數據劫持的部分學習到類似的應用。下面我們來實現一個方法裝飾器,希望在被裝飾的方法執行前后在控制臺打印出一些調試信息,代碼實現大致如下:

      function log(target, key, descriptor){

      const originMethod = target[key];

      const decoratedMethod = ()=>{

      console.log(‘方法執行前’);

      const result = originMethod();

      console.log(‘方法執行后’);

      return result;

      }

      //返回新方法

      target[key] = decoratedMethod;

      }

      你只需要在被裝飾的方法上一行寫上@log來標記就可以了,當然也可以通過工廠方法將日志的內容以參數的形式傳入。其他類型的裝飾器本文中不再贅述,它們的工作方式是相似的,下一節中我們來看看Inversify.js是如何使用裝飾器語法來實現依賴注入的。

      用inversify.js實現依賴注入

      Inversify.js提供了更加完備的依賴注入實現,它是使用Typescript編寫的。

      基本使用

      官方網站已經提供了基本的示例代碼和使用方式,首先是接口定義:

      // file interfaces.ts

      export interface Warrior {

      fight(): string;

      sneak(): string;

      }

      export interface Weapon {

      hit(): string;

      }

      export interface ThrowableWeapon {

      throw(): string;

      }

      上面的代碼中定義并導出了戰士、武器和可投擲武器這三個接口,還記得嗎?依賴注入是“SOLID”設計原則中依賴倒置原則的一種實踐,上層模塊和底層模塊應該依賴于共同的抽象,當不同的類使用implements關鍵字來實現接口或者將某個標識符的類型聲明為接口時,就需要滿足接口聲明的結構限制,于是接口就成為了它們“共同的抽象”,而且Typescript中的接口定義只用于類型約束和校驗,上線前編譯為JavaScript后就消失了。接下來是類型定義:

      // file types.ts

      const TYPES = {

      Warrior: Symbol.for("Warrior"),

      Weapon: Symbol.for("Weapon"),

      ThrowableWeapon: Symbol.for("ThrowableWeapon")

      };

      export { TYPES };

      和接口聲明不同的是,這里的類型定義是一個對象字面量,它編譯后并不會消失,Inversify.js在運行時需要使用它來作為模塊的標識符,當然也支持使用字符串字面量,就像前文中我們自己實現IOC容器時所做的那樣。接下來就是類定義時的聲明環節:

      import { injectable, inject } from "inversify";

      import "reflect-metadata";

      import { Weapon, ThrowableWeapon, Warrior } from "./interfaces";

      import { TYPES } from "./types";

      @injectable()

      class Katana implements Weapon {

      public hit() {

      return "cut!";

      }

      }

      @injectable()

      class Shuriken implements ThrowableWeapon {

      public throw() {

      return "hit!";

      }

      }

      @injectable()

      class Ninja implements Warrior {

      private _katana: Weapon;

      private _shuriken: ThrowableWeapon;

      public constructor(

      @inject(TYPES.Weapon) katana: Weapon,

      @inject(TYPES.ThrowableWeapon) shuriken: ThrowableWeapon

      ) {

      this._katana = katana;

      this._shuriken = shuriken;

      }

      public fight() { return this._katana.hit(); }

      public sneak() { return this._shuriken.throw(); }

      }

      export { Ninja, Katana, Shuriken };

      可以看到最核心的兩個API是從inversify中引入的injectable和inject這兩個裝飾器,這也是在大多數依賴注入框架中使用的術語,injectable是可注入的意思,也就是告知依賴注入框架這個類需要被注冊到容器中,inject是注入的意思,它是一個裝飾器工廠,接受的參數就是前文在types中定義的類型名,如果你覺得這里難以理解,可以將它直接當做字符串來對待,其作用也就是告知框架在為這個變量注入依賴時需要按照哪個key去查找對應的模塊,如果將這種語法和AngularJS中的依賴注入進行比較就會發現,它已經不需要開發者手動來維護依賴數組了。最后需要處理的,就是容器配置的部分:

      // file inversify.config.ts

      import { Container } from "inversify";

      import { TYPES } from "./types";

      import { Warrior, Weapon, ThrowableWeapon } from "./interfaces";

      import { Ninja, Katana, Shuriken } from "./entities";

      const myContainer = new Container();

      myContainer.bind(TYPES.Warrior).to(Ninja);

      myContainer.bind(TYPES.Weapon).to(Katana);

      myContainer.bind(TYPES.ThrowableWeapon).to(Shuriken);

      export { myContainer };

      不要受到Typescript復雜性的干擾,這里和前文中自己實現的IOC容器類的使用方式是一樣的,只不過我們使用的API是ioc.bind(key, value),而這里的實現是ioc.bind(key).to(value),最后就可以來使用這個IOC容器實例了:

      import { myContainer } from "./inversify.config";

      import { TYPES } from "./types";

      import { Warrior } from "./interfaces";

      const ninja = myContainer.get(TYPES.Warrior);

      expect(ninja.fight()).eql("cut!"); // true

      expect(ninja.sneak()).eql("hit!"); // true

      inversify.js提供了get方法來從容器中獲取指定的類,這樣就可以在代碼中使用Container實例來管理項目中的類了,示例代碼可以在本章的代碼倉庫中找到。

      源碼淺析

      本節中我們深入源碼層面來進行一些探索,很多讀者一提到源碼就會望而卻步,但Inversify.js代碼層面的實現可能比你想象的要簡單很多,但想要弄清楚背后的思路和框架的結構,還是需要花費不少時間和精力的。首先是injectable裝飾器的定義:

      import * as ERRORS_MSGS from "../constants/error_msgs";

      import * as METADATA_KEY from "../constants/metadata_keys";

      function injectable() {

      return function (target) {

      if (Reflect.hasOwnMetadata(METADATA_KEY.PARAM_TYPES, target)) {

      throw new Error(ERRORS_MSGS.DUPLICATED_INJECTABLE_DECORATOR);

      }

      var types = Reflect.getMetadata(METADATA_KEY.DESIGN_PARAM_TYPES, target) || [];

      Reflect.defineMetadata(METADATA_KEY.PARAM_TYPES, types, target);

      return target;

      };

      }

      export { injectable };

      Reflect對象是ES6標準中定義的全局對象,用于為原本掛載在Object.prototype對象上的API提供函數化的實現,Reflect.defineMetadata方法并不是標準的API,而是由引入的reflect-metadata庫提供的擴展能力,metadata也被稱為“元信息”,通常是指需要隱藏在程序內部的與業務邏輯無關的附加信息。如果我們自己來實現,很大概率會將一個名為_metadata的屬性直接掛載在對象上,但是在reflect-metadata的幫助下,元信息的鍵值對與實體對象或對象屬性之間以映射的形式存在,從而避免了對目標對象的污染,其用法如下:

      // 為類添加元信息

      Reflect.defineMetadata(metadataKey, metadataValue, target);

      // 為類的屬性添加元信息

      Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);

      injectable源碼中引入的METADATA_KEY對象實際上只是一些字符串而已。當你把上面代碼中的常量標識符都替換為對應的字符串后就非常容易理解了:

      function injectable() {

      return function (target) {

      if (Reflect.hasOwnMetadata(‘inversify:paramtypes’, target)) {

      throw new Error(/*...*/);

      }

      var types = Reflect.getMetadata(‘design:paramtypes’, target) || [];

      Reflect.defineMetadata(‘inversify:paramtypes’, types, target);

      return target;

      };

      }

      可以看到injectable裝飾器所做的事情就是把與target對應的key為“design:paramtypes”的元信息賦值給了key為“inversify:paramtypes”的元信息。再來看看inject裝飾器工廠的源碼:

      function inject(serviceIdentifier) {

      return function (target, targetKey, index) {

      if (serviceIdentifier === undefined) {

      throw new Error(UNDEFINED_INJECT_ANNOTATION(target.name));

      一統江湖的大前端(10)——inversify.js控制反轉

      }

      var metadata = new Metadata(METADATA_KEY.INJECT_TAG, serviceIdentifier);

      if (typeof index === "number") {

      tagParameter(target, targetKey, index, metadata);

      }

      else {

      tagProperty(target, targetKey, metadata);

      }

      };

      }

      export { inject };

      inject是一個裝飾器工廠,這里的邏輯就是根據傳入的標識符(也就是前文中定義的types),實例化一個元信息對象,然后根據形參的類型來調用不同的處理函數,當裝飾器作為參數裝飾器時,第三個參數index是該參數在函數形參中的順序索引,是數字類型的,否則將認為該裝飾器是作為屬性裝飾器使用的,tagParameter和tagProperty底層調用的是同一個函數,其核心邏輯是在進行了大量的容錯檢查后,將新的元信息添加到正確的數組中保存起來。事實上無論是injectable還是inject,它們作為裝飾器所承擔的任務都是對于元信息的保存,IOC的實例管理能力都是依賴于容器類Container來實現的。

      Inversify.js中的Container類將實例化的過程分解為多個自定義的階段,并增加了多容器管理、多值注入、自定義中間件等諸多擴展機制,源代碼本身閱讀起來并不困難,但理論化相對較強且英文的術語較多,對于初中級開發者的實用價值非常有限,所以筆者不打算在本文中詳細展開分析Container類的實現,社區也有很多非常詳細的源碼結構分析的文章,足以幫助感興趣的同學繼續深入了解。

      停下來

      如果你第一次接觸依賴注入相關的知識,可能也會和筆者當初一樣,覺得這樣的理論和寫法非常“高級”,迫不及待地想要深入了解,事實上即使花費很多時間去瀏覽源碼,我在實際工作中也幾乎從來沒有使用過它,但“解耦”的意識卻留在了我的意識里。作為軟件工程師,我們需要去了解技術背后的原理和思想,以便擴展自己的思維,但對技術的敬畏之心不應該演變成對高級技術的盲目崇拜。“依賴注入”不過是設計模式的一種,模式總會有它適合或不適合的使用場景,常用的設計模式還有很多,經典的設計思想也有很多,只有靈活運用才能讓自己在代碼結構組織的工作上游刃有余,請不要讓執念限制了自己思維的廣度。

      ----

      大前端團隊邀請各路高手前來玩耍,團隊和諧有愛,技術硬核,字節范兒正,覆蓋前端各個方向技術棧,總有位置適合你,Base北京,社招實習都有HC,不要猶豫,有意者歡迎私信我。

      JavaScript web前端

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:怎么在wps表格中繪圖
      下一篇:word表格如何導入excel2013(word表格如何導入cdr)
      相關文章
      色窝窝亚洲AV网在线观看| 中文字幕亚洲专区| 色偷偷尼玛图亚洲综合| 亚洲av乱码一区二区三区| 精品亚洲成AV人在线观看| 香蕉视频在线观看亚洲| 亚洲五月综合缴情在线观看| 黑人大战亚洲人精品一区| 久久久久国产成人精品亚洲午夜 | 日产亚洲一区二区三区| 久久久久亚洲AV成人无码| 亚洲av无码一区二区三区网站 | 亚洲av无码不卡一区二区三区| 久久精品国产亚洲沈樵| 亚洲精品乱码久久久久久自慰| 国产亚洲综合久久系列| 亚洲国产另类久久久精品| 国产亚洲精品自在久久| 亚洲av永久无码精品秋霞电影影院 | 亚洲爽爽一区二区三区| 国产日韩成人亚洲丁香婷婷| 中文字幕无码精品亚洲资源网| 国产L精品国产亚洲区久久| 区久久AAA片69亚洲| 国产AV无码专区亚洲精品| 亚洲AV美女一区二区三区| 精品亚洲成AV人在线观看| 亚洲av乱码一区二区三区香蕉| 中文字幕乱码亚洲无线三区 | 天堂亚洲国产中文在线| 亚洲日韩国产欧美一区二区三区 | 精品久久久久久亚洲综合网| 亚洲AV无码之日韩精品| 亚洲一区二区三区偷拍女厕 | 日韩亚洲翔田千里在线| 亚洲精品无码成人片在线观看| 亚洲综合色自拍一区| 亚洲视频在线观看一区| 亚洲乱码一二三四五六区| 亚洲中文字幕无码爆乳app| 国产综合激情在线亚洲第一页 |