Angular

Часть 1, Часть 2

Фреймворк Angular был задуман платформенно-независимым. Такой подход позволяет запускать Angular-приложения в разных средах: в браузере, сервере, веб-воркере и даже на мобильных устройствах.

В данной серии статей я опишу, как это вообще возможно — запускать Angular-приложения в разных средах. Мы также научимся создавать пользовательскую платформу Angular, с помощью которой можно визуализировать приложения из терминала, используя графику ASCII.

Любое Angular-приложение начинается с файла main.ts :

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { PlatformRef } from '@angular/core';
 
 
// Создать браузерную платформу
const platformRef: PlatformRef = platformBrowserDynamic();
 
// Начальная загрузка приложения
platformRef.bootstrapModule(AppModule);

Здесь мы создаём новый экземпляр PlatformRef и затем вызываем метод bootstrapModule . Это то место, где запускается Angular-приложение. В этой статье мы попробуем разобраться, как происходит процесс начальной загрузки приложения.

Как было сказано выше, любое Angular-приложение начинается со следующего вызова:

platformRef.bootstrapModule(AppModule)

Вот полный алгоритм метода bootstrapModule:

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

Обсудим его шаг за шагом.

Содержание

  • Компиляция модуля
  • Корневая Angular-зона
  • Обработка ошибок
  • Инициализаторы
  • Компоненты загрузки

Компиляция модуля

Первый этап в процессе начальной загрузки приложения — это компиляция модуля. 

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

Когда мы вызываем метод bootstrapModule(AppModule, options) на PlatformRef , прежде всего, он компилирует этот модуль. Здесь moduleType относится к AppModule. Инжектор – это всего лишь экземпляр Injector, внедрённый через конструктор. А опции есть опции компилятора, задействованные в качестве второго аргумента в методе bootstrapModule.

Чтобы узнать больше о процессе компиляции модуля, рассмотрим поподробнее функцию compileNgModuleFactory.

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

Во-первых, Angular извлекает экземпляр CompilerFactory из инжектора. CompilerFactory — это абстрактный класс, отвечающий за создание экземпляра Compiler. Например, когда мы запускаем Angular-приложение в режиме разработки, то в качестве реализации выступает JitCompilerFactory . Тогда в результате вызова функции compilerFactory.createCompiler() создаётся такая JitCompiler реализация для компилятора Compiler . Затем этот compiler запрашивается для компилирования нашего AppModule.

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

Здесь Angular загружает все модули, директивы и конвейеры метаданных. Затем компилирует все компоненты. Во время проведения компиляции компонентов идёт поиск всех метаданных компонентов, зарегистрированных в приложении. Потом Angular обращается к компилятору, чтобы скомпилировать шаблоны всех имеющихся компонентов. Последнее, что нам надо здесь сделать, — это скомпилировать корневой модуль приложения. На этом этапе Angular распознает все требующиеся метаданные для модуля и возвращает фабрику модуля.

Когда компиляция модуля завершается, PlatformRef получает moduleFactory и может начать процесс начальной загрузки.

Root NgZone

Прежде чем осуществлять загрузку Angular-приложения, PlatformRef нужно создать root NgZone.

const ngZone = new NgZone();

ngZone.run(() => {
  
  const moduleRef = moduleFactory.create(this.injector);
  // Остальная логика начальной загрузки
});

Root NgZone должен быть инстанцирован ещё до создания AppModule, потому что нам нужно поместить всю логику приложения в эту зону. Между тем, во время создания Angular-модули могут, в свою очередь, активно создавать некоторые провайдеры. Вот почему даже логика создания корневого модуля должна быть помещена внутрь этой зоны.

И только когда создан root NgZone, PlatformRef может инстанцировать корневой модуль через фабрику корневого модуля, появившуюся в качестве результата на этапе компиляции модуля.

Обработка ошибок

Когда root NgZone создан и корневой модуль уже инстанцирован, самое время настроить глобальную обработку ошибок:

// Получить обработчик ошибок из инжектора
const exceptionHandler: ErrorHandler = injector.get(ErrorHandler);
 
// Настроить обработку ошибок вне Angular
// Убедиться, что обнаружение изменений не запустится 
zone.runOutsideAngular(
 
  // Отслеживать ошибки зоны
  () => zone.onError.subscribe({
    next: (error: any) => {
 
      // Вызвать обработчик ошибок
      exceptionHandler.handleError(error);
    }
  })
);

ErrorHandler отвечает в Angular за правильную регистрацию и обработку ошибок. Чтобы настроить ErrorHandler, PlatformRef должен извлечь имеющийся ErrorHandler из инжектора, затем отслеживать поток ошибок из корневой зоны и осуществлять вызов метода handlerError, таким образом реагируя на каждое событие ошибки.

Обратите внимание: вся логика обработки ошибок заключена в функцию zone.runOutsideAngular. Эта функция гарантирует, что любой код, выполняемый внутри, не приведёт к запуску обнаружения изменений.

Инициализаторы

Когда проведена настройка ErrorHandler, самое время запустить инициализаторы приложения.

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

Здесь Angular использует элемент ApplicationInitStatus, чтобы запустить инициализаторы приложения. Инициализаторы приложения – это функции, исполнение которых требуется непосредственно перед загрузкой приложения. Например, платформа веб-вокера имеет следующий инициализатор:

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

Таким образом, инициализаторы приложения — это всего лишь функции, представленные токеном APP_INITIALIZER. Здесь все токены APP_INITIALIZER, внедрённые в ApplicationInitStatus, используют следующий оператор:

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

Когда вызывается метод runInitializers, он просто выполняет все инициализаторы приложения и возвращает результат, используя Promise.all().

Компоненты загрузки

На этом этапе PlatformRef завершается со всеми приготовлениями и уже готов к выполнению загрузки AppComponent! Если помните, мы ранее уже видели, как создавался экземпляр корневого модуля:

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

Любой корневой Angular-модуль должен содержать массив загрузочных компонентов:

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

PlatformRef просто выполняет перебор значений массива загрузочных компонентов и обращается к ApplicationRef, чтобы осуществить загрузку каждого компонента:

const appRef = injector.get(ApplicationRef);

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

ApplicationRef внутри лишь создаёт и визуализирует компоненты:

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

Приведённый выше алгоритм должен быть знаком тем из вас, кто создавал динамические Angular-компоненты. Здесь примечательно то, как был использован ComponentFactoryResolver для того, чтобы распознатьcomponentFactory для AppComponent, а затем просто создать его.

Вот и всё! Мы сделали это! На данном этапе у нас на экран выведен AppComponent, благодаря которому будут визуализированы все остальные части приложения.

Заключение

Поздравляем! Вы добрались до конца статьи, в которой мы вместе прошли процесс начальной загрузки приложения. И теперь у нас есть все необходимые знания, чтобы начать создавать собственную платформу, с помощью которой можно визуализировать Angular-приложения из терминала, используя графику ASCII.

Читайте также:


Перевод статьи Nikita Poltoratsky: Angular Platforms in depth. Part 2. Application bootstrap process