
作為“為大型前端項目”而設計的前端架構,Angular 其實有許多值得參考和學習的設計,本系列主要用于研究這些設計和功能的實作原理。
本文主要圍繞 Angular 中的最大特點——依賴注入,介紹 Angular 中多級依賴注入的設計。
在 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()。
是以,子產品注入器的分層結構如下:
在我們實際的應用中,它很可能是這樣的:
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,
};
}
引入元素注入器樹,原因是這樣的設計比較簡單。通過更改注入器層次結構,避免交錯插入子產品群組件注入器,進而導緻延遲加載子產品的雙倍執行個體化。
因為每個注入器都隻有一個父對象,并且每次解析都必須精确地尋找一個注入器來檢索依賴項。
元素注入器(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),同時會建立根元素注入器,在這裡elInjector為Injector.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
依賴解析過程,解析算法會基于視圖層次結構,如圖所示進行:
如果在子元件中解析某些令牌,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 解析依賴的過程也有調整,優先尋找元素注入器以及父視圖元素注入器等注入器的依賴,隻有元素注入器中無法找到令牌時,才會查詢子產品注入器中的依賴。
感謝你的閱讀,如果有什麼問題,請在留言區給我留言。