Часть 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.
Читайте также:
- Веселимся с Angular и трансформаторами в TypeScript
- Визуализация данных и веб-отчёты в Angular
- Переиспользование форм в Angular
Перевод статьи Nikita Poltoratsky: Angular Platforms in depth. Part 2. Application bootstrap process