Angular|Angular Universal 的演进历史

想象这样一个场景:您已经在您的 Web 项目上工作了几个月,这很可能是一个 Web 应用程序,更具体地说,是一个“单页应用程序”。 但是现在是时候将您的应用程序交付并发布给数百万用户和……搜索引擎了。 为了使您的应用程序成功,它必须被搜索引擎索引,即需要添加 SEO 支持!
我们可以把 Angular Universal 理解成:Universal is Angular for the Headless Web.
您不再需要浏览器容器(也称为 WebView)来运行 Angular。 由于它与 DOM 无关,因此 Angular 可以在任何有 JavaScript 运行时的地方运行,比如 Node.js.
Angular|Angular Universal 的演进历史
文章图片

此图说明了 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|Angular Universal 的演进历史
文章图片

【Angular|Angular Universal 的演进历史】只要有可能,在直接接触 DOM 之前请三思。 每次要与浏览器的 DOM 交互时,请确保使用 Angular Renderer 或渲染抽象。
下图是 Angular Universal Application Structure.
Angular|Angular Universal 的演进历史
文章图片

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|Angular Universal 的演进历史
文章图片

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

    推荐阅读