首頁  >  文章  >  web前端  >  深入了解Angular中的依賴注入模式(玩法案例)

深入了解Angular中的依賴注入模式(玩法案例)

青灯夜游
青灯夜游轉載
2022-06-24 15:28:432612瀏覽

本篇文章帶大家深入了解Angular中的依賴注入模式,分享依賴注入模式的應用和玩法案例,希望對大家有幫助!

深入了解Angular中的依賴注入模式(玩法案例)

1 注入,一個元件樹狀層級通訊模式& 設計模式

1.1 元件通訊模式

在Angular工程開發中,通常我們使用Input屬性綁定和Output事件綁定進行元件通信,然而Input和Output卻只能在父子元件中傳遞訊息。元件根據呼叫關係形成一棵元件樹,如果只有屬性綁定和事件綁定,那麼兩個非直接關係元件要通信,需要通過各個連接點本身,中間人需要不斷處理和傳遞一些它本身不需要知道的資訊(如圖1左)。而Angular中提供的Injectable的Service,可以在模組、元件或指令等提供,搭配在建構函數的注入,正好能解決這個問題(圖1右)。 【相關教學推薦:《angular教學》】

深入了解Angular中的依賴注入模式(玩法案例)

圖1 元件通訊模式

左圖只透過父子元件傳遞訊息,節點a和節點b進行通訊就需要經過諸多節點;如果節點c想要透過一些配置控制節點b,他們中間的節點也必須設定額外的屬性或事件來透傳對應的資訊。右圖的依賴注入模式節點c可以提供一個供節點a、b通信的服務,節點a直接和節點c提供服務通信,節點b也直接和節點c提供的服務通信,最後通信就被簡化了,中間節點也沒有耦合該部分內容,對上下層組件發生的通訊無明顯的感知。

1.2 使用依賴注入實現控制反轉

依賴注入(DI)並不是Angular特有的,它是實現控制反轉(IOC)設計模式的手段,依賴注入的出現解決手動實例化過度耦合的問題,所有資源不由使用資源的雙方管理,而由不使用資源資源中心或第三方提供,這樣能帶來很多好處。第一,資源集中管理,實現資源的可配置與易管理。第二,降低了使用資源雙方的依賴程度,也就是我們所說的耦合度。

類比現實世界就是,我們去購買商品例如一支鉛筆,我們只需要找個商店購買一支類型為鉛筆的商品,我們不關心這支鉛筆產地是哪裡,木頭和鉛筆芯都是怎麼黏合的,我們只需要它能完成鉛筆的書寫功能即可,我們不會和具體的鉛筆製造商或工廠有聯繫。而對於商店,它就可以自己去合適的管道採購鉛筆,實現資源的可配置。

結合編碼場景,更具體的說,使用者不需要明確建立實例(new操作),就能注入並使用實例,實例的建立由提供者(providers)決定。資源的管理是透過令牌(token),由於不關心提供者,不關心實例的創建,使用方就可以透過一些局部注入的手段(對token進行二次配置),最終實現替換實例,依賴注入模式的應用和切面編程(AOP)相輔相成。

2 Angular中的依賴注入

依賴注入是Angular框架最重要幾個的核心模組之一,Angular不僅提供Service類型的注入,本身元件樹就是一顆注入依賴樹, 函數和值也可以被注入。也就是說在Angular框架中,子元件是可以透過父元件的token(通常是類別名稱),注入父元件實例的。在元件庫開發中有大量案例是透過注入父元件,實現互動和通訊的,包括參數掛載,狀態共享,甚至取得父元件所在節點的DOM等等。

2.1 解析依賴

要使用Angular的注入,首先就要明白它的注入解析的過程。類似node_modules的解析過程,當找不到依賴都有找不到依賴會一直冒泡到父層去找依賴。舊版(v6前)的Angular會將注入解析的過程分成多層模組注入器,多層組件注入器和元素注入器。新版(v9後)簡化為兩層模型,第一個查詢鍊是靜態DOM層級的元素注入器、元件注入器等統稱為元素注入器,另一個查詢鍊是模組注入器。解析的順序和解析失敗後的預設值官方的這個程式碼註解文檔(provider_flag)裡講的比較清楚了。

深入了解Angular中的依賴注入模式(玩法案例)

圖2 兩層注入器尋找依賴過程( 圖片來源)

也就是說元件/指令以及在元件/指令層級提供注入內容會優先在元件視圖中元素裡尋找依賴一直到根元素,如果沒有找到則接著在元素當前所在模組,引用(包含模組引用和路由懶加載引用)該模組的父級模組一次往上找直到根模組和平台模組。

注意這裡注入器是有繼承的,元素注入器可以建立並繼承父元素的注入器的尋找函數,模組注入器也類似。當不斷繼承之後,就有點像js物件的prototype鏈了。

2.2 配置提供者

明白了依賴解析的順序優先級,我們就可以在適當的層級對內容進行提供。我們已經知道它有兩種類型:模組注入和元素注入。

  • 模組注入器:在@NgModule的元資料屬性裡可以設定providers,也可以使用v6以後提供的@Injectable宣告provideIn宣告為模組名稱、'root'等。 (實際上在root根模組之上還有兩個注入器,Platform和Null,這裡不討論它們。)

  • 元素注入器:在元件@Component的元資料屬性裡可以設定providers,viewProviders, 或是在指令的@Directive元資料裡的providers.

另外,實際上@Injectable裝飾器除了用了宣告模組注入器外,也可以聲明為元素注入器。更經常會將其聲明為在root提供,以實現單例。它透過類別自己整合元資料來避免模組或元件直接明確聲明provider,這樣如果該類別沒有任何元件指令服務等類別注入它,就沒有程式碼連結到該類型聲明,就可以被編譯器忽略,從而實現了搖樹。

還有一個提供方法是宣告InjectionToken的時候直接給值。

這裡給出這幾種方式的速寫模板:

@NgModule({  providers: [
    // 模块注入器
  ]
})
export class MyModule {}
@Component({  providers: [
    // 元素注入器 - 组件
  ],  
  viewProviders: [
      // 元素注入器- 组件视图
  ]
})
export class MyComponent {}
@Directive({  providers: [
   // 元素注入器 - 指令
 ]
})
export class MyDirective {}
@Injectable({
 providedIn: 'root'
})
export class MyService {}
export const MY_INJECT_TOKEN = new InjectionToken<myclass>('my-inject-token', {
 providedIn: 'root',
 factory: () => {
    return new MyClass();
 }
});</myclass>

提供依賴的位置不同的選擇會帶來一些差異,最終影響著包的大小,依賴的能被注入的範圍和依賴的生命週期。對於不同的場景,如單例(root),服務隔離(module),多重編輯窗(component)等都有不同的適用方案,應選擇合理的位置,避免共享的信息不當,或者代碼打包的冗餘。

2.3 多樣的值函數工具

如果只是提供實例的注入,那還顯示不出Angular框架依賴注入的彈性。 Angular提供了許多靈活的注入工具,useClass 自動建立新實例,useValue 使用靜態值, useExisting 可以重複使用現有的實例,useFactory 透過函數來構造,搭配指定deps 指定構造函數參數,這些組合起來玩法可以非常花樣。可以半路截胡一個類別的token令牌替換成另一個自己準備好的實例,可以造一個token先保存起來值或者實例,然後再在後面需要用到的時候重新替換回去,甚至可以用工廠函數返回實例的局部資訊實作會映射成另一個物件或屬性值。這裡的玩法會透過後面的案例來闡述,這裡就先不展開。官網也有很多例子可以參考。

2.4 注入消費和裝飾器

Angular中的注入可以在建構函式constructor內註入,也可以拿到注入器injector透過get方法取得已有的注入元素。

Angular支援在註入的時候增加裝飾器進行標記,

  • @Host() 來限制冒泡
  • @Self() 限制為元素本身
  • @SkipSelf() 限制在元素本身以上
  • @Optional() 標記為可選
  • @Inject() 限制為自訂Token令牌

#這裡有一篇文章《@Self or @Optional @Host? The visual guide to Angular DI decorators.》非常生動形像地展示父子組件間如果使用了不同的裝飾器,最後會命中的實例有什麼不同。

深入了解Angular中的依賴注入模式(玩法案例)

圖3 不同註入裝飾器的篩選結果

#2.4.1 補充:宿主視圖與@Host

這幾個裝飾器裡面,最不好理解的可能就是@Host了,這裡補充一些@Host的具體說明。 官方對@Host裝飾器的解釋是

...retrieve a dependency from any injector until reaching the host element

#

Host在這裡是宿主的意思,@Host這個裝飾者將會限定查詢的範圍在宿主元素(host element)以內。什麼是宿主元素呢?假如B組件是A組件模板所使用的組件,那麼A組件實例就是B組件實例的宿主元素。元件模板產生的內容稱為View(視圖),同一個View對於不同元件來說可能是不同視圖。如果A元件在自己的template範圍內使用B元件(見圖4),A的模板內容所形成的視圖(紅框部分)對A元件來說就是A的內嵌視圖,B元件在這個視圖內,所以對B來說這個視圖就是B的宿主視圖。裝飾器@Host就是限定搜尋範圍為宿主視圖之內,找不到不會再進行冒泡了。

深入了解Angular中的依賴注入模式(玩法案例)

圖4 內嵌視圖和宿主視圖

#3 案例和玩法

下面我們透過真實的案例,來看看依賴注入到底是怎麼運作起來的,怎麼排查錯誤,以及還能怎麼玩。

3.1 案例一: 模態窗建立動態元件,找不到元件問題

DevUI元件庫的模態窗元件提供了一個服務ModalService,該服務可以彈出一個模態框,而且可以配置為自訂的元件。業務的同學常在使用這個元件的時候報錯,包找不到自訂的元件。

例如以下的報錯:

深入了解Angular中的依賴注入模式(玩法案例)

圖5 使用ModalService的時候建立引用EditorX的元件的報錯找不到對應服務提供者

分析ModalService是如何建立自訂元件的,ModalService來源碼Open函數 第52行和第95行。能看到,componentFactoryResolver如果沒有傳入就使用ModalService注入的componentFactoryResolver。而大多數情況下,業務會在根模組引入一次DevUIModule,但不會在目前模組中引入ModalModule。也就是現狀圖6是這樣的。根據圖6,ModalService的injector內是沒有EditorXModuleService的。

深入了解Angular中的依賴注入模式(玩法案例)

圖6 模組服務提供關係圖

根據注入器的繼承,解決方法有四個:

  • 把EditorXModule 放到ModalModule 聲明的地方,這樣注入器就能找到EditorXModule提供的EditorModuleService —— 這是最糟糕的一種解法,本身loadChildren實現的懶加載就是為了減少首頁模組的加載,結果是子頁內需要用到的內容卻放在AppModule,首次載入就把富文本的大模組給加載了,加重了FMP(First Meaningful Paint),不可採取。

  • 在引入 EditorXModule 且使用 ModalService 的模組裡引入 ModalService —— 可取。只有一種情況不太可取,就是呼叫 ModalService 的是另一個靠頂層的公共 Service,這樣還是把不必要的模組放在了上層去載入。

  • 在觸發使用ModalService的元件,注入目前模組的componentFactoryResolver,並傳給ModalService的open函數參數- 可取, 可以在真正使用的地方再引入EditorXModule。

  • 在使用的模組裡,手動提供一個ModalService —— 可取,解決了注入搜尋的問題。

四種方法其實都是在解決 ModalService 所用的componentFactoryResolver的injector內部鍊式上有EditorXModuleService問題。保證在兩層搜尋鏈上,這個問題就可以解決了。

知識點小結:模組注入器繼承和尋找範圍。

3.2 案例二:CdkVirtualScrollFor找不到CdkVirtualScrollViewport

通常我們多個地方使用同一個模板的時候,會透過template 提取公共部分,之前DevUI Select元件開發的時候開發者想將共用的部分抽出來報錯了。

深入了解Angular中的依賴注入模式(玩法案例)

深入了解Angular中的依賴注入模式(玩法案例)

#圖7 程式碼移動與找不到注入封包錯誤

這裡是由於 CdkVirtualScrollFor指令需要注入一個CdkVirtualScrollViewport,然而元素注入injector繼承體係是繼承靜態AST關係的DOM,動態的不行,所以發生以下查詢行為,查找後報失敗。

深入了解Angular中的依賴注入模式(玩法案例)

圖8 元素注入器查詢鏈尋找範圍

最後解::要麼1)保持原始程式碼位置不變,要麼2)需要把整個模板內嵌就能找到了。

深入了解Angular中的依賴注入模式(玩法案例)

圖9 內嵌整塊模組使得能CdkVitualScrollFo能找到CdkVirtualScrollViewport(解法二)

#知識點小結:元素注入器的查詢鏈條是靜態模板的DOM元素祖先。

3.3 案例三: 表單校驗的元件被封裝到子元件內無法校驗問題

這個案例來自這篇部落格《Angular: Nested template driven form》。

在使用表單校驗的時候我們也遇到了一樣的問題。如圖10所示,由於某些原因我們把三個欄位的位址封裝成一個元件以供重複使用。

1深入了解Angular中的依賴注入模式(玩法案例)

圖10 把表單的位址三個欄位封裝成一個子元件

這時候我們會發現報錯了,ngModelGroup需要一個host內部的ControlContainer,也就是ngForm指令提供的內容。

1深入了解Angular中的依賴注入模式(玩法案例)

圖11 ngModelGroup 找不到ControlContainer

查看ngModelGroup程式碼可以看到它只加入了host裝飾器的限制。

1深入了解Angular中的依賴注入模式(玩法案例)

圖12 ng_model_group.ts限定了注入ControlContainer的範圍

這裡可以使用viewProvider搭配usingExisting為AddressComponent的宿主視圖增加ControlContainer的Provider

1深入了解Angular中的依賴注入模式(玩法案例)

#圖13 使用viewProviders為巢狀元件提供外部的Provider

知識點小結:viewProvider 和usingExisting 搭配的妙用。

3.4 案例四:拖曳模組提供的Service,由於懶加載,不是單例了,導致無法互相拖曳

內部的業務平台有涉及跨多個模組的拖曳,由於涉及了loadChildren懶加載,每個模組會單獨打包DevUI元件庫的DragDropModule,該Module提供了一個DragDropService。拖曳指令分為可拖曳指令Draggable和可放置指令Droppable,兩個指令透過DragDropService進行通訊。原本引入同一個模組使用模組提供的Service是可以通訊的,但是懶載入後DragDropModule模組被打包了兩次,也對應產生兩份隔離的實例。這時候處於一個懶載入模組裡的Draggable指令就無法與另一個懶載入模組裡的Droppable指令進行通訊了,因為此時DragDropService並不是同個實例了。

1深入了解Angular中的依賴注入模式(玩法案例)

圖14 懶載入模組導致服務不是同一實例/單例

這裡明顯我們的述求是需要單例,而單例的做法通常就是providerIn: 'root'就好了,那麼是不是就讓元件庫的DragDropService不要提供在模組級別,直接提供root界別的可好。但是細細想下來,這裡面又會有其他的問題。元件庫本身是提供給多種多樣的業務使用的,萬一有的業務在頁面的兩個地方分別有兩組對應的拖曳並不想要聯動起來。這時候單例反而破壞了這種基於模組的自然隔離。

那麼要實作單例由業務側來做替換會比較合理。記得我們前面提到的依賴查詢鏈,元素的注入器是優先被尋找的,找不到才開始找模組注入器。所以替換思路就是我們提供元素層級的provider即可。

1深入了解Angular中的依賴注入模式(玩法案例)

圖15 用擴充方法取得一個新的DragDropService並將它標記為在root層級提供

1深入了解Angular中的依賴注入模式(玩法案例)

1深入了解Angular中的依賴注入模式(玩法案例)##

圖16 利用同個selector可以疊加重複指令,給元件庫的Draggable指令和Droppable指令疊加一個額外的指令並把DragDropService的token替換成已經在root提供單例的DragDropGlobalService

#如圖15和16, 我們透過元素注入器,疊加了指令,把DragDropService這個令牌替換成我們自己的全域單例的實例。這時候需要使用這個全域單例的DragDropService的地方,我們只需要引入宣告並導出了這兩個extra指令的模組就是使得元件庫的Draggable指令Droppable指令能夠跨懶載入模組進行通訊了。

知識點小結:元素注入器優先權高於模組注入器。

3.5 案例五: 局部主題功能場景怎麼讓下拉式選單附著在局部問題

DevUI元件庫的主題化是使用了CSS自訂屬性(css變量)聲明:root的css變數值從而實現了主題切換。如果我們要在一個介面內同時展示不同主題的預覽,我們可以在DOM元素局部重新聲明css變數從而達到局部主題的功能。之前在做主題仿色產生器的時候就用了這樣一個方法來是局部應用一個主題。

1深入了解Angular中的依賴注入模式(玩法案例)

圖17 局部主題功能

但是僅僅局部應用css變數值還不夠,有一些下拉彈出層它是預設附著在body最後面的,也就是說它的附著層在局部變數的外部,這將會導致一個非常尷尬的問題。局部主題的元件的下拉框下拉出來是外部的主題的樣式。

深入了解Angular中的依賴注入模式(玩法案例)

圖18 局部主題內元件附著外部的疊加層下拉框主題不正確

這時候怎麼辦?我們應該把附著點移動回局部主題dom內部。

已知DevUI元件庫的DatePickerPro元件的Overlay使用的是Angular CDK的Overlay,經過一輪分析我們用注入替換如下:

1)首先我們繼承OverlayContainer並實現自己的ElementOverlayContainer如下圖。

2深入了解Angular中的依賴注入模式(玩法案例)

圖19 自訂ElementOverlayContainer並替換掉_createContainer邏輯

2)然後在預覽的元件側,直接提供我們新ElementOverlayContainer,並提供新的Overlay,好讓新的Overlay能使用我們的OverlayContainer。原本Overlay和OverlayContainer都提供在root上,這裡我們要涵蓋這兩個。

2深入了解Angular中的依賴注入模式(玩法案例)

圖20 替換OverlayContainer為自訂的ElementOverlayContainer,提供一個新的Overlay

這時候再去預覽網站,彈出層的DOM就順利被附著到component-preview這個元素裡面了。

2深入了解Angular中的依賴注入模式(玩法案例)

圖21 cdk的Overlay容器被附著到指定的dom內部,局部主題預覽成功

DevUI元件庫內部還有自訂的OverlayContainerRef用於部分組件和模態框抽屜板凳,也需要進行相應的替換。最終能實現彈窗彈出層等完美支援局部主題。

知識點小結:好的抽像模式可以使得模組可替換,實現優雅的切面程式設計。

3.6 案例六: CdkOverlay要求在滾動條地方加上CdkScrollable指令,但無法給入口元件最外層加上該指令如何處理

#到了最後一個案例,想講一點不太正規的做法,以方便大家理解provider的本質,配置provider本質上就是讓它幫你做實例化或映射到某個存在的實例。

我們知道如果使用了cdkOverlay,如果我們想要彈出框跟隨滾動條滾動也能懸浮在正確的位置的話,我們就需要給滾動條加上cdkScrollable的指令。

還是上一個例子的場景。我們整個頁面是透過路由載入進來的,貪圖簡單我把滾動條寫在了元件的host了。

2深入了解Angular中的依賴注入模式(玩法案例)

圖22 內容溢出滾動條把overflow:auto 寫在元件:host裡

這樣我們就遇到了一個比較難搞的問題,模組是router定義指定過來的,也就是沒有任何地方明確地呼叫<app-theme-picker-customize></app-theme-picker-customize>,那cdkScrollable指令該怎麼加進去呢?解法如下,這裡隱藏掉了部分程式碼只留下核心程式碼。

2深入了解Angular中的依賴注入模式(玩法案例)

圖23 透過注入建立實例並手動呼叫生命週期

這裡透過注入產生了一個cdkScrollable的實例,並在元件的生命週期階段同步地呼叫生命週期。

這種解法不是正規手段,但確實解決了問題,這裡就作為一種思路和探索留給讀者品味。

知識點小結: 依賴注入組態提供者可以實現建立實例,但要注意實例將當做普通Service類別對待,無法擁有以完整生命週期。

3.7 更多玩法: 自訂替換platform,實作讓Angular框架跑在terminal終端機上的互動

可以參考這篇文章:《Rendering Angular applications in Terminal

2深入了解Angular中的依賴注入模式(玩法案例)

圖24 替換RendererFactory2渲染器等內容, 讓Angular運行在終端terminal上

作者透過替換RendererFactory2等渲染器,讓Angular應用可以跑在終端terminal。這就是Angular設計的靈活度,連platform都可以替換掉的強大的靈活。詳細的替換細節可以查看原始文章,這裡就不展開了。

知識點小結:依賴注入的強大之處,在於提供者可以自行配置,最後實作替換邏輯。

4 總結

本文介紹了控制反轉的依賴注入模式及其好處,介紹了Angular中依賴注入是如何查找依賴,如何配置提供者,如何用限定與濾波作用的裝飾器拿到想要的實例,進一步透過N個案例分析如何結合依賴注入的知識點來解決開發程式設計會遇到的問題。

正確的理解依賴查找過程,我們便能在準確的位置配置上提供者(案例一二),截胡替換其他實例為單例(案例四、五),甚至能跨嵌套元件包裹的限制銜接上提供的實例(案例三)或以提供的方法曲線實現指令實例化(案例六)。

其中案例五看似是簡單的替換,但是要能寫出能被替換的程式碼結構需要對注入模式有深入的了解,並對各個功能有比較好的合理的抽象,抽像不得當,就無法發揮依賴注入的最大功效了。注入模式為模組可插拔,插件化,零件化提供了更多可能的空間,降低耦合度,增加靈活性,是模組之間能更優雅、協調地一起工作。

依賴注入功能的強大,除了能完成優化組件通訊路徑,更重要的是還能實現控制反轉,給封裝好的元件暴露更多切面程式設計的切面,一些業務特殊邏輯的實現也可以變得靈活起來。

更多程式相關知識,請造訪:程式設計影片! !

以上是深入了解Angular中的依賴注入模式(玩法案例)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除