Angular Universal 的演進歷史

注销發表於2022-01-09

想象這樣一個場景:您已經在您的 Web 專案上工作了幾個月,這很可能是一個 Web 應用程式,更具體地說,是一個“單頁應用程式”。 但是現在是時候將您的應用程式交付併發布給數百萬使用者和……搜尋引擎了。 為了使您的應用程式成功,它必須被搜尋引擎索引,即需要新增 SEO 支援!

我們可以把 Angular Universal 理解成:Universal is Angular for the Headless Web.

您不再需要瀏覽器容器(也稱為 WebView)來執行 Angular。 由於它與 DOM 無關,因此 Angular 可以在任何有 JavaScript 執行時的地方執行,比如 Node.js.

此圖說明了 Universal 在瀏覽器之外執行典型 Angular Web 應用程式的能力。 顯然我們需要一個 JavaScript 執行時,這就是我們預設支援 Node.js(由 V8 引擎提供支援)的原因。 當然,現在也湧現出了越來越多的其他伺服器端技術,如 PHP、Java、Python、Go……

有了 Angular Universal 之後,您的應用程式可以在瀏覽器之外解釋——讓我們以伺服器為例——請求您的 SPA 的客戶端將收到所請求路由/URL 的靜態完全呈現頁面。 此頁面包含所有相關資源,即影像、樣式表、字型……甚至是透過 Angular 服務傳入的資料。

Universal 能夠重新連線一些預設的 Angular provider 實現,以便它們可以在目標平臺上工作。 當客戶端收到渲染的頁面時,它也會收到原始的 Angular 應用程式—— Angular Universal 使得應用程式在瀏覽器裡看起來幾乎是瞬間就完成了載入。 載入後,Angular 客戶端應用會處理剩下的事情。

事實上,Universal 與 Preboot.js 庫捆綁在一起,其唯一作用是確保兩個狀態同步。Preboot.js 在幕後所做的只是簡單而智慧地記錄 Angular 載入程式之前發生的事件; 並在 Angular 完成載入後對這些事件進行重播。

由於 Angular 的渲染抽象,Universal 成為可能。 事實上,當您編寫應用程式程式碼時,該邏輯會被 Angular 的編譯器解析為 AST——我們在這裡真正簡化了事情。 然後 AST 被 Angular 的渲染層使用,它使用一個不依賴於 DOM 的抽象渲染器。 Angular 允許您使用不同的渲染器。 預設情況下,Angular 附帶 DOMRenderer,因此您的應用程式可以在瀏覽器中呈現,這可能是 95% 的用例。

這就是 Universal 的用武之地。 Universal 帶有一堆預渲染器,適用於所有主流技術和構建工具。

Dependency Injection and Providers

Angular 的另一個亮點是它的 DI 系統。 事實上,Angular 是唯一實現這種設計模式的前端框架,它允許輕鬆完成如此多的偉大任務(比如控制反轉)。 多虧了 DI,您可以例如在執行時交換兩個不同的實現,這在測試中被大量使用。

在 Universal,我們利用這個 DI 系統為您提供許多特定於目標平臺的服務。 對於 Node,我們提供了一個自定義的 ServerModule,它實現了 Node 的伺服器特定 API,例如請求,而不是瀏覽器的 XHR。 Universal 還附帶了一個特定於 Node 的自定義渲染器,當然,我們為您提供了一堆預渲染器——我們稱之為——例如用於您的 Node 後端技術的 Express 渲染器或 Webpack 渲染器。 對於其他非 JavaScript 技術,例如 .NetCore 或 Java,您也應該期待其他預渲染器。

好訊息是 Universal Application 與經典的 Angular 應用程式沒有什麼不同。 應用程式邏輯實際上保持不變。

只要有可能,在直接接觸 DOM 之前請三思。 每次要與瀏覽器的 DOM 互動時,請確保使用 Angular Renderer 或渲染抽象。

下圖是 Angular Universal Application Structure.

browser.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './index';
@NgModule({
  bootstrap: [ AppComponent ],
  declarations: [ AppComponent ],
  imports: [
    BrowserModule.withServerTransition({appId: 'some-app-id'}),
    ...
  ]
})
export class AppBrowserModule {}

請注意,您需要使用 withServerTransition() 方法初始化 BrowserModule。 這將確保基於瀏覽器的應用程式將從伺服器呈現的應用程式過渡。

server.module.ts

該模組專用於您的伺服器環境。 ServerModule 提供了一組來自 @angular/platform-server 包的 provider.

import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppComponent, AppBrowserModule } from './browser.module';
@NgModule({
  bootstrap: [ AppComponent ],
  declarations: [ AppComponent ],
  imports: [
    ServerModule,
    AppBrowserModule,
    ...
  ]
})
export class AppServerModule {}

在 AppServerModule 中,您應該同時匯入 ServerModule 和 AppBrowserModule,以便它們共享相同的 appId,即 AppBrowserModule 使用的 transition ID。

client.ts

該檔案負責在客戶端引導您的應用程式。 這裡沒有什麼新東西,只是通常的引導過程(在 AOT 模式下):

import { platformBrowser } from '@angular/platform-browser';
import { AppModuleNgFactory } from './ngfactory/src/app.ngfactory';
import { enableProdMode } from '@angular/core';
enableProdMode();
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);

server.ts

此檔案確實特定於您的伺服器/後端環境。 在這裡,我們的目標是 Node.js,更準確地說是 Express 框架來處理所有客戶端請求和渲染過程。 為此,我們正在使用和註冊代表 Express 的 Angular Universal 渲染引擎的 ngExpressEngine(見下一段):

import { 
  platformServer, 
  renderModuleFactory
} from '@angular/platform-server';
import { 
  AppServerModuleNgFactory
} from './ngfactory/src/app.server.ngfactory';
import { enableProdMode } from '@angular/core';
import { AppServerModule } from './server.module';
import * as express from 'express';
import {ngExpressEngine} from './express-engine';

enableProdMode();

const app = express();

app.engine('html', ngExpressEngine({
  baseUrl: 'http://localhost:4200',
  bootstrap: [AppServerModuleNgFactory]
}));

app.set('view engine', 'html');
app.set('views', 'src')

app.get('/', (req, res) => {
  res.render('index', {req});
});

app.listen(8200,() => {
  console.log('listening...')
});

給 express 開發一個簡單的渲染器:

const fs = require('fs');
const path = require('path');
import {renderModuleFactory} from '@angular/platform-server';

export function ngExpressEngine(setupOptions){
  return function(filePath, options, callback){
    renderModuleFactory(setupOptions.bootstrap[0], {
      document: fs.readFileSync(filePath).toString(),
      url: options.req.url
    })
    .then(string => {
      callback(null, string);
    });
  }
}

這裡唯一重要的部分是 renderModuleFactory 方法。 該方法所做的基本上是將 Angular 應用程式引導到從文件解析的虛擬 DOM 樹中,並將結果 DOM 狀態序列化為字串,然後將其傳遞給 Express 引擎 API。
您當然可以向此渲染器新增一些快取機制,以避免在每次請求時從磁碟讀取。 這是一個簡單的例子:

const fs = require('fs');
const path = require('path');
import {renderModuleFactory} from '@angular/platform-server';
const cache = new Map();
export function ngExpressEngine(setupOptions){
  return function(filePath, options, callback){
    if (!cache.has(filePath)){
      const content  = fs.readFileSync(filePath).toString();
      cache.set(filePath, content);
    }
    renderModuleFactory(setupOptions.bootstrap[0], {
      document: cache.get(filePath),
      url: options.req.url
    })
    .then(string => {
      callback(null, string);
    });
  }
}

由於您可以完全控制伺服器呈現的內容,因此您可以輕鬆新增任何您想要的 SEO 支援。 我們可以想象使用@angular/platform-browser 提供的 Meta 和 Title:

import { Component } from '@angular/core';
import { Meta, Title } from "@angular/platform-browser";
@Component({
  selector: 'home-view',
  template: `<h3>Home View</h3>`
})
export class HomeView {
  constructor(seo: Meta, title: Title) {
    title.setTitle('Current Title Page');
    seo.addTags([
      {name: 'author', content: 'Wassim Chegham'},
      {name: 'keywords', content: 'angular,universal,iot,omega2+'},
      {
        name: 'description', 
        content: 'Angular Universal running on Omega2+'
      }
    ]);
  }
}

最後的效果如下:

更多Jerry的原創文章,盡在:"汪子熙":

相關文章