作為「為大型前端專案」而設計的前端框架,Angular 其實有許多值得參考和學習的設計,本系列主要用於研究這些設計和功能的實現原理。本文主要圍繞Angular 中的最大特點-依賴注入,介紹Angular 中多層次依賴注入的設計。 【相關教學推薦:《angular教學》】
上一篇我們介紹了Angular 中的Injectot
注入器、 Provider
提供者,以及注入器機制。那麼,在Angular 應用中,各個元件和模組間又是怎樣共享依賴的,同樣的服務是否可以多次實例化呢?
組件和模組的依賴注入過程,離不開Angular 多層次依賴注入的設計,我們來看看。
前面我們說過,Angular 中的注入器是可繼承、且分層的。
在Angular 中,有兩個注入器層次結構:
ModuleInjector
模組注入器:使用@NgModule()
或@Injectable()
註解在此層次結構中配置ModuleInjector
ElementInjector
元素注入器:在每個DOM 元素上隱式建立模組注入器和元素注入器都是樹狀結構的,但它們的分層結構並不完全一致。
模組注入器的分層結構,除了與應用程式中模組設計有關係,還有平台模組(PlatformModule)注入器與應用程式模組(AppModule)注入器的分層結構。
在Angular 術語中,平台是供Angular 應用程式在其中運行的上下文。 Angular 應用程式最常見的平台是Web 瀏覽器,但它也可以是行動裝置的作業系統或Web 伺服器。
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()
。而在平台模組注入器之下,則還有應用程式模組注入器。
每個應用程式都有至少一個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()
。
因此,模組注入器的分層結構如下:
在我們實際的應用中,它很可能是這樣的:
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
)包含elInjector
和ngModule
注入器的參考:
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, }; }
引入元素注入器樹,原因是這樣的設計比較簡單。透過更改注入器層次結構,避免交錯插入模組和組件注入器,從而導致延遲載入模組的雙倍實例化。因為每個注入器都只有一個父對象,而且每次解析都必須精確地尋找一個注入器來檢索依賴項。
在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
) ,同時會建立根元素注入器,在這裡elInjector
為Injector.NULL
。
在這裡,Angular 的注入器樹被分成元素注入器樹和模組注入器樹,這兩個平行的樹了。
Angular 會有規律的建立下級注入器,每當Angular 建立一個在@Component()
中指定了providers
的元件實例時,它也會為該實例建立一個新的子注入器。類似的,當在運行期間載入一個新的NgModule
時,Angular 也可以為它創建一個擁有自己的提供者的注入器。
子模組和元件注入器彼此獨立,並且會為所提供的服務分別建立自己的實例。當Angular 銷毀NgModule
或元件實例時,也會銷毀這些注入器以及注入器中的那些服務實例。
上面我們介紹了Angular 中的兩種注入器樹:模組注入器樹和元素注入器樹。那麼,Angular 在提供依賴時,又會以怎樣的方式去進行解析呢。
在Angular 種,當為組件/指令解析token 獲取依賴時,Angular 分為兩個階段來解析它:
ElementInjector
層次結構(其父級)ModuleInjector
層次結構(其父級)其過程如下(參考多級注入器-解析規則):
當元件宣告依賴項時,Angular 會嘗試使用它自己的ElementInjector
來滿足該依賴。
如果元件的注入器缺少提供者,它將把請求傳給其父元件的ElementInjector
。
這些請求將繼續轉發,直到Angular 找到可以處理該請求的注入器或用完祖先ElementInjector
。
如果Angular 在任何ElementInjector
中都找不到提供者,它將返回到發起請求的元素,並在ModuleInjector
層次結構中進行查找。
如果Angular 仍然找不到提供者,它將引發錯誤。
為此,Angular 引入一種特殊的合併注入器。
合併注入器本身沒有任何值,它只是視圖和元素定義的組合。
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
依賴解析過程,解析演算法會基於視圖層次結構,如圖所示進行:
如果在子元件中解析某些令牌,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 解析依賴的過程也有調整,優先尋找元素注入器以及父視圖元素注入器等注入器的依賴,只有當元素注入器中無法找到令牌時,才會查詢模組注入器中的依賴。