Часть 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.
Специально для сайта ITWORLD.UZ. Новость взята с сайта NOP::Nuances of programming