Angular Platforms. Part 2: The Application Bootstrap Process
Angular was built for portability across diverse execution contexts, ranging from the browser and server to web-workers and mobile runtimes. This versatility is a core design principle.
Series:
- Angular Platforms in depth. Part 1. What are Angular Platforms?
- Angular Platforms in depth. Part 2. Application bootstrap process
- Angular Platforms in depth. Part 3. Rendering Angular applications in Terminal
Every Angular app begins in main.ts:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { PlatformRef } from '@angular/core';
// Create Browser Platform
const platformRef: PlatformRef = platformBrowserDynamic();
// Bootstrap Application
platformRef.bootstrapModule(AppModule);
That snippet shows us creating a new PlatformRef instance and calling its bootstrapModule method. That call is the official starting point of an Angular app. This article will explore exactly what happens during the bootstrap sequence.
To grasp the fundamentals of platforms and their creation, refer to the first part of this series: Angular Platforms in depth. Part 1. What are Angular Platforms?
As mentioned, a bootstrap always starts with the same line of code:
platformRef.bootstrapModule(AppModule);
The entire implementation of bootstrapModule looks like this:
bootstrapModule<M>(
moduleType: Type<M>,
compilerOptions: (CompilerOptions & BootstrapOptions) | Array<CompilerOptions & BootstrapOptions> = [],
): Promise<NgModuleRef<M>> {
const options = optionsReducer({}, compilerOptions);
return compileNgModuleFactory(this.injector, options, moduleType)
.then(moduleFactory => {
const ngZoneOption = options ? options.ngZone : undefined;
const ngZone = getNgZone(ngZoneOption);
const providers: StaticProvider[] = [{ provide: NgZone, useValue: ngZone }];
return ngZone.run(() => {
const ngZoneInjector = Injector.create(
{ providers: providers, parent: this.injector, name: moduleFactory.moduleType.name });
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?');
}
const localeId = moduleRef.injector.get(LOCALE_ID, DEFAULT_LOCALE_ID);
setLocaleId(localeId);
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;
});
});
});
});
}
We will now break it down section by section.
Table of contents
- Module Compilation
- Root NgZone
- Error handling
- Initializers
- Bootstrap components
Module Compilation
Compiling the module is the first step taken when the bootstrap process kicks off.
bootstrapModule<M>(moduleType: Type<M>, options: CompilerOptions): Promise<NgModuleRef<M>> {
return compileNgModuleFactory(this.injector, options, moduleType)
.then((moduleFactory: NgModuleFactory) => {
// ...
});
}
When you execute bootstrapModule(AppModule, options) on PlatformRef, the module is compiled immediately. Here, moduleType points to the AppModule itself. The injector in question is injected via the constructor. The options variable holds the compiler options passed as the second parameter.
Let’s look closer at the compileNgModuleFactory function to see how the compilation actually works.
function compileNgModuleFactory<M>(
injector: Injector,
options: CompilerOptions,
moduleType: Type<M>
): Promise<NgModuleFactory<M>> {
const compilerFactory: CompilerFactory = injector.get(CompilerFactory);
const compiler = compilerFactory.createCompiler([options]);
return compiler.compileModuleAsync(moduleType);
}
To begin, Angular requests an instance of CompilerFactory from the injector. This abstract class creates instances of the Compiler. In dev mode, for instance, JitCompilerFactory is supplied, which produces a JitCompiler when compilerFactory.createCompiler() is invoked. The resulting compiler is then tasked with compiling AppModule.
export class JitCompiler {
private compileModuleAsync(moduleType: Type): Promise<NgModuleFactory> {
return this._loadModules(moduleType)
.then(() => {
this._compileComponents(moduleType);
return this._compileModule(moduleType);
});
}
}
In this phase, Angular loads metadata for all modules, directives, and pipes. Afterwards, all components are compiled, with Angular resolving their metadata and compiling each component's template in place. Finally, the root application module is compiled, resolving its metadata and delivering a module factory.
Once compilation completes, PlatformRef has its moduleFactory and the bootstrap sequence can proceed.
Root NgZone
Before the application can launch, a root NgZone must be created.
const ngZone = new NgZone();
ngZone.run(() => {
const moduleRef = moduleFactory.create(this.injector);
// The rest bootstrap logic
});
This root NgZone is required before AppModule creation because all application code needs to run within the zone. Also, during module creation, providers might be created eagerly, meaning the root module creation itself must be within the zone.
Only after this root NgZone is live can PlatformRef instantiate the root module using the module factory produced earlier.
Error handling
With the root NgZone active and the root module instantiated, the next step is setting up a global error handler:
// Get error handler from injector
const exceptionHandler: ErrorHandler = injector.get(ErrorHandler);
// Setup error handling outside Angular
// To make sure change-detection will not be triggered
zone.runOutsideAngular(
// Subscribe on zone errors
() => zone.onError.subscribe({
next: (error: any) => {
// Call error handler
exceptionHandler.handleError(error);
}
})
);
ErrorHandler is Angular’s mechanism for logging and responding to errors. In order to install it, PlatformRef fetches the provided ErrorHandler from the injector. It then subscribes to the root zone's error stream, calling handlerError as a response to every error event.
Interestingly, the error-handling logic itself is enveloped within zone.runOutsideAngular. This ensures that code executed inside will never trigger a change detection cycle.
Initializers
With ErrorHandler ready, the next step is running application initializers.
const initStatus: ApplicationInitStatus = moduleRef.injector.get(ApplicationInitStatus);
initStatus.runInitializers().then(() => {
// ...
});
Angular relies on the ApplicationInitStatus entity here. Initializers are specific functions that need to run prior to the app bootstrapping. For example, the web worker platform defines one such initializer:
{provide: APP_INITIALIZER, useValue: setupWebWorker, multi: true}
Thus, initializers are functions provided under the APP_INITIALIZER token. All APP_INITIALIZER tokens are inserted into ApplicationInitStatus via this injection:
constructor(@Inject(APP_INITIALIZER) private appInits: (() => any)[]) {
When runInitializers executes, it runs all of them at once, combining the outcomes with Promise.all().
Bootstrap components
Having finished all preparatory work, PlatformRef can now actually bootstrap AppComponent. Recall how a root module instance gets created:
const moduleRef = moduleFactory.create(this.injector);
Each root module must specify an array of bootstrap components:
@NgModule({
bootstrap: [AppComponent],
})
export class AppModule {}
PlatformRef then loops over these components and delegates the actual bootstrapping to ApplicationRef:
const appRef = injector.get(ApplicationRef);
moduleRef._bootstrapComponents.forEach(f => appRef.bootstrap(f));
Internally, ApplicationRef creates and renders each component:
const componentFactory =
this._componentFactoryResolver.resolveComponentFactory(component);
const compRef = componentFactory.create();
Viewers familiar with dynamic component creation will find this pattern recognizable. We can see ComponentFactoryResolver being used to get the componentFactory for AppComponent, followed by its creation.
And with that, the initial render is complete. AppComponent is now on the screen, ready to render the rest of the application.
Conclusion
That brings us to the end of the article. We have examined the complete application bootstrap sequence. We now understand all the steps necessary to build a custom platform capable of rendering Angular apps in the terminal via ASCII graphics.
For more details, don't miss the rest of the series:
- Angular Platforms in depth. Part 1. What are Angular Platforms?
- Angular Platforms in depth. Part 2. Application bootstrap process
- Angular Platforms in depth. Part 3. Rendering Angular applications in Terminal
Keep up with new Angular content and follow me on twitter for the latest updates.
