通常一個angular項目會有一個個模塊(Module)來管理各自的業務,並且必須有一個根模塊(AppModule)作為應用的入口模塊,整個應用都圍繞AppModule展開。可以這么說,AppModule是一個angular項目的起點。
不過單從angular的啟動過程來說,AppModule就是其工作的終點。整個angular框架的啟動過程都是為了使AppModule可以工作而展開的。本文算是筆者單就閱讀angular源碼中的啟動過程相關部分的總結,angular源碼博大精深,有任何筆者理解不夠或錯誤的地方還望包涵並指正。
源碼中的一些類與個人的翻譯
在Typescript的幫助下angular框架實現了究極抽象,其中有幾個固定風格的命名,筆者個人的翻譯如下:
- XXX-Injector: 注入器 ( 也就是到處注入的服務實例 )
- XXX-Factory: 工廠 ( 包括了 編譯器工廠、平台工廠、模塊工廠、組建工廠等,所有這些類都由工廠創建出來 )
- XXX-Ref: 引用 ( 工廠創建出的所有類都是一個引用,通過引用來進行控制 )
啟動過程的實現目標
首先不直接查看angular源代碼,而是從實際項目的啟動代碼入手,一般的實際項目通過這樣的代碼啟動:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app';
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));
根據涉及的兩個方法的返回類型可以看出angular的啟動整體來看分為兩部曲:
- 創建平台引用 platformBrowserDynamic()
- 使用創建的平台引用進一步創建模塊引用 bootstrapModule(AppModule)
接下來就要利用好TypeScript究極智能代碼提示到處F12來進入angular的源代碼了。
得到平台引用 PlatformRef
首先在 /packages/platform-browser-dynamic/src/platform-browser-dynamic.ts 下找到 platformBrowserDynamic 方法的定義:
// JIT下創建 平台工廠
export const platformBrowserDynamic = createPlatformFactory(
platformCoreDynamic, 'browserDynamic', INTERNAL_BROWSER_DYNAMIC_PLATFORM_PROVIDERS);
可見 platformBrowserDynamic 本身是一個平台工廠,直接執行平台工廠就可以得到一個平台引用。
根據傳入的參數可以得出,創建好的平台名稱為 browserDynamic,注入了一個名字很長的服務,並且依賴一個父級工廠叫做 platformCoreDynamic。
進入 INTERNAL_BROWSER_DYNAMIC_PLATFORM_PROVIDERS 發現有意思的幾個注入器如下:
{ // 傳入的是一個資源加載器 內含一個get方法用來創建XmlHttpRequest對象請求資源
provide: COMPILER_OPTIONS,
useValue: {providers: [{provide: ResourceLoader, useClass: ResourceLoaderImpl}]},
multi: true
},
{provide: PLATFORM_INITIALIZER, useValue: initDomAdapter, multi: true}, // 初始化DOM適配器 實例化一個類 包含所有DOM操作
{provide: PlatformLocation, useClass: BrowserPlatformLocation}, // 瀏覽器端定位服務 提供了一些從DOM中得到url、歷史等信息的能力
{provide: DOCUMENT, useFactory: _document, deps: []}, // windows 的 document 對象
進入父平台 platformCoreDynamic 的實現:
// 作為根平台工廠的父平台 這個也使用平台工廠創建,但其父平台為 核心平台
export const platformCoreDynamic = createPlatformFactory(platformCore, 'coreDynamic', [
{provide: COMPILER_OPTIONS, useValue: {}, multi: true},
{provide: CompilerFactory, useClass: JitCompilerFactory},
]);
這個平台表面上也沒做什么厲害的事情,不就是注入了一個編譯選項 COMPILER_OPTIONS,還是空的,以及一個看名字很厲害實際上更厲害的 JitCompilerFactory,直接翻譯為JIT編譯器工廠,那就是用來生成編譯器的東西。
然后繼續往下進入爺爺平台 platformCore 的實現:
// 核心平台的服務商 比較厲害的就是 PlatformRef_
const _CORE_PLATFORM_PROVIDERS: Provider[] = [
{ provide: PLATFORM_ID, useValue: 'unknown' }, // 用於設置缺省平台名
PlatformRef_, // 核心模塊引用
{ provide: PlatformRef, useExisting: PlatformRef_ }, // 模塊引用就是核心模塊引用
{ provide: Reflector, useFactory: _reflector, deps: [] }, // Reflect待研究
TestabilityRegistry, // 測試支持待研究
Console, // 日志服務
];
// 核心工廠無父平台 就直接傳null 命名為 core
export const platformCore = createPlatformFactory(null, 'core', _CORE_PLATFORM_PROVIDERS);
親戚關系就到爺爺這里為止了,現在進入 createPlatformFactory 看看平台工廠是怎么創建的(不是創建平台,而是創建工廠):
export function createPlatformFactory(
parentPlatformFactory: ((extraProviders?: Provider[]) => PlatformRef) | null, name: string, // 父級工廠
providers: Provider[] = []
): (extraProviders?: Provider[]) => PlatformRef {
const marker = new InjectionToken(`Platform: ${name}`);
return (extraProviders: Provider[] = []) => {
let platform = getPlatform();
// 保證只能創建一個平台
if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) {
if (parentPlatformFactory) {
// 有父級平台工廠的話使用父級並注入自己包含的注入器
parentPlatformFactory(providers.concat(extraProviders).concat({provide: marker, useValue: true}));
} else {
// 沒有父級平台說明是爺爺平台了 那就直接創建平台
createPlatform(ReflectiveInjector.resolveAndCreate(providers.concat(extraProviders).concat({provide: marker, useValue: true})));
}
}
// 確保平台已被創建並包含maker信息並返回創建好的平台
return assertPlatform(marker);
};
}
export function assertPlatform(requiredToken: any): PlatformRef {
const platform = getPlatform();
if (!platform) { // 確保平台被創建
throw new Error('No platform exists!');
}
if (!platform.injector.get(requiredToken, null)) { // 這里是確保 marker 被注入
throw new Error('A platform with a different configuration has been created. Please destroy it first.');
}
return platform;
}
看來核心的創建平台的代碼就在 createPlatform 里了:
// 最頂級的平台工廠會執行並創建出平台
export function createPlatform(injector: Injector): PlatformRef {
// 存在平台 && 未銷毀 && 不允許多個平台
if (_platform && !_platform.destroyed && !_platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) {
throw new Error('There can be only one platform. Destroy the previous one to create a new one.');
}
// 得到這個平台引用
_platform = injector.get(PlatformRef);
// 得到服務商
const inits = injector.get(PLATFORM_INITIALIZER, null);
// 執行初始化
if (inits) inits.forEach((init: any) => init());
// 返回初始化完成的平台
return _platform;
}
看完 createPlatform,沒錯angular又繼續踢皮球了,回憶創建爺爺平台時注入的那一堆服務中,就有個 PlatformRef,可不就是平台引用嗎~
使用創建的平台引用啟動根模塊
上面的爺爺平台注入了一個平台引用,其實現是 PlatformRef_,其提供了啟動模塊的一些方法,將在兩部曲的第二步中用到。
首先徑直在 /packages/core/src/application_ref.ts 下的 PlatformRef_ 中找到 bootstrapModule 方法:
// 啟動根模塊就是調用這個
bootstrapModule<M>(
moduleType: Type<M>, // 模塊類
compilerOptions: CompilerOptions|CompilerOptions[] = [] // 編譯選項
): Promise<NgModuleRef<M>> {
return this._bootstrapModuleWithZone(moduleType, compilerOptions);
}
private _bootstrapModuleWithZone<M>(
moduleType: Type<M>, compilerOptions: CompilerOptions|CompilerOptions[] = [],
ngZone?: NgZone
): Promise<NgModuleRef<M>> {
const compilerFactory: CompilerFactory = this.injector.get(CompilerFactory); // 從注入器中得到編譯器工廠
const compiler = compilerFactory.createCompiler( // 創建出編譯器 傳入的是編譯選項 會創建一堆注入器 包括編譯器 並會將編譯器作為返回
Array.isArray(compilerOptions) ? compilerOptions : [compilerOptions]
);
return compiler.compileModuleAsync(moduleType) // 編譯模塊
.then((moduleFactory) => this._bootstrapModuleFactoryWithZone(moduleFactory, ngZone)); // 啟動模塊工廠並加入NgZone
}
里面涉及到了幾個步驟:
- 從注入器中取出前面在爸爸平台注入的 CompilerFactory
- 使用 CompilerFactory 創建出編譯器並傳入編譯選項(可以為空)
- 使用編譯器異步編譯傳入的根模塊,並在回調中執行一個名字很長的方法 _bootstrapModuleFactoryWithZone
其中 compileModuleAsync 即編譯模塊的細節涉及到了許多其他的東西,限於篇幅本文暫且不去解讀它。
現在最后剩下了一個 _bootstrapModuleFactoryWithZone 方法。此方法做的事情主要是使用傳入的模塊工廠(由異步編譯模塊得到)創建出最終的模塊來,並為其注入一個新建的NgZone實例:
private _bootstrapModuleFactoryWithZone<M>(moduleFactory: NgModuleFactory<M>, ngZone?: NgZone):
Promise<NgModuleRef<M>> {
// 創建新的NgZone實例
if (!ngZone) ngZone = new NgZone({enableLongStackTrace: isDevMode()});
return ngZone.run(() => {
const ngZoneInjector =
ReflectiveInjector.resolveAndCreate([{provide: NgZone, useValue: ngZone}], this.injector);
// 創建模塊引用(注入上面的注入器) 注入的服務商包括 NgZone 和 傳入的其他注入器
const moduleRef = <InternalNgModuleRef<M>>moduleFactory.create(ngZoneInjector);
const exceptionHandler: ErrorHandler = moduleRef.injector.get(ErrorHandler, null);
if (!exceptionHandler) {
throw new Error('No ErrorHandler. Is platform module (BrowserModule) included?');
}
moduleRef.onDestroy(() => remove(this._modules, moduleRef));
ngZone !.runOutsideAngular(() => ngZone !.onError.subscribe({next: (error: any) => {
exceptionHandler.handleError(error);
}}));
return _callAndReportToErrorHandler(exceptionHandler, ngZone !, () => {
const initStatus: ApplicationInitStatus = moduleRef.injector.get(ApplicationInitStatus);
// 執行初始化 細節待研究
initStatus.runInitializers();
return initStatus.donePromise.then(() => {
this._moduleDoBootstrap(moduleRef);
return moduleRef;
});
});
});
}
收回前面最后這兩個字,現在皮球又踢給了 _moduleDoBootstrap 方法:
private _moduleDoBootstrap(moduleRef: InternalNgModuleRef<any>): void {
// 得到app引用
const appRef = moduleRef.injector.get(ApplicationRef) as ApplicationRef;
if (moduleRef._bootstrapComponents.length > 0) {
// 啟動所有啟動組件 至於啟動組件哪里來待研究
moduleRef._bootstrapComponents.forEach(f => appRef.bootstrap(f));
} else if (moduleRef.instance.ngDoBootstrap) {
// 屬於手動執行ngDoBootstrap方法的情況
moduleRef.instance.ngDoBootstrap(appRef);
} else {
// 既沒有啟動組件又沒有手動執行啟動,則拋出錯誤
throw new Error(
`The module ${stringify(moduleRef.instance.constructor)} was bootstrapped, but it does not declare "@NgModule.bootstrap" components nor a "ngDoBootstrap" method. ` +
`Please define one of these.`);
}
// 現在模塊也啟動好了 連啟動組件都初始化好了 把這個模塊push到平台的模塊列表中吧
this._modules.push(moduleRef);
}
這里是不是有點恍然大悟,模塊啟動好后,此方法中進一步操作了啟動組件,也就是我們在AppModule中都要配置給bootstrap的入口組件:
bootstrap: [ AppComponent ]
至此模塊引用也徹底創建好了,也就是說angular項目終於是啟動成功了,當然其中模塊以及組件編譯過程還深不可測,值得細細研究。
總結
- 回顧angular項目的啟動,分為平台的創建和模塊的創建兩步
- 可以認為平台就是一個服務,平台的創建做的事情就是創建一個對象,一個注入了一大堆服務的對象
- 一個angular應用只能有一個平台,或者說此平台被視為angular應用本身,由此平台來編譯模塊,管理服務等
- 模塊的創建由於涉及到很多編譯方面的內容本文還來不及去深入,不過已知的信息是:
- 模塊由平台使用其注入的編譯器工廠生產出一個編譯器進行編譯得到
- 完成模塊的編譯后要進一步啟動模塊,啟動的方式是得到其應用引用(ApplicationRef)來啟動其啟動組件(AppComponent)