Angular Universal 的演进历史

想象这样一个场景:您已经在您的 Web 项目上工作了几个月,这很可能是一个 Web 应用程序,更具体地说,是一个“单页应用程序”。 但是现在是时候将您的应用程序交付并发布给数百万用户和……搜索引擎了。 为了使您的应用程序成功,它必须被搜索引擎索引,即需要添加 SEO 支持!

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

您不再需要浏览器容器(也称为 WebView)来运行 Angular。 由于它与 DOM 无关,因此 Angular 可以在任何有 JavaScript 运行时的地方运行,比如 Node.js.

Angular Universal 的演进历史_第1张图片

此图说明了 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 应用程序没有什么不同。 应用程序逻辑实际上保持不变。

Angular Universal 的演进历史_第2张图片

只要有可能,在直接接触 DOM 之前请三思。 每次要与浏览器的 DOM 交互时,请确保使用 Angular Renderer 或渲染抽象。

下图是 Angular Universal Application Structure.

Angular Universal 的演进历史_第3张图片

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: `

Home View

` }) 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+' } ]); } }

最后的效果如下:

Angular Universal 的演进历史_第4张图片

更多Jerry的原创文章,尽在:"汪子熙":
Angular Universal 的演进历史_第5张图片

你可能感兴趣的