AngularInDepth уходит от Medium. Эта статья, ее обновления и более свежие статьи размещены на новой платформе inDepth.dev

В официальной документации Angular указано, что для начальной загрузки приложения вы должны поместить в файл main.ts следующее:

platformBrowserDynamic().bootstrapModule(AppModule);

Первая часть заявления platformBrowserDynamic() создает платформу. Документы Angular описывают платформу как:

точка входа для Angular на веб-странице. Каждая страница имеет ровно одну платформу, а службы (такие как отражение), общие для каждого приложения Angular, запущенного на странице, связаны в ее области действия.

Angular также имеет концепцию запущенного экземпляра приложения, которую вы обычно можете внедрить с помощью токена ApplicationRef. На одной платформе потенциально может быть много приложений. Каждое приложение создается из модуля с использованием метода bootstrapModule. Это именно тот метод, который используется в main.ts. Таким образом, оператор, показанный в документации, сначала создает платформу, а затем экземпляр приложения.

Когда приложение создается, Angular проверяет свойство bootstrap модуля, используемого для начальной загрузки приложения (AppModule):

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

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

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

NgDoBootstrap

Представьте, что у нас есть два компонента A компонент и B компонент. Мы решим во время выполнения, какой из них следует использовать в приложении. Давайте определим эти два компонента:

import { Component } from '@angular/core';

@Component({
  selector: 'a-comp',
  template: `<span>I am A component</span>`
})
export class AComponent {}

@Component({
  selector: 'b-comp',
  template: `<span>I am B component</span>`
})
export class BComponent {}

И прописываем их в AppModule:

@NgModule({
  imports: [BrowserModule],
  declarations: [AComponent, BComponent],
  entryComponents: [AComponent, BComponent]
})
export class AppModule {}

Здесь важно то, что мы не регистрируем их в свойстве bootstrap, так как мы будем загружать их вручную. Также мы должны зарегистрировать их в entryComponents, поскольку мы хотим, чтобы компилятор создавал для них фабрики. Angular автоматически добавляет все компоненты, указанные в свойстве bootstrap, к компонентам записи, поэтому обычно вы не добавляете корневой компонент в entryComponents.

Кроме того, поскольку мы не знаем, будет ли использоваться компонент A или B, мы не указываем эти селекторы в index.html, поэтому теперь это выглядит так:

<body>
  <h1 id="status">
     Loading AppComponent content here ...
  </h1>
</body>

Теперь, если вы запустите приложение сейчас, вы получите следующую ошибку:

Модуль AppModule был загружен, но он не объявляет ни компоненты «@ NgModule.bootstrap», ни метод «ngDoBootstrap». Пожалуйста, определите одно из этих

Так что в основном Angular жалуется, что мы не указали, какой компонент следует использовать для начальной загрузки. И мы не знаем заранее. Позже мы будем загружать приложение вручную, и для этого нам нужно добавить метод ngDoBoostrap в класс AppModule:

export class AppModule {
  ngDoBootstrap(app) {  }
}

Angular передает этому методу ссылку на запущенное приложение в виде ApplicationRef. Позже, когда мы будем готовы к загрузке приложения, мы будем использовать метод bootstrap из ApplicationRef для инициализации корневого компонента.

Давайте определим собственный метод bootstrapRootComponent, который будет отвечать за загрузку корневого компонента, когда он станет доступным:

// app - reference to the running application (ApplicationRef)
// name - name (selector) of the component to bootstrap
function bootstrapRootComponent(app, name) {
  // define the possible bootstrap components 
  // with their selectors (html host elements)
  const options = {
    'a-comp': AComponent,
    'b-comp': BComponent
  };
  // obtain reference to the DOM element that shows status
  // and change the status to `Loaded`
  const statusElement = document.querySelector('#status');
  statusElement.textContent = 'Loaded';
  // create DOM element for the component being bootstrapped
  // and add it to the DOM
  const componentElement = document.createElement(name);
  document.body.appendChild(componentElement);
  // bootstrap the application with the selected component
  const component = options[name];
  app.bootstrap(component);
}

Требуется ссылка на ApplicationRef и имя компонента для начальной загрузки. Кроме того, мы определили карту options со всеми возможными компонентами начальной загрузки. Селектор компонентов действует как ключ. Мы будем использовать его для поиска класса компонента, когда получим необходимую информацию с сервера.

Я также создал фиктивную fetch функцию, которая имитирует HTTP запрос к серверу и возвращает b-comp селектор в течение 2 секунд:

function fetch(url) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('b-comp');
    }, 2000);
  });
}

Итак, теперь у нас есть bootstrap функция, которая запускает корневой компонент, давайте используем ее в ngDoBootstrap методе модуля:

export class AppModule {
  ngDoBootstrap(app) {
    fetch('url/to/fetch/component/name')
      .then((name)=>{ this.bootstrapRootComponent(app, name)});
  }
}

Вот и все. Вот пример stackblitz, демонстрирующий решение.

Это работает с AOT?

Да, конечно. Вам просто нужно предварительно скомпилировать все ваши компоненты и использовать фабрики при загрузке приложения:

import {AComponentNgFactory, BComponentNgFactory} from './components.ngfactory.ts';
@NgModule({
  imports: [BrowserModule],
  declarations: [AComponent, BComponent]
})
export class AppModule {
  ngDoBootstrap(app) {
    fetch('url/to/fetch/component/name')
      .then((name)=>{ this.bootstrapRootComponent(app, name)});
  }
  bootstrapRootComponent(app, name) {
    const options = {
      'a-comp': AComponentNgFactory,
      'b-comp': BComponentNgFactory
    };

Обратите внимание, что нам не нужно указывать компоненты в entryComponents, поскольку у нас уже есть фабрики, и нам не нужно их компилировать.

Спасибо за прочтение! Если вам понравилась эта статья, нажмите кнопку хлопка под 👏. Это очень много значит для меня и помогает другим людям увидеть историю.

Чтобы узнать больше, подпишитесь на меня в Twitter и Medium.