<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>

          Nestjs模塊機制的概念和實現(xiàn)原理

          共 8039字,需瀏覽 17分鐘

           ·

          2022-04-11 22:13


          原文鏈接:?https://mp.weixin.qq.com/s/fQVPuoB7Lk88a_N4OQbPng
          作者:?子慕大詩人

          1 前言

          Nest 提供了模塊機制,通過在模塊裝飾器中定義提供者、導入、導出和提供者構造函數(shù)便完成了依賴注入,通過模塊樹組織整個應用程序的開發(fā)。按照框架本身的約定直接擼一個應用程序,是完全沒有問題的??墒?,于我而言對于框架宣稱的依賴注入、控制反轉、模塊、提供者、元數(shù)據(jù)、相關裝飾器等等,覺得缺乏一個更清晰系統(tǒng)的認識。

          • 為什么需要控制反轉?

          • 什么是依賴注入?

          • 裝飾器做了啥?

          • 模塊 (@Module) 中的提供者(providers),導入(imports)、導出(exports)是什么實現(xiàn)原理?

          好像能夠理解,能夠意會,但是讓我自己從頭說清楚,我說不清楚。于是進行了一番探索,便有了這篇文章。從現(xiàn)在起,我們從新出發(fā),進入正文。

          2 兩個階段

          2.1 Express、Koa

          一個語言和其技術社區(qū)的發(fā)展過程,一定是從底層功能逐漸往上豐富發(fā)展的,就像是樹根慢慢生長為樹枝再長滿樹葉的過程。在較早,Nodejs 出現(xiàn)了 Express 和 Koa 這樣的基本 Web 服務框架。能夠提供一個非?;A的服務能力。基于這樣的框架,大量的中間件、插件開始在社區(qū)誕生,為框架提供更加豐富的服務。我們需要自己去組織應用依賴,搭建應用腳手架,靈活又繁瑣,也具有一定工作量。

          發(fā)展到后面,一些生產更高效、規(guī)則更統(tǒng)一的框架便誕生了,開啟了一個更新的階段。

          2.2 EggJs、Nestjs

          為了更加適應快速生產應用,統(tǒng)一規(guī)范,開箱即用,便發(fā)展出了 EggJs、NestJs、Midway等框架。此類框架,通過實現(xiàn)底層生命周期,將一個應用的實現(xiàn)抽象為一個通用可擴展的過程,我們只需要按照框架提供的配置方式,便可以更簡單的實現(xiàn)應用程序??蚣軐崿F(xiàn)了程序的過程控制,而我們只需要在合適位置組裝我們的零件就行,這看起來更像是流水線工作,每個流程被分割的很清楚,也省去了很多實現(xiàn)成本。

          2.3 小結

          上面的兩個階段只是一個鋪墊,我們可以大致了解到,框架的升級是提高了生產效率,而要實現(xiàn)框架的升級,就會引入一些設計思路和模式,Nest 中就出現(xiàn)了控制反轉、依賴注入、元編程的概念,下面我們來聊聊。

          3 控制反轉和依賴注入

          3.1 依賴注入

          一個應用程序實際就是非常多的抽象類,通過互相調用實現(xiàn)應用的所有功能。隨著應用代碼和功能復雜度的增加,項目一定會越來越難以維護,因為類越來越多,相互之間的關系越來越復雜。

          舉個例子,假如我們使用 Koa 開發(fā)我們的應用,Koa 本身主要實現(xiàn)了一套基礎的 Web 服務能力,我們在實現(xiàn)應用的過程中,會定義很多類,這些類的實例化方式、相互依賴關系,都會由我們在代碼邏輯自由組織和控制。每個類的實例化都是由我們手動 new,并且我們可以控制某個類是只實例化一次然后被共享,還是每次都實例化。下面的 B 類依賴 A,每次實例化 B 的時候,A 都會被實例化一次,所以對于每個實例 B 來說,A 是不被共享的實例。

          class A{}
          // B
          class B{
          contructor(){
          this.a = new A();
          }
          }

          下面的 C 是獲取的外部實例,所以多個 C 實例是共享的 app.a 這個實例。

          class A{}
          // C
          const app = {};
          app.a = new A();
          class C{
          contructor(){
          this.a = app.a;
          }
          }

          下面的 D 是通過構造函數(shù)參數(shù)傳入,可以每次傳入一個非共享實例,也可以傳入共享的 app.a 這個實例(D 和 F 共享 app.a),并且由于現(xiàn)在是參數(shù)的方式傳入,我也可以傳入一個 X 類實例。

          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,通過傳值的方式注入到 B 中。通過構造函數(shù)注入(傳值)只是一種實現(xiàn)方式,也可以通過實現(xiàn) set 方法調用傳入,或者是其他任何方式,只要能把外部的一個依賴,傳入到內部就行。其實就這么簡單。

          class A{}
          // D
          class D{
          setDep(a){
          this.a = a;
          }
          }
          const d = new D()
          d.setDep(new A())

          3.2 All in 依賴注入?

          隨著迭代進行,出現(xiàn)了 B 根據(jù)不同的前置條件依賴會發(fā)生變化。比如,前置條件一?this.a?需要傳入 A 的實例,前置條件二this.a需要傳入 X 的實例。這個時候,我們就會開始做實際的抽象了。我們就會改造成上面 D 這樣依賴注入的方式。

          初期,我們在實現(xiàn)應用的時候,在滿足當時需求的情況下,就會實現(xiàn)出 B 和 C 類的寫法,這本身也沒有什么問題,項目迭代了幾年之后,都不一定會動這部分代碼。我們要是去考慮后期擴展什么的,是會影響開發(fā)效率的,而且不一定派的上用場。所以大部分時候,我們都是遇到需要抽象的場景,再對部分代碼做抽象改造。

          // 改造前
          class B{
          contructor(){
          this.a = new A();
          }
          }
          new B()

          // 改造后
          class D{
          contructor(a){
          this.a = a;
          }
          }
          new D(new A())
          new D(new X())

          按照目前的開發(fā)模式,CBD三種類都會存在,B 和 C有一定的幾率發(fā)展成為 D,每次升級 D 的抽象過程,我們會需要重構代碼,這是一種實現(xiàn)成本。

          這里舉這個例子是想說明,在一個沒有任何約束或者規(guī)定的開發(fā)模式下。我們是可以自由的寫代碼來達到各種類與類之間依賴控制。在一個完全開放的環(huán)境里,是非常自由的,這是一個刀耕火種的原始時代。由于沒有一個固定的代碼開發(fā)模式,沒有一個最高行動綱領,隨著不同開發(fā)人員的介入或者說同一個開發(fā)者不同時間段寫代碼的差別,代碼在增長的過程中,依賴關系會變得非常不清晰,該共享的實例可能被多次實例化,浪費內存。從代碼中,很難看清楚一個完整的依賴關系結構,代碼可能會變得非常難以維護。

          那我們每定義一個類,都按照依賴注入的方式來寫,都寫成 D 這樣的,那 C 和 B 的抽象過程就被提前了,這樣后期擴展也比較方便,減少了改造成本。所以把這叫All in 依賴注入,也就是我們所有依賴都通過依賴注入的方式實現(xiàn)。

          可這樣前期的實現(xiàn)成本又變高了,很難在團隊協(xié)作中達到統(tǒng)一并且堅持下去,最終可能會落地失敗,這也可以被定義為是一種過度設計,因為額外的實現(xiàn)成本,不一定能帶來收益。

          3.3 控制反轉

          既然已經約定好了統(tǒng)一使用依賴注入的方式,那是否可以通過框架的底層封裝,實現(xiàn)一個底層控制器,約定一個依賴配置規(guī)則,控制器根據(jù)我們定義的依賴配置來控制實例化過程和依賴共享,幫助我們實現(xiàn)類管理。這樣的設計模式就叫控制反轉。

          控制反轉可能第一次聽說的時候會很難理解,控制指的什么?反轉了啥?

          猜測是由于開發(fā)者一開始就用此類框架,并沒有體驗過上個“Express、Koa時代”,缺乏舊社會毒打。加上這反轉的用詞,在程序中顯得非常的抽象,難以望文生義。

          前文我們說的實現(xiàn) Koa 應用,所有的類完全由我們自由控制的,所以可以看作是一個常規(guī)的程序控制方式,那就叫它:控制正轉。而我們使用 Nest,它底層實現(xiàn)一套控制器,我們只需要在實際開發(fā)過程中,按照約定寫配置代碼,框架程序就會幫我們管理類的依賴注入,所以就把它叫作:控制反轉。

          本質就是把程序的實現(xiàn)過程交給框架程序去統(tǒng)一管理,控制權從開發(fā)者,交給了框架程序。

          控制正轉:開發(fā)者純手動控制程序

          控制反轉:框架程序控制

          舉個現(xiàn)實的例子,一個人本來是自己開車去上班的,他的目的就是到達公司。它自己開車,自己控制路線。而如果交出開車的控制權,就是去趕公交,他只需要選擇一個對應的班車就可以到達公司了。單從控制來說,人就是被解放出來了,只需要記住坐那趟公交就行了,犯錯的幾率也小了,人也輕松了不少。公交系統(tǒng)就是控制器,公交線路就是約定配置。

          通過如上的實際對比,我想應該有點能理解控制反轉了。

          3.4 小結

          從 Koa 到 Nest,從前端的 JQuery 到 Vue React。其實都是一步步通過框架封裝,去解決上個時代低效率的問題。

          上面的 Koa 應用開發(fā),通過非常原始的方式去控制依賴和實例化,就類似于前端中的 JQuery 操作 dom ,這種很原始的方式就把它叫控制正轉,而 Vue React 就好似 Nest 提供了一層程序控制器,他們可以都叫控制反轉。這也是個人理解,如果有問題期望大神指出。

          下面再來說說 Nest 中的模塊 @Module,依賴注入、控制反轉需要它作為媒介。

          4 Nestjs的模塊(@Module)

          Nestjs實現(xiàn)了控制反轉,約定配置模塊(@module)的 imports、exports、providers 管理提供者也就是類的依賴注入。

          providers 可以理解是在當前模塊注冊和實例化類,下面的 A 和 B 就在當前模塊被實例化,如果B在構造函數(shù)中引用 A,就是引用的當前 ModuleD 的 A 實例。

          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?就是把當前模塊中的?providers?中實例化的類,作為可被外部模塊共享的類。比如現(xiàn)在 ModuleF 的 C 類實例化的時候,想直接注入 ModuleD 的 A 類實例。就在 ModuleD 中設置導出(exports)A,在 ModuleF 中通過?imports?導入 ModuleD。

          按照下面的寫法,控制反轉程序會自動掃描依賴,首先看自己模塊的 providers 中,有沒有提供者 A,如果沒有就去尋找導入的 ModuleD 中是否有 A 實例,發(fā)現(xiàn)存在,就取得 ModuleD 的 A 實例注入到 C 實例之中。

          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;
          }
          }

          因此想要讓外部模塊使用當前模塊的類實例,必須先在當前模塊的providers里定義實例化類,再定義導出這個類,否則就會報錯。

          //正確
          @Module({
          providers: [A],
          exports: [A]
          })
          //錯誤
          @Module({
          providers: [],
          exports: [A]
          })

          這里還是提一嘴ts的知識點

          export class C {
          constructor(private a: A) {
          }
          }

          由于 TypeScript 支持 constructor 參數(shù)(private、protected、public、readonly)隱式自動定義為 class 屬性 (Parameter Property),因此無需使用?this.a = a。Nest 中都是這樣的寫法。

          5 Nest 元編程

          元編程的概念在 Nest 框架中得到了體現(xiàn),它其中的控制反轉、裝飾器,就是元編程的實現(xiàn)。大概可以理解為,元編程本質還是編程,只是中間多了一些抽象的程序,這個抽象程序能夠識別元數(shù)據(jù)(如@Module中的對象數(shù)據(jù)),其實就是一種擴展能力,能夠將其他程序作為數(shù)據(jù)來處理。我們在編寫這樣的抽象程序,就是在元編程了。

          5.1 元數(shù)據(jù)

          Nest 文檔中也常提到了元數(shù)據(jù),元數(shù)據(jù)這個概念第一次看到的話,也會比較費解,需要隨著接觸時間增長習慣成理解,可以不用太過糾結。

          元數(shù)據(jù)的定義是:描述數(shù)據(jù)的數(shù)據(jù),主要是描述數(shù)據(jù)屬性的信息,也可以理解為描述程序的數(shù)據(jù)。

          Nest 中 @Module 配置的exports、providers、imports、controllers都是元數(shù)據(jù),因為它是用來描述程序關系的數(shù)據(jù),這個數(shù)據(jù)信息不是展示給終端用戶的實際數(shù)據(jù),而是給框架程序讀取識別的。

          5.2 Nest 裝飾器

          如果看看 Nest 中的裝飾器源碼,會發(fā)現(xiàn),幾乎每一個裝飾器本身只是通過 reflect-metadata 定義了一個元數(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ù)組里傳入了類,在程序實際運行時providers里的類,會被框架程序自動實例化變?yōu)樘峁┱?,不需要開發(fā)者顯示的去執(zhí)行實例化和依賴注入。類只有在模塊中實例化了之后才變成了提供者。providers中的類被反射了成了提供者,控制反轉就是利用的反射技術。

          換個例子的話,就是數(shù)據(jù)庫中的?ORM(對象關系映射),使用 ORM 只需要定義表字段,ORM 庫會自動把對象數(shù)據(jù)轉換為 SQL 語句。

          const data = TableModel.build();

          data.time = 1;
          data.browser = 'chrome';

          data.save();
          // SQL: INSERT INTO tableName (time,browser) [{"time":1,"browser":"chrome"}]

          ORM 庫就是利用了反射技術,讓使用者只需要關注字段數(shù)據(jù)本身,對象被 ORM 庫反射成為了 SQL 執(zhí)行語句,開發(fā)者只需要關注數(shù)據(jù)字段,而不需要去寫 SQL 了。

          5.3 reflect-metadata

          reflect-metadata 是一個反射庫,Nest 用它來管理元數(shù)據(jù)。reflect-metadata 使用 WeakMap,創(chuàng)建一個全局單實例,通過 set 和 get 方法設置和獲取被裝飾對象(類、方法等)的元數(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ù)存在了全局單例對象中,進行統(tǒng)一管理。reflect-metadata 并不是實現(xiàn)具體的反射,而是提供了一個輔助反射實現(xiàn)的工具庫。

          6 最后

          現(xiàn)在再來看看前面的幾個疑問。

          1. 為什么需要控制反轉?

          2. 什么是依賴注入?

          3. 裝飾器做了啥?

          4. 模塊 (@Module) 中的提供者(providers),導入(imports)、導出(exports)是什么實現(xiàn)原理?

          1 和 2 我想前面已經說清楚了,如果還有點模糊,建議再回去看一遍并查閱一些其它文章資料,通過不同作者的思維來幫助理解知識。

          6.1 問題 [3 4] 總述:

          Nest 利用反射技術、實現(xiàn)了控制反轉,提供了元編程能力,開發(fā)者使用 @Module 裝飾器修飾類并定義元數(shù)據(jù)(providers\imports\exports),元數(shù)據(jù)被存儲在全局對象中(使用 reflect-metadata 庫)。程序運行后,Nest 框架內部的控制程序讀取和注冊模塊樹,掃描元數(shù)據(jù)并實例化類,使其成為提供者,并根據(jù)模塊元數(shù)據(jù)中的 providers\imports\exports 定義,在所有模塊的提供者中尋找當前類的其它依賴類的實例(提供者),找到后通過構造函數(shù)注入。

          本文概念較多,也并沒有做太詳細的解析,概念需要時間慢慢理解,如果一時理解不透徹,也不必太過著急。好吧,就到這里,這篇文章還是花費不少精力,喜歡的朋友期望你能一鍵三連~

          Node 社群



          我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關的交流、學習、共建。下方加 考拉 好友回復「Node」即可。



          如果你覺得這篇內容對你有幫助,我想請你幫我2個小忙:

          1. 點個「在看」,讓更多人也能看到這篇文章
          2. 訂閱官方博客?www.inode.club?讓我們一起成長

          點贊和在看就是最大的支持??

          瀏覽 38
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  99无码视频。 | 日韩福利一区二区 | 五月天淫香淫色 | 日韩精品毛片免费视频 | 日本岛国红桃A |