想象这样一个场景:您已经在您的 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: `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+'
}
]);
}
}
最后的效果如下: