[譯] 如何手動啟動 Angular 程式

lx1036發表於2018-03-23

原文連結:How to manually bootstrap an Angular application

Angular 官方文件寫到,為了啟動 Angular 程式,必須在 main.ts 檔案裡寫上如下程式碼:

platformBrowserDynamic().bootstrapModule(AppModule);
複製程式碼

這行程式碼 platformBrowserDynamic() 是為了構造一個 platform,Angular 官方文件對 platform 的定義是(譯者注:為清晰理解,platform 定義不翻譯):

the entry point for Angular on a web page. Each page has exactly one platform, and services (such as reflection) which are common to every Angular application running on the page are bound in its scope.

同時,Angular 也有 執行的程式例項(running application instance) 的概念,你可以使用 ApplicationRef 標記(token)作為引數注入從而獲取其例項。上文的 platform 定義也隱含了一個 platform 可以擁有多個 application 物件,而每一個 application 物件是通過 bootstrapModule 構造出來的,構造方法就像上文 main.ts 檔案中使用的那樣。所以,上文的 main.ts 檔案中程式碼,首先構造了一個 platform 物件和一個 application 物件。

application 物件被正在構造時,Angular 會去檢查模組 AppModulebootstrap 屬性,該模組是用來啟動程式的:

@NgModule({
  imports: [BrowserModule],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}
複製程式碼

bootstrap 屬性通常包含用來啟動程式的元件(譯者注:即根元件),Angular 會在 DOM 中查詢並匹配到該啟動元件的選擇器,然後例項化該啟動元件。

Angular 啟動過程隱含了你想要哪一個元件去啟動程式,但是如果啟動程式的元件是在執行時才被定義的該怎麼辦呢?當你獲得該元件時,又該如何啟動程式呢?事實上這是個非常簡單的過程。

NgDoBootstrap

假設我們有 AB 兩個元件,將編碼決定執行時使用哪一個元件來啟動程式,首先讓我們定義這兩個元件吧:

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 屬性中註冊這兩個元件,並且通過在 entryComponents 註冊元件,Angular 編譯器(譯者注:Angular 提供了 @angular/compiler 包用來編譯我們寫的 angular 程式碼,同時還提供了 @angular/compiler-cli CLI 工具)會為這兩個元件建立工廠類(譯者注:Angular Compiler 在編譯每一個元件時,會首先把該元件類轉換為對應的元件工廠類,即 a.component.ts 被編譯為 a.component.ngfactory.ts)。因為 Angular 會自動把在 bootstrap 屬性中註冊的元件自動加入入口元件列表,所以通常不需要把根元件註冊到 entryComponents 屬性中。(譯者注:即在 bootstrap 屬性中註冊的元件不需要在 entryComponents 中重複註冊)。

由於我們不知道 A 還是 B 元件會被使用,所以沒法在 index.html 中指定選擇器,所以 index.html 看起來只能這麼寫(譯者注:我們不知道服務端返回的是 A 還是 B 元件資訊):

<body>
  <h1 id="status">
     Loading AppComponent content here ...
  </h1>
</body>
複製程式碼

如果此時執行程式會有如下錯誤:

The module AppModule was bootstrapped, but it does not declare “@NgModule.bootstrap” components nor a “ngDoBootstrap” method. Please define one of these

錯誤資訊告訴我們, Angular 在向抱怨我們沒有指定具體使用哪一個元件來啟動程式,但是我們的確不能提前知道(譯者注:我們不知道服務端何時返回什麼)。等會兒我們得手動在 AppModule 類中新增 ngDoBootstrap 方法來啟動程式:

export class AppModule {
  ngDoBootstrap(app) {  }
}
複製程式碼

Angular 會把 ApplicationRef 作為引數傳給 ngDoBootstrap(譯者注:參考 Angular 原始碼中這一行),等會準備啟動程式時,使用 ApplicationRefbootstrap 方法初始化根元件。

讓我們寫一個自定義方法 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` 
  //(譯者注:改變 id 為 #status 的內容)
  const statusElement = document.querySelector('#status');
  statusElement.textContent = 'Loaded';
  // create DOM element for the component being bootstrapped
  // and add it to the DOM
  // (譯者注:建立一個 DOM 元素)
  const componentElement = document.createElement(name);
  document.body.appendChild(componentElement);
  // bootstrap the application with the selected component
  const component = options[name];
  app.bootstrap(component); // (譯者注:使用 bootstrap() 方法啟動元件)
}
複製程式碼

傳入該方法的引數是 ApplicationRef 和啟動元件的名稱,同時定義變數 options 來對映所有可能的啟動元件,並以元件選擇器作為 key,當我們從伺服器中獲取所需要資訊後,再根據該資訊查詢是哪一個元件類。

先構建一個 fetch 方法來模擬 HTTP 請求,該請求會在 2 秒後返回 B 元件選擇器即 b-comp 字串:

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

現在我們擁有 bootstrap 方法來啟動元件,在 AppModule 模組的 ngDoBootstrap 方法中使用該啟動方法吧:

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

這裡我做了個 stackblitz demo 來驗證該解決方法。(譯者注:譯者把該作者 demo 中 angular 版本升級到最新版本 5.2.9,可以檢視 angular-bootstrap-process,2 秒後會根據服務端返回資訊自定義啟動 application

在 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 屬性中註冊元件,因為我們已經有了元件的工廠類了,沒必要再通過 Angular Compiler 去編譯元件獲得元件工廠類了。(譯者注:components.ngfactory.ts 是由 Angular AOT Compiler 生成的,最新 Angular 版本 在 CLI 裡隱藏了該資訊,在記憶體裡臨時生成 xxx.factory.ts 檔案,不像之前版本可以通過指令物理生成這中間臨時檔案,儲存在硬碟裡。)

相關文章