淺析Angular中的多級依賴注入設計

2022-03-15 13:00:23
本篇文章帶大家進行Angular原始碼學習,介紹一下多級依賴注入設計,希望對大家有所幫助!

作為「為大型前端專案」而設計的前端框架,Angular 其實有許多值得參考和學習的設計,本系列主要用於研究這些設計和功能的實現原理。本文主要圍繞 Angular 中的最大特點——依賴注入,介紹 Angular 中多級依賴注入的設計。【相關教學推薦:《》】

上一篇我們介紹了 Angular 中的Injectot注入器、Provider提供者,以及注入器機制。那麼,在 Angular 應用中,各個元件和模組間又是怎樣共用依賴的,同樣的服務是否可以多次範例化呢?

元件和模組的依賴注入過程,離不開 Angular 多級依賴注入的設計,我們來看看。

多級依賴注入

我們說過,Angular 中的注入器是可繼承、且分層的。

在 Angular 中,有兩個注入器層次結構:

  • ModuleInjector模組注入器:使用@NgModule()@Injectable()註解在此層次結構中設定ModuleInjector
  • ElementInjector元素注入器:在每個 DOM 元素上隱式建立

模組注入器和元素注入器都是樹狀結構的,但它們的分層結構並不完全一致。

模組注入器

模組注入器的分層結構,除了與應用中模組設計有關係,還有平臺模組(PlatformModule)注入器與應用程式模組(AppModule)注入器的分層結構。

平臺模組(PlatformModule)注入器

在 Angular 術語中,平臺是供 Angular 應用程式在其中執行的上下文。Angular 應用程式最常見的平臺是 Web 瀏覽器,但它也可以是移動裝置的作業系統或 Web 伺服器。

Angular 應用在啟動時,會建立一個平臺層:

  • 平臺是 Angular 在網頁上的入口點,每個頁面只有一個平臺
  • 頁面上執行的每個 Angular 應用程式,所共有的服務都在平臺內繫結

一個 Angular 平臺,主要包括建立模組範例、銷燬等功能:

@Injectable()
export class PlatformRef {
  // 傳入注入器,作為平臺注入器
  constructor(private _injector: Injector) {}

  // 為給定的平臺建立一個 @NgModule 的範例,以進行離線編譯
  bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options?: BootstrapOptions):
      Promise<NgModuleRef<M>> {}

  // 使用給定的執行時編譯器,為給定的平臺建立一個 @NgModule 的範例
  bootstrapModule<M>(
      moduleType: Type<M>,
      compilerOptions: (CompilerOptions&BootstrapOptions)|
      Array<CompilerOptions&BootstrapOptions> = []): Promise<NgModuleRef<M>> {}

  // 註冊銷燬平臺時要呼叫的偵聽器
  onDestroy(callback: () => void): void {}

  // 獲取平臺注入器
  // 該平臺注入器是頁面上每個 Angular 應用程式的父注入器,並提供單例提供程式
  get injector(): Injector {}

  // 銷燬頁面上的當前 Angular 平臺和所有 Angular 應用程式,包括銷燬在平臺上註冊的所有模組和偵聽器
  destroy() {}
}

實際上,平臺在啟動的時候(bootstrapModuleFactory方法中),在ngZone.run中建立ngZoneInjector,以便在 Angular 區域中建立所有範例化的服務,而ApplicationRef(頁面上執行的 Angular 應用程式)將在 Angular 區域之外建立。

在瀏覽器中啟動時,會建立瀏覽器平臺:

export const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef =
    createPlatformFactory(platformCore, 'browser', INTERNAL_BROWSER_PLATFORM_PROVIDERS);

// 其中,platformCore 平臺必須包含在任何其他平臺中
export const platformCore = createPlatformFactory(null, 'core', _CORE_PLATFORM_PROVIDERS);

使用平臺工廠(例如上面的createPlatformFactory)建立平臺時,將隱式初始化頁面的平臺:

export function createPlatformFactory(
    parentPlatformFactory: ((extraProviders?: StaticProvider[]) => PlatformRef)|null, name: string,
    providers: StaticProvider[] = []): (extraProviders?: StaticProvider[]) => PlatformRef {
  const desc = `Platform: ${name}`;
  const marker = new InjectionToken(desc); // DI 令牌
  return (extraProviders: StaticProvider[] = []) => {
    let platform = getPlatform();
    // 若平臺已建立,則不做處理
    if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) {
      if (parentPlatformFactory) {
        // 若有父級平臺,則直接使用父級平臺,並更新相應的提供者
        parentPlatformFactory(
            providers.concat(extraProviders).concat({provide: marker, useValue: true}));
      } else {
        const injectedProviders: StaticProvider[] =
            providers.concat(extraProviders).concat({provide: marker, useValue: true}, {
              provide: INJECTOR_SCOPE,
              useValue: 'platform'
            });
        // 若無父級平臺,則新建注入器,並建立平臺
        createPlatform(Injector.create({providers: injectedProviders, name: desc}));
      }
    }
    return assertPlatform(marker);
  };
}

通過以上過程,我們知道 Angular 應用在建立平臺的時候,建立平臺的模組注入器ModuleInjector。我們從上一節Injector定義中也能看到,NullInjector是所有注入器的頂部:

export abstract class Injector {
  static NULL: Injector = new NullInjector();
}

因此,在平臺模組注入器之上,還有NullInjector()。而在平臺模組注入器之下,則還有應用程式模組注入器。

應用程式根模組(AppModule)注入器

每個應用程式有至少一個 Angular 模組,根模組就是用來啟動此應用的模組:

@NgModule({ providers: APPLICATION_MODULE_PROVIDERS })
export class ApplicationModule {
  // ApplicationRef 需要載入程式提供元件
  constructor(appRef: ApplicationRef) {}
}

AppModule根應用模組由BrowserModule重新匯出,當我們使用 CLI 的new命令建立新應用時,它會自動包含在根AppModule中。應用程式根模組中,提供者關聯著內建的 DI 令牌,用於為載入程式設定根注入器。

Angular 還將ComponentFactoryResolver新增到根模組注入器中。此解析器儲存了entryComponents系列工廠,因此它負責動態建立元件。

模組注入器層級

到這裡,我們可以簡單地梳理出模組注入器的層級關係:

  • 模組注入器樹的最上層則是應用程式根模組(AppModule)注入器,稱作 root。

  • 在 root 之上還有兩個注入器,一個是平臺模組(PlatformModule)注入器,一個是NullInjector()

因此,模組注入器的分層結構如下:

1.png

在我們實際的應用中,它很可能是這樣的:

2.png

Angular DI 具有分層注入體系,這意味著下級注入器也可以建立它們自己的服務範例。

元素注入器

前面說過,在 Angular 中有兩個注入器層次結構,分別是模組注入器和元素注入器。

元素注入器的引入

當 Angular 中懶載入的模組開始廣泛使用時,出現了一個 issue:依賴注入系統導致懶載入模組的範例化加倍。

在這一次修復中,引入了新的設計注入器使用兩棵並行的樹,一棵用於元素,另一棵用於模組

Angular 會為所有entryComponents建立宿主工廠,它們是所有其他元件的根檢視。

這意味著每次我們建立動態 Angular 元件時,都會使用根資料(RootData)建立根檢視(RootView):

class ComponentFactory_ extends ComponentFactory<any>{
  create(
      injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: string|any,
      ngModule?: NgModuleRef<any>): ComponentRef<any> {
    if (!ngModule) {
      throw new Error('ngModule should be provided');
    }
    const viewDef = resolveDefinition(this.viewDefFactory);
    const componentNodeIndex = viewDef.nodes[0].element!.componentProvider!.nodeIndex;
    // 使用根資料建立根檢視
    const view = Services.createRootView(
        injector, projectableNodes || [], rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT);
    // view.nodes 的存取器
    const component = asProviderData(view, componentNodeIndex).instance;
    if (rootSelectorOrNode) {
      view.renderer.setAttribute(asElementData(view, 0).renderElement, 'ng-version', VERSION.full);
    }
    // 建立元件
    return new ComponentRef_(view, new ViewRef_(view), component);
  }
}

該根資料(RootData)包含對elInjectorngModule注入器的參照:

function createRootData(
    elInjector: Injector, ngModule: NgModuleRef<any>, rendererFactory: RendererFactory2,
    projectableNodes: any[][], rootSelectorOrNode: any): RootData {
  const sanitizer = ngModule.injector.get(Sanitizer);
  const errorHandler = ngModule.injector.get(ErrorHandler);
  const renderer = rendererFactory.createRenderer(null, null);
  return {
    ngModule,
    injector: elInjector,
    projectableNodes,
    selectorOrNode: rootSelectorOrNode,
    sanitizer,
    rendererFactory,
    renderer,
    errorHandler,
  };
}

引入元素注入器樹,原因是這樣的設計比較簡單。通過更改注入器層次結構,避免交錯插入模組和元件注入器,從而導致延遲載入模組的雙倍範例化。因為每個注入器都只有一個父物件,並且每次解析都必須精確地尋找一個注入器來檢索依賴項。

元素注入器(Element Injector)

在 Angular 中,檢視是模板的表示形式,它包含不同型別的節點,其中便有元素節點,元素注入器位於此節點上:

export interface ElementDef {
  ...
  // 在該檢視中可見的 DI 的公共提供者
  publicProviders: {[tokenKey: string]: NodeDef}|null;
  // 與 visiblePublicProviders 相同,但還包括位於此元素上的私有提供者
  allProviders: {[tokenKey: string]: NodeDef}|null;
}

預設情況下ElementInjector為空,除非在@Directive()@Component()providers屬性中進行設定。

當 Angular 為巢狀的 HTML 元素建立元素注入器時,要麼從父元素注入器繼承它,要麼直接將父元素注入器分配給子節點定義。

如果子 HTML 元素上的元素注入器具有提供者,則應該繼承該注入器。否則,無需為子元件建立單獨的注入器,並且如果需要,可以直接從父級的注入器中解決依賴項。

元素注入器與模組注入器的設計

那麼,元素注入器與模組注入器是從哪個地方開始成為平行樹的呢?

我們已經知道,應用程式根模組(AppModule)會在使用 CLI 的new命令建立新應用時,自動包含在根AppModule中。

當應用程式(ApplicationRef)啟動(bootstrap)時,會建立entryComponent

const compRef = componentFactory.create(Injector.NULL, [], selectorOrNode, ngModule);

該過程會使用根資料(RootData)建立根檢視(RootView),同時會建立根元素注入器,在這裡elInjectorInjector.NULL

在這裡,Angular 的注入器樹被分成元素注入器樹和模組注入器樹,這兩個平行的樹了。

Angular 會有規律的建立下級注入器,每當 Angular 建立一個在@Component()中指定了providers的元件範例時,它也會為該範例建立一個新的子注入器。類似的,當在執行期間載入一個新的NgModule時,Angular 也可以為它建立一個擁有自己的提供者的注入器。

子模組和元件注入器彼此獨立,並且會為所提供的服務分別建立自己的範例。當 Angular 銷燬NgModule或元件範例時,也會銷燬這些注入器以及注入器中的那些服務範例。

Angular 解析依賴過程

上面我們介紹了 Angular 中的兩種注入器樹:模組注入器樹和元素注入器樹。那麼,Angular 在提供依賴時,又會以怎樣的方式去進行解析呢。

在 Angular 種,當為元件/指令解析 token 獲取依賴時,Angular 分為兩個階段來解析它:

  • 針對ElementInjector層次結構(其父級)
  • 針對ModuleInjector層次結構(其父級)

其過程如下(參考多級注入器-解析規則):

  • 當元件宣告依賴項時,Angular 會嘗試使用它自己的ElementInjector來滿足該依賴。

  • 如果元件的注入器缺少提供者,它將把請求傳給其父元件的ElementInjector

  • 這些請求將繼續轉發,直到 Angular 找到可以處理該請求的注入器或用完祖先ElementInjector

  • 如果 Angular 在任何ElementInjector中都找不到提供者,它將返回到發起請求的元素,並在ModuleInjector層次結構中進行查詢。

  • 如果 Angular 仍然找不到提供者,它將引發錯誤。

為此,Angular 引入一種特殊的合併注入器。

合併注入器(Merge Injector)

合併注入器本身沒有任何值,它只是檢視和元素定義的組合。

class Injector_ implements Injector {
  constructor(private view: ViewData, private elDef: NodeDef|null) {}
  get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any {
    const allowPrivateServices =
        this.elDef ? (this.elDef.flags & NodeFlags.ComponentView) !== 0 : false;
    return Services.resolveDep(
        this.view, this.elDef, allowPrivateServices,
        {flags: DepFlags.None, token, tokenKey: tokenKey(token)}, notFoundValue);
  }
}

當 Angular 解析依賴項時,合併注入器則是元素注入器樹和模組注入器樹之間的橋樑。當 Angular 嘗試解析元件或指令中的某些依賴關係時,會使用合併注入器來遍歷元素注入器樹,然後,如果找不到依賴關係,則切換到模組注入器樹以解決依賴關係。

class ViewContainerRef_ implements ViewContainerData {
  ...
  // 父級試圖元素注入器的查詢
  get parentInjector(): Injector {
    let view = this._view;
    let elDef = this._elDef.parent;
    while (!elDef && view) {
      elDef = viewParentEl(view);
      view = view.parent!;
    }

    return view ? new Injector_(view, elDef) : new Injector_(this._view, null);
  }
}

解析過程

注入器是可繼承的,這意味著如果指定的注入器無法解析某個依賴,它就會請求父注入器來解析它。具體的解析演演算法在resolveDep()方法中實現:

export function resolveDep(
    view: ViewData, elDef: NodeDef, allowPrivateServices: boolean, depDef: DepDef,
    notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any {
  //
  //          mod1
  //         /
  //       el1   mod2
  //         \  /
  //         el2
  //
  // 請求 el2.injector.get(token)時,按以下順序檢查並返回找到的第一個值:
  // - el2.injector.get(token, default)
  // - el1.injector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) -> do not check the module
  // - mod2.injector.get(token, default)
}

如果是<child></child>這樣模板的根AppComponent元件,那麼在 Angular 中將具有三個檢視:

<!-- HostView_AppComponent -->
    <my-app></my-app>
<!-- View_AppComponent -->
    <child></child>
<!-- View_ChildComponent -->
    some content

依賴解析過程,解析演演算法會基於檢視層次結構,如圖所示進行:

3.png

如果在子元件中解析某些令牌,Angular 將:

  • 首先檢視子元素注入器,進行檢查elRef.element.allProviders|publicProviders

  • 然後遍歷所有父檢視元素(1),並檢查元素注入器中的提供者。

  • 如果下一個父檢視元素等於null(2),則返回到startView(3),檢查startView.rootData.elnjector(4)。

  • 只有在找不到令牌的情況下,才檢查startView.rootData module.injector( 5 )。

由此可見,Angular 在遍歷元件以解析某些依賴性時,將搜尋特定檢視的父元素而不是特定元素的父元素。檢視的父元素可以通過以下方法獲得:

// 對於元件檢視,這是宿主元素
// 對於嵌入式檢視,這是包含檢視容器的父節點的索引
export function viewParentEl(view: ViewData): NodeDef|null {
  const parentView = view.parent;
  if (parentView) {
    return view.parentNodeDef !.parent;
  } else {
    return null;
  }
}

總結

本文主要介紹了 Angular 中注入器的層級結構,在 Angular 中有兩棵平行的注入器樹:模組注入器樹和元素注入器樹。

元素注入器樹的引入,主要是為了解決依賴注入解析懶載入模組時,導致模組的雙倍範例化問題。在元素注入器樹引入後,Angular 解析依賴的過程也有調整,優先尋找元素注入器以及父檢視元素注入器等注入器的依賴,只有元素注入器中無法找到令牌時,才會查詢模組注入器中的依賴。

更多程式設計相關知識,請存取:!!

以上就是淺析Angular中的多級依賴注入設計的詳細內容,更多請關注TW511.COM其它相關文章!