The Angular framework was designed with flexibility in mind. That approach allows Angular applications to be executed across different environments — browser, server, web-worker, and even mobile devices are possible.

In this series of articles, I’m going to reveal to you how does it even possible — execute Angular applications across different environments. Also, we’ll learn how to build custom Angular platform which renders applications inside the system’s terminal using ASCII graphics.

Articles:


Each Angular application starts with the main.ts file:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { PlatformRef } from '@angular/core';
 
 
// Create Browser Platform
const platformRef: PlatformRef = platformBrowserDynamic();
 
// Bootstrap Application
platformRef.bootstrapModule(AppModule);

Here, you could notice that we’re creating a new instance of PlatformRef and then calling bootstrapModule method on it. It’s a place where Angular application starts. In this article, we’re going to dive deeper into the application bootstrap process.

If you want to learn more on what are Angular platforms and how are they created, please, check previous article from the series: Angular Platforms in depth. Part 1. What are Angular Platforms?

As I stated previously, each Angular application starts with the following call:

platformRef.bootstrapModule(AppModule);

Here is the full code of bootstrapModule method:

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;
            });
          });
        });
      });
  }

Let’s discuss it step by step.

Table of contents#

  • Module Compilation
  • Root NgZone
  • Error handling
  • Initializers
  • Bootstrap components

Module Compilation#

The first step in the application bootstrap process is a module compilation.

bootstrapModule<M>(moduleType: Type<M>, options: CompilerOptions): Promise<NgModuleRef<M>> {
    
  return compileNgModuleFactory(this.injector, options, moduleType)
    .then((moduleFactory: NgModuleFactory) => {
  
      // ...
    });
}

So, when we’re calling bootstrapModule(AppModule, options) on PlatformRef first of all, it compiles that module. Here, moduleType refers to the AppModule. The injector is just an instance of Injector injected through constructor. And options are compiler options provided as the second argument into bootstrapModule method.

Let’s dive deeper at the compileNgModuleFactory function to learn more about the module compilation process.

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);
}

First of all, Angular retrieves an instance of CompilerFactory from the injector. CompilerFactory is an abstract class which is responsible for the creation of an instance of the Compiler. For instance, when we’re starting Angular application in dev mode, then JitCompilerFactory implementation will be provided. Then, JitCompilerimplementation of the Compiler will be created as a result of compilerFactory.createCompiler() function call. Then, compiler is asked to compiler our AppModule.

export class JitCompiler {
  
  private compileModuleAsync(moduleType: Type): Promise<NgModuleFactory> {
  
    return this._loadModules(moduleType)
      .then(() => {
        this._compileComponents(moduleType);
        return this._compileModule(moduleType);
      });
  }
}

Here, Angular loads all the modules, directives and pipes metadata. Then, it compiles all the components. During the compilation of the components, it searches for all components metadata registered in the application, then asks the compiler to compile all component’s templates in place. The last thing we need to do here is to actually compile the root application module. On that stage, Angular resolves all the required metadata for the module and return module factory.

When module compilation is done, PlatformRef has moduleFactory and can start the bootstrap process.

Root NgZone#

Before actually bootstrapping Angular application PlatformRef need to create a root NgZone.

const ngZone = new NgZone();

ngZone.run(() => {
  
  const moduleRef = moduleFactory.create(this.injector);
  // The rest bootstrap logic
});

Root NgZone has to be instantiated even before AppModule creation, because we need to wrap all the application logic inside the zone. Meanwhile, during creation Angular modules may create some providers eagerly, that’s why even root module creation logic has to be wrapped into the zone.

And only when the root NgZone created PlatformRef could instantiate the root module through the root module factory created as a result of the module compilation step.

Error handling#

When root NgZone created and root module already instantiated it’s time to set 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 in Angular, is responsible for proper errors logging and handling. So, to set up ErrorHandlerPlatformRef need to retrieve provided ErrorHandler from the injector. Then, subscribe to errors stream from the root zone and call handlerError method as a reaction for each error event.

But look, all the error handling logic is wrapped in zone.runOutsideAngular function. That function makes sure that any code executed inside will never trigger a change detection run.

Initializers#

When ErrorHandler set up, it’s a time to run application initializers.

const initStatus: ApplicationInitStatus = moduleRef.injector.get(ApplicationInitStatus);
initStatus.runInitializers().then(() => {
  // ...
});

Here, Angular uses ApplicationInitStatus entity to run application initializers. Application initializers are just functions, which execution required to be done right before the application bootstrap. For instance, the web worker platform has the following initializer:

{provide: APP_INITIALIZER, useValue: setupWebWorker, multi: true}

So, application initializers are just functions that provided under APP_INITIALIZER token. Then, all APP_INITIALIZERtokens injected into ApplicationInitStatus using the following statement:

constructor(@Inject(APP_INITIALIZER) private appInits: (() => any)[]) {

When runInitializers method called it just execute all the app initializers and return the result using Promise.all().

Bootstrap components#

Ok, on that stage PlatformRef is done with all preparations and ready to actually bootstrap AppComponent! As you remember, above we’ve seen how to root module instance was created:

const moduleRef = moduleFactory.create(this.injector);

Each root Angular module has to contain bootstrap components array:

@NgModule({
  bootstrap: [AppComponent],
})
export class AppModule {}

PlatformRef then just iterates through that bootstrap components array and asks ApplicationRef to actually bootstrap each component:

const appRef = injector.get(ApplicationRef);

moduleRef._bootstrapComponents.forEach(f => appRef.bootstrap(f));

ApplicationRef inside just creates and renders components:

const componentFactory =
  this._componentFactoryResolver.resolveComponentFactory(component);
  
const compRef = componentFactory.create();

The code above has to be familiar for those of you who created some Angular components dynamically. Here, we could notice how ComponentFactoryResolver utilized to resolve componentFactory for AppComponent and then just creates it.

And that’s it! We did it! On that stage, we have AppComponent rendered on the screen, which will render all the rest parts of the application.


Conclusion#

Congratulations 🥳 you’ve reached the end of the article. During the article, we went through the application bootstrap process. And now we have all the required knowledge to start building our custom platform which will render Angular applications inside the system’s terminal using ASCII graphics.

If you want to get deeper knowledge on Angular platforms, take a look at the rest articles of the series:

Also, follow me on twitter to be notified about new Angular articles as soon as possible!