你知道 Angular Universal 吗?这可是让网站获得更好 SEO 支持的一大利器。

通常情况下,Angular 应用是在浏览器中运行的,直接在 DOM 上渲染页面并与用户交互。而 Angular Universal 则是在服务端完成渲染(也就是我们常说的 Server-Side Rendering,SSR),生成静态的应用程序网页,再交给客户端展示。这么做的好处很明显——页面渲染速度更快,能在提供完整交互之前,先把内容展示给用户。
话说在前头,本文的示例是基于 Angular 14 环境完成的,新版本中的部分细节可能有所不同,建议结合 Angular 官方文档来参考。
使用 SSR 的好处
对 SEO 更加友好
虽然包括 Google 在内的某些搜索引擎和社交媒体,都声称能对由 JavaScript 驱动的单页应用(SPA)进行内容爬取,但从实际效果来看,结果往往差强人意。静态 HTML 网站的 SEO 表现,依然要明显优于依赖动态渲染的网站。这一点,Angular 官方也持有同样的看法——别忘了,Angular 可是 Google 亲生的。
通过 Universal,我们可以生成无 JavaScript 的静态版本应用,这对于搜索爬虫、外部链接导航来说,支持得更加到位。
提高移动端的性能
部分移动设备可能不支持或仅有限地支持 JavaScript,这直接导致网站访问体验大打折扣。在这样的场景下,提供一个无 JS 版本的应用,就显得尤为必要了。
更快地展示首页
用户对首屏加载速度的容忍度极低。根据 eBay 的数据,搜索结果展示速度每提升 100 毫秒,用户“加入购物车”的使用率就能提高 0.5%。这个数字足够说明问题了。
有了 Universal,应用的首页会以完整形态的纯 HTML 网页呈现给用户。即便浏览器不支持 JavaScript,页面内容也照样能展示出来。虽然此时网页还不能处理浏览器事件,但通过 routerLink 进行页面导航是没问题的。
这一方案的精妙之处在于:先用静态页面抓住用户的注意力,在他们浏览页面的同时,后台悄无声息地加载整个 Angular 应用,带给用户一种极速加载的体验。
为项目增加 SSR
Angular CLI 提供了一条捷径,能轻松地将一个普通 Angular 项目转换为支持 SSR 的版本。只需要一条命令:
ng add @nguniversal/express-engine
建议在运行这个命令之前,先把所有改动提交到版本控制里。这个命令会对项目做以下修改:
添加服务端文件:
main.server.ts- 服务端主程序文件app/app.server.module.ts- 服务端应用程序主模块tsconfig.server.json- TypeScript 服务端配置文件server.ts- Express web server 的运行文件
修改的文件:
package.json- 添加 SSR 所需的依赖和运行脚本angular.json- 添加开发和构建 SSR 应用所需的配置
替换浏览器 API
由于 Universal 应用并不在浏览器环境中执行,因此一些浏览器专属的 API 或功能会失效。最典型的就是服务端无法使用 window、document、navigator、location 这些全局对象。
Angular 为此专门提供了两个可注入对象,用于在服务端环境中替代这些浏览器对象:Location 和 DOCUMENT。
举个例子,在浏览器中我们通常通过 window.location.href 来获取当前地址。换成 SSR 之后,代码就变成这样:
import { Location } from '@angular/common';
export class AbmNavbarComponent implements OnInit{
// ctor 中注入 Location
constructor(private _location:Location){
//...
}
ngOnInit() {
// 打印当前地址
console.log(this._location.path(true));
}
}
同样的,如果在浏览器中使用 document.getElementById() 获取 DOM 元素,改成 SSR 之后是这样:
import { DOCUMENT } from '@angular/common';
export class AbmFoxComponent implements OnInit{
// ctor 中注入 DOCUMENT
constructor(@Inject(DOCUMENT) private _document: Document) { }
ngOnInit() {
// 获取 id 为 fox-container 的 DOM
const container = this._document.getElementById('fox-container');
}
}
使用 URL 绝对地址
在 Angular SSR 应用中,HTTP 请求的 URL 地址必须是绝对地址(即以 http/https 开头,不能是 /api/heros 这样的相对地址)。官方推荐的做法是把请求的完整 URL 路径设置到 renderModule() 或 renderModuleFactory() 的 options 参数中。不过在 v14 自动生成的代码里,并没有显式调用这两个方法的代码。但通过拦截 HTTP 请求,也能达到同样的效果。
下面先准备一个拦截器,假设这个文件位于项目的 shared/universal-relative.interceptor.ts 路径:
import { HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Inject, Injectable, Optional } from '@angular/core';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { Request } from 'express';
// 忽略大小写检查
const startsWithAny = (arr: string[] = []) => (value = '') => {
return arr.some(test => value.toLowerCase().startsWith(test.toLowerCase()));
};
// http, https, 相对协议地址
const isAbsoluteURL = startsWithAny(['http', '//']);
@Injectable()
export class UniversalRelativeInterceptor implements HttpInterceptor {
constructor(@Optional() @Inject(REQUEST) protected request: Request) { }
intercept(req: HttpRequest, next: HttpHandler) {
// 不是绝对地址的 URL
if (!isAbsoluteURL(req.url)) {
let protocolHost: string;
if (this.request) {
// 如果注入的 REQUEST 不为空,则从注入的 SSR REQUEST 中获取协议和地址
protocolHost = `${this.request.protocol}://${this.request.get('host')}`;
} else {
// 如果注入的 REQUEST 为空,比如在进行 prerender build:
// 这里需要根据实际情况添加自定义的地址前缀
protocolHost = 'https://www.example.com';
}
const pathSeparator = !req.url.startsWith('/') ? '/' : '';
const url = protocolHost + pathSeparator + req.url;
const serverRequest = req.clone({ url });
return next.handle(serverRequest);
} else {
return next.handle(req);
}
}
}
然后在 app.server.module.ts 文件中把它 provide 出来:
import { UniversalRelativeInterceptor } from './shared/universal-relative.interceptor';
// ... 其他 imports
@NgModule({
imports: [
AppModule,
ServerModule,
// 如果你用了 @angular/flext-layout,这里也需要引入服务端模块
FlexLayoutServerModule,
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: UniversalRelativeInterceptor,
multi: true
}
],
bootstrap: [AppComponent],
})
export class AppServerModule { }
这样一来,任何针对相对地址的请求都会被自动转换为绝对地址请求,在 SSR 场景下就不会再出问题了。
Prerender 预渲染静态 HTML
经过上面的步骤,如果我们通过 npm run build:ssr 构建项目,你会发现 dist/ 下面只有一个 index.html 文件。打开这个文件,会发现它里面仍然有 这样的元素,页面内容并没有直接在 HTML 中生成。这是因为 Angular 使用的是动态路由,比如 /product/:id 这种形式,页面的渲染结果需要在执行 JS 之后才能确定。因此,Angular 使用 Express 作为 Web 服务器,在运行时根据用户请求(比如爬虫请求),通过模板引擎动态生成静态 HTML 界面。
而 prerender(通过 npm run prerender)则会在构建时就生成静态 HTML 文件。比如做一个企业官网,页面数量不多,那就很适合用预渲染技术,把这几个页面的静态 HTML 文件提前生成好,避免运行时再做动态生成,从而进一步提升网页的访问速度和用户体验。
预渲染路径配置
需要进行预渲染的网页路径,有几种提供方式:
1. 通过命令行的附加参数:
ng run:prerender --routes /product/1 /product/2
2. 如果路径比较多,比如针对 product/:id 这种动态路径,可以准备一个路径文件:
routes.txt
/products/1 /products/23 /products/145 /products/555
然后在命令行参数中指定该文件:
ng run:prerender --routes-file routes.txt
3. 在项目的 angular.json 文件中直接配置路径:
"prerender": {
"builder": "@nguniversal/builders:prerender",
"options": {
"routes": [ // 这里配置
"/",
"/main/home",
"/main/service",
"/main/team",
"/main/contact"
]
},
配置完成后,重新执行预渲染命令(npm run prerender 或上面提到的带命令行参数的方式)。编译完成后,再打开 dist/ 下的 index.html,你会发现 消失了,取而代之的是主页的实际内容。同时也会生成相应的路径目录以及各个目录下的 index.html 子页面文件。
SEO 优化
SEO 的关键在于网页的 title、keywords 和 description。对于希望被搜索引擎收录的页面,我们需要在代码中提供这些内容。
在 Angular 14 中,如果路由界面是通过 Routes 配置的,可以直接将网页的静态 title 写在路由配置里:
{ path: 'home', component: AbmHomeComponent, title: '<你想显示在浏览器 tab 上的标题>' },
另外,Angular 也提供了可注入的 Title 和 Meta 服务,用于动态修改网页的标题和 meta 信息:
import { Meta, Title } from '@angular/platform-browser';
export class AbmHomeComponent implements OnInit {
constructor(
private _title: Title,
private _meta: Meta,
) { }
ngOnInit() {
this._title.setTitle('<此页的标题>');
this._meta.addTags([
{ name: 'keywords', content: '<此页的 keywords,以英文逗号隔开>' },
{ name: 'description', content: '<此页的描述>' }
]);
}
}
总结
Angular 作为一款企业级 SPA 开发框架,在模块化组织和团队协作开发方面,有着自己独特的优势。发展到 v14 这个版本,更是提供了不依赖 NgModule 的独立 Component 功能,进一步简化了模块化的架构。
Angular Universal 的核心目标,是将 Angular 应用进行服务端渲染和生成静态 HTML。对于用户交互复杂的 SPA,其实并不推荐使用 SSR。但对于页面数量较少、又有 SEO 需求的网站或系统,用上 Universal 和 SSR 技术,绝对是一个明智的选择。
