如何快速入門VUE3.0:進入學習
Nest 提供了模組機制,透過在模組裝飾器中定義提供者、匯入、匯出和提供者建構函式便完成了依賴注入,透過模組樹組織整個應用程式的開發。依照框架本身的約定直接擼一個應用程序,是完全沒有問題的。可是,於我而言對於框架宣稱的依賴注入、控制反轉、模組、提供者、元資料、相關裝飾器等等,覺得缺乏一個更清晰系統的認識。
- 為什麼需要控制反轉?
- 什麼是依賴注入?
- 裝飾器做了啥?
- 模組(@Module) 中的提供者(providers),導入(imports)、導出(exports)是什麼實作原理?
好像能夠理解,能夠意會,但是讓我自己從頭說清楚,我說不清楚。於是進行了一番探索,便有了這篇文章。從現在起,我們從新出發,進入正文。
1.1 Express、Koa
一個語言和其技術社群的發展過程,一定是從底層功能逐漸往上豐富發展的,就像是樹根慢慢生長為樹枝再長滿樹葉的過程。在較早,Nodejs 出現了Express 和Koa 這樣的基本Web 服務框架。能夠提供一個非常基礎的服務能力。基於這樣的框架,大量的中間件、插件開始在社區誕生,為框架提供更豐富的服務。我們需要自己去組織應用依賴,搭建應用腳手架,靈活又繁瑣,也有一定工作量。
發展到後面,一些生產更有效率、規則更統一的框架便誕生了,開啟了一個更新的階段。
1.2 EggJs、Nestjs
為了更適應快速生產應用,統一規範,開箱即用,便發展出了EggJs、NestJs、Midway等框架。此類框架,透過實現底層生命週期,將一個應用的實作抽象化為一個通用可擴展的過程,我們只需要按照框架提供的配置方式,便可以更簡單的實作應用程式。框架實現了程序的過程控制,而我們只需要在合適位置組裝我們的零件就行,這看起來更像是流水線工作,每個流程被分割的很清楚,也省去了很多實現成本。
1.3 小結
上面的兩個階段只是一個鋪墊,我們可以大致了解到,框架的升級是提高了生產效率,而要實現框架的升級,就會引入一些設計思路和模式,Nest 中就出現了控制反轉、依賴注入、元程式設計的概念,下面我們來聊聊。
2.1 依賴注入
一個應用程式實際上是非常多的抽象類,透過互相呼叫實現應用的所有功能。隨著應用程式碼和功能複雜度的增加,專案一定會越來越難以維護,因為類別越來越多,相互之間的關係也越來越複雜。
舉個例子,假如我們使用Koa 開發我們的應用,Koa 本身主要實現了一套基礎的Web 服務能力,我們在實現應用的過程中,會定義很多類,這些類的實例化方式、相互依賴關係,都會由我們在程式碼邏輯自由組織和控制。每個類別的實例化都是由我們手動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 是透過建構子參數傳入,可以每次傳入一個非共享實例,也可以傳入共享的app.a 這個實例(D 和F 共享app.a),並且由於現在是參數的方式傳入,我也可以傳入一個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 中。透過建構函式註入(傳值)只是一種實作方式,也可以透過實作set 方法呼叫傳入,或是其他任何方式,只要能把外部的一個依賴,傳入到內部就行。其實就這麼簡單。
class A{} // D class D{ setDep(a){ this.a = a; } } const d = new D() d.setDep(new A())
2.2 All in 依賴注入?
隨著迭代進行,出現了B 根據不同的前置條件依賴會改變。例如,前置條件一this.a
需要傳入A 的實例,前置條件二this.a
需要傳入X 的實例。這時候,我們就會開始做實際的抽象了。我們就會改造成上面D 這樣依賴注入的方式。
初期,我們在實現應用的時候,在滿足當時需求的情況下,就會實現出B 和C 類的寫法,這本身也沒有什麼問題,專案迭代了幾年之後,都不一定會動這部分程式碼。我們要是去考慮後期擴充什麼的,是會影響開發效率的,而且不一定派的上用場。所以大部分時候,我們都是遇到需要抽象的場景,再對部分程式碼做抽象改造。
// 改造前class B{ contructor(){ this.a = new A(); } } new B() // 改造後class D{ contructor(a){ this.a = a; } } new D(new A()) new D(new X())
依照目前的開發模式,CBD三種類都會存在,B 和C有一定的幾率發展成D,每次升級D 的抽象過程,我們會需要重構程式碼,這是一種實現成本。
這裡舉這個例子是想說明,在一個沒有任何約束或規定的開發模式下。我們是可以自由的寫程式碼來達到各種類別與類別之間依賴控制。在一個完全開放的環境裡,是非常自由的,這是一個刀耕火種的原始時代。由於沒有一個固定的程式碼開發模式,沒有一個最高行動綱領,隨著不同開發人員的介入或者說同一個開發者不同時間段寫程式碼的差別,程式碼在增長的過程中,依賴關係會變得非常不清晰,該共享的實例可能被多次實例化,浪費記憶體。從程式碼中,很難看清楚一個完整的依賴關係結構,程式碼可能會變得非常難以維護。
那我們每定義一個類,都按照依賴注入的方式來寫,都寫成D 這樣的,那C 和B 的抽象過程就被提前了,這樣後期擴展也比較方便,減少了改造成本。所以把這叫做All in 依赖注入
,也就是我們所有依賴都透過依賴注入的方式來實現。
但這樣前期的實現成本又變高了,很難在團隊協作中達到統一並且堅持下去,最終可能會落地失敗,這也可以被定義為是一種過度設計,因為額外的實現成本,不一定能帶來收益。
2.3 控制反轉
既然已經約定好了統一使用依賴注入的方式,那是否可以透過框架的底層封裝,實現一個底層控制器,約定一個依賴配置規則,控制器根據我們定義的依賴配置來控制實例化過程和依賴共享,幫助我們實現類別管理。這樣的設計模式就叫控制反轉。
控制反轉可能第一次聽到的時候會很難理解,控制指的什麼?反轉了啥?
猜測是由於開發者一開始就使用此類框架,並沒有體驗過上個“Express、Koa時代”,缺乏舊社會毒打。加上這反轉的用詞,在程序中顯得非常的抽象,難以望文生義。
前文我們說的實作Koa 應用,所有的類別完全由我們自由控制的,所以可以看作是一個常規的程式控制方式,那就叫它:控制正轉。而我們使用Nest,它底層實作一套控制器,我們只需要在實際開發過程中,依照約定寫設定程式碼,框架程式就會幫我們管理類別的依賴注入,所以就把它叫作:控制反轉。
本質就是把程序的實現過程交給框架程序去統一管理,控制權從開發者,交給了框架程序。
控制正轉:開發者純手動控製程序
控制反轉:框架程序控制
舉個現實的例子,一個人本來就是自己開車去上班的,他的目的就是要到達公司。它自己開車,自己控制路線。而如果交出開車的控制權,就是去趕公車,他只需要選擇一個對應的班車就可以到達公司了。單從控制來說,人就是被解放出來了,只要記住坐那趟公車就行了,犯錯的幾率也小了,人也輕鬆了不少。公車系統就是控制器,公車線路就是約定配置。
透過如上的實際對比,我想應該有點能理解控制反轉了。
2.4 小結
從Koa 到Nest,從前端的JQuery 到Vue React。其實都是一步步透過框架封裝,去解決上個時代低效率的問題。
上面的Koa 應用開發,透過非常原始的方式去控制依賴和實例化,就類似於前端中的JQuery 操作dom ,這種很原始的方式就把它叫控制正轉,而Vue React 就好似Nest 提供了一層程式控制器,他們可以都叫控制反轉。這也是個人理解,如果有問題期望大神指出。
下面再來談談Nest 中的模組@Module,依賴注入、控制反轉需要它作為媒介。
Nestjs實現了控制反轉,約定配置模組(@module)的imports、exports、providers 管理提供者也就是類別的依賴注入。
providers 可以理解是在當前模組註冊和實例化類,下面的A 和B 就在當前模組被實例化,如果B在構造函數中引用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
實例化的類,作為可被外部模組共享的類別。例如現在ModuleF 的C 類別實例化的時候,就想直接注入ModuleD 的A 類別實例。就在ModuleD 中設定導出(exports)A,在ModuleF 中透過imports
導入ModuleD。
依照下面的寫法,控制反轉程式會自動掃描依賴,先看自己模組的providers 中,有沒有提供者A,如果沒有就去尋找導入的ModuleD 中是否有A 實例,發現存在,就取得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] })
後期補充
模組查找實例的過程回看了一下,確實有點不清晰。核心點就是providers裡的類別會被實例化,實例化後就是提供者,模組裡只有providers裡的類別會被實例化,而導出和導入只是一個組織關係配置。模組會優先使用自己的提供者,如果沒有,再去找導入的模組是否有對應提供者
這裡還是提一嘴ts的知識點
export class C { constructor(private a: A) { } }
由於TypeScript 支援constructor 參數(private、protected、public、readonly)隱式自動定義為class 屬性(Parameter Property),因此無需使用this.a = a
。 Nest 中都是這樣的寫法。
元編程的概念在Nest 框架中得到了體現,它其中的控制反轉、裝飾器,就是元編程的實現。大概可以理解為,元編程本質還是編程,只是中間多了一些抽象的程序,這個抽象程序能夠識別元數據(如@Module中的對象數據),其實就是一種擴展能力,能夠將其他程序作為數據來處理。我們在寫這樣的抽象程序,就是在元編程了。
4.1 元數據
Nest 文件中也常提到了元數據,元數據這個概念第一次看到的話,也會比較費解,需要隨著接觸時間增長習慣成理解,可以不用太過糾結。
元資料的定義是:描述資料的數據,主要是描述資料屬性的訊息,也可以理解為描述程式的資料。
Nest 中@Module 配置的exports、providers、imports、controllers
都是元數據,因為它是用來描述程式關係的數據,這個數據資訊不是展示給終端用戶的實際數據,而是給框架程式讀取識別的。
4.2 Nest 裝飾器
如果看看Nest 中的裝飾器源碼,會發現,幾乎每一個裝飾器本身只是透過reflect-metadata 定義了一個元資料。
@Injectable裝飾器
export function Injectable(options?: InjectableOptions): ClassDecorator { return (target: object) => { Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target); Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target); }; }
這裡存在反射的概念,反射也比較好理解,拿@Module 裝飾器舉例,定義元數據providers
,只是往providers
數組裡傳入了類,在程序實際運行時providers
裡的類,會被框架程序自動實例化變成提供者,不需要開發者顯示的去執行實例化和依賴注入。類別只有在模組中實例化了之後才變成了提供者。 providers
中的類別被反射了成了提供者,控制反轉就是利用的反射技術。
換個例子的話,就是資料庫中的ORM(物件關係映射) ,使用ORM 只需要定義表格字段,ORM 函式庫會自動把物件資料轉換為SQL 語句。
const data = TableModel.build(); data.time = 1; data.browser = 'chrome'; data.save(); // SQL: INSERT INTO tableName (time,browser) [{"time":1,"browser":"chrome"}]
ORM 函式庫就是利用了反射技術,讓使用者只需要關注字段資料本身,物件被ORM函式庫反射成為了SQL 執行語句,開發者只需要關注資料字段,而不需要去寫SQL 了。
4.3 reflect-metadata
reflect-metadata 是一個反射庫,Nest 用它來管理元資料。 reflect-metadata 使用WeakMap,建立一個全域單一實例,透過set 和get 方法設定和取得被裝飾物件(類別、方法等)的元資料。
// 隨便看看即可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 把被裝飾者的元資料存在了全域單例物件中,進行統一管理。 reflect-metadata 並不是實現具體的反射,而是提供了一個輔助反射實作的工具庫。
現在再來看看前面的幾個問題。
為什麼需要控制反轉?
什麼是依賴注入?
裝飾器做了啥?
模組(@Module) 中的提供者(providers),導入(imports)、導出(exports)是什麼實作原理?
1 和2 我想前面已經說清楚了,如果還有點模糊,建議再回去看一遍並查閱一些其它文章資料,透過不同作者的思維來幫助理解知識。
5.1 問題[3 4] 總述:
Nest 利用反射技術、實現了控制反轉,提供了元編程能力,開發者使用@Module 裝飾器修飾類別並定義元資料(providersimportsexports),元資料被儲存在全域物件中(使用reflect-metadata 庫)。程式運行後,Nest 框架內部的控製程式讀取和註冊模組樹,掃描元資料並實例化類,使其成為提供者,並根據模組元資料中的providersimportsexports 定義,在所有模組的提供者中尋找當前類別的其它依賴類別的實例(提供者),找到後透過建構子注入。
本文概念較多,也沒有做太詳細的解析,概念需要時間慢慢理解,如果一時理解不透徹,也不必太過急。好吧,就到這裡,這篇文章還是花費不少精力,喜歡的朋友期望你能一鍵三連~