Angular 服务端渲染
Angular Universal:Angular 统一平台简介
本指南讲的是Angular Universal(统一平台),一项在服务端运行 Angular 应用的技术。
标准的 Angular 应用会运行在浏览器中,它会在 DOM 中渲染页面,以响应用户的操作。而Angular Universal 会在服务端运行,生成一些静态的应用页面,稍后再通过客户端进行启动。这意味着该应用的渲染通常会更快,让用户可以在应用变得完全可交互之前,先查看应用的布局。
要了解 SSR 的其它技术和概念的详细信息,请参阅这篇文章。
可以使用 Angular CLI
来轻松为应用做好服务端渲染的准备。CLI 的 @nguniversal/express-engine
模板会执行如下必要步骤。
Angular Universal 需要活跃 LTS 或 维护中 LTS版本的 Node.js。参见 package.json 文件中的
engines
属性,以了解当前支持的版本。
注意:
下载已完成的范例代码,并将其运行在一个 Node.js® Express 服务器中。
Universal 教程
这次演练的基础是“英雄之旅”教程。
在这个例子中,Angular CLI 使用 预先(AoT)编译器编译并打包了该应用的 Universal 版本。Node.js Express Web 服务器则会根据客户端的请求,利用 Universal 编译 HTML 页面。
要创建服务端应用模块 app.server.module.ts
,请运行以下 CLI 命令。
ng add @nguniversal/express-engine
该命令会创建如下文件夹结构。
标有 *
的文件都是新增的,不在原始的教程范例中。
Universal 实战
要使用 Universal 在本地系统中渲染你的应用,请使用如下命令。
npm run dev:ssr
打开浏览器,导航到 http://localhost:4200
。你会看到熟悉的“英雄之旅”仪表盘页面。
通过 routerLinks
导航时能正常工作,因为它们使用的是内置的链接元素(<a>
)。你可以从仪表盘进入 英雄列表页面,然后返回。你可以点击仪表盘页面上的一个英雄来显示他的详情页面。
如果你限制下网速(稍后会讲操作步骤),让客户端脚本下载时间变长,你会注意到:
- 你无法添加或删除英雄
- 仪表盘页面上的搜索框会被忽略
- “详情”页面上的后退和保存按钮不起作用
不支持除了点击 routerLink
以外的任何用户事件。你必须等待完整的客户端应用启动并运行,或者使用 preboot 之类的库来缓冲这些事件,这样你就可以在客户端脚本加载完毕后重放这些事件。
在开发机器上,从服务端渲染的应用过渡到客户端应用的过程会很快,但是你还是应该在实际场景中测试一下你的应用。
你可以通过模拟速度较慢的网络来更清晰地看到这种转换,如下所示:
- 打开 Chrome 开发者工具,进入 Network 标签页。
- 找一下菜单栏最右侧的 Network Throttling 下拉菜单。
- 尝试一下 “3G” 的速度吧。
服务端渲染的应用仍然可以快速启动,但完整的客户端应用可能需要几秒钟才能加载完。
为何需要服务端渲染?
有三个主要的理由来为你的应用创建一个 Universal 版本。
- 通过搜索引擎优化(SEO)来帮助网络爬虫。
- 提升手机和低功耗设备上的性能
- 迅速显示出第一个支持首次内容绘制(FCP)的页面
帮助网络爬虫(SEO)
Google、Bing、Facebook、Twitter 和其它社交媒体网站都依赖网络爬虫去索引你的应用内容,并且让它的内容可以通过网络搜索到。 这些网络爬虫可能不会像人类那样导航到你的具有高度交互性的 Angular 应用,并为其建立索引。
Angular Universal 可以为你生成应用的静态版本,它易搜索、可链接,浏览时也不必借助 JavaScript。它也让站点可以被预览,因为每个 URL 返回的都是一个完全渲染好的页面。
提升手机和低功耗设备上的性能
有些设备不支持 JavaScript 或 JavaScript 执行得很差,导致用户体验不可接受。对于这些情况,你可能会需要该应用的服务端渲染的、无 JavaScript 的版本。虽然有一些限制,不过这个版本可能是那些完全没办法使用该应用的人的唯一选择。
快速显示第一页
快速显示第一页对于吸引用户是至关重要的。加载速度更快的页面效果更好,即使其差异只有 100 毫秒也是如此(https://web.dev/shopping-for-speed-on-ebay/)。你的应用要启动得更快一点,以便在用户决定做别的事情之前吸引他们的注意力。
使用 Angular Universal,你可以为应用生成“着陆页”,它们看起来就和完整的应用一样。这些着陆页是纯 HTML,并且即使 JavaScript 被禁用了也能显示。这些页面不会处理浏览器事件,不过它们可以用 [routerLink]
(guide/router-reference#router-link)
在这个网站中导航。
在实践中,你可能要使用一个着陆页的静态版本来保持用户的注意力。同时,你也会在幕后加载完整的 Angular 应用。用户会觉得着陆页几乎是立即出现的,而当完整的应用加载完之后,又可以获得完整的交互体验。
Universal Web 服务器
Universal Web 服务器使用 Universal 模板引擎渲染出的静态 HTML 来响应对应用页面的请求。 服务器接收并响应来自客户端(通常是浏览器)的 HTTP 请求,并回复静态文件,如脚本、CSS 和图片。 它可以直接响应数据请求,也可以作为独立数据服务器的代理进行响应。
这个例子中的范例 Web 服务器是基于常见的 Express 框架的。
注意:
任何一种 Web 服务器技术都可以作为 Universal 应用的服务器,只要它能调用 Universal 的 renderModule()
函数。 这里所讨论的这些原则和决策点也适用于任何 Web 服务器技术。
Universal 应用使用 platform-server
包(而不是 platform-browser
),它提供了 DOM 的服务端实现、XMLHttpRequest
以及其它不依赖浏览器的底层特性。
服务器(这个例子中使用的是 Node.js Express 服务器)会把客户端对应用页面的请求传给 NgUniversal 的 ngExpressEngine
。在内部实现上,它会调用 Universal 的 renderModule()
函数,它还提供了缓存等有用的工具函数。
renderModule()
函数接受一个模板 HTML 页面(通常是 index.html
)、一个包含组件的 Angular 模块和一个用于决定该显示哪些组件的路由作为输入。 该路由从客户端的请求中传给服务器。
每次请求都会给出所请求路由的一个适当的视图。renderModule()
在模板中的 <app>
标记中渲染出这个视图,并为客户端创建一个完成的 HTML 页面。
最后,服务器就会把渲染好的页面返回给客户端。
使用浏览器 API
由于 Universal 应用并没有运行在浏览器中,因此该服务器上可能会缺少浏览器的某些 API 和其它能力。
比如,服务端应用不能引用浏览器独有的全局对象,比如 window
、document
、navigator
或 location
。
Angular 提供了一些这些对象的可注入的抽象层,比如 Location
或 DOCUMENT
,它可以作为你所调用的 API 的等效替身。如果 Angular 没有提供它,你也可以写一个自己的抽象层,当在浏览器中运行时,就把它委托给浏览器 API,当它在服务器中运行时,就提供一个符合要求的代用实现(也叫垫片 - shimming)。
同样,由于没有鼠标或键盘事件,因此 Universal 应用也不能依赖于用户点击某个按钮来显示某个组件。Universal 应用必须仅仅根据客户端过来的请求决定要渲染的内容。把该应用做成可路由的,就是一种好方案。
Universal 模板引擎
server.ts
文件中最重要的部分是 ngExpressEngine()
函数。
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
ngExpressEngine()
是对 Universal 的 renderModule()
函数的封装。它会把客户端请求转换成服务端渲染的 HTML 页面。它接受一个具有下列属性的对象:
属性 |
详情 |
---|---|
bootstrap
|
在服务器上渲染时用于引导应用程序的根 |
extraProviders
|
这是可选的,可以让你指定仅在服务器渲染应用程序时才适用的依赖提供者。当你的应用需要某些只能由当前运行的服务器实例确定的信息时,可以执行此操作。 |
ngExpressEngine()
函数返回了一个会解析成渲染好的页面的承诺(Promise)。接下来你的引擎要决定拿这个页面做点什么。在这个引擎的 Promise
回调函数中,把渲染好的页面返回给了 Web 服务器,然后服务器通过 HTTP 响应把它转发给了客户端。
注意:
这个包装器帮助隐藏了 renderModule()
的复杂性。 在 Universal 代码库中还有更多针对其它后端技术的包装器。
过滤请求的 URL
注意:
当使用 NgUniversal Express 原理图时,将自动处理稍后描述的基本行为。当你要尝试理解其底层行为或在不使用原理图的情况下自行实现它时,这一节会很有用。
Web 服务器必须把对应用页面的请求和其它类型的请求区分开。
这可不像拦截对根路径 /
的请求那么简单。浏览器可以请求应用中的任何一个路由地址,比如 /dashboard
、/heroes
或 /detail:12
。事实上,如果应用只会通过服务器渲染,那么应用中点击的任何一个链接都会发到服务器,就像导航时的地址会发到路由器一样。
幸运的是,应用的路由具有一些共同特征:它们的 URL 一般不带文件扩展名。(数据请求也可能缺少扩展名,但是它们很容易识别出来,因为它们总是以 /api
开头,所有的静态资源的请求都会带有一个扩展名,比如 main.js
或 /node_modules/zone.js/dist/zone.js
)。
由于使用了路由,所以我们可以轻松的识别出这三种类型的请求,并分别处理它们。
路由请求类型 |
详情 |
---|---|
数据请求 |
请求的 URL 用 |
应用导航 |
请求的 URL 不带扩展名。 |
静态资产 |
所有其它请求。 |
Node.js Express 服务器是一系列中间件构成的管道,它会挨个对 URL 请求进行过滤和处理。你可以调用 app.get()
来配置 Express 服务器的管道,就像下面这个数据请求一样。
// TODO: implement data requests securely
server.get('/api/**', (req, res) => {
res.status(404).send('data requests are not yet supported');
});
注意:
这个范例服务器不会处理数据请求。
本教程的“内存 Web API” 模块(一个演示及开发工具)拦截了所有 HTTP 调用,并且模拟了远端数据服务器的行为。在实践中,你应该移除这个模块,并且在服务器上注册你的 Web API 中间件。
下列代码会过滤出不带扩展名的 URL,并把它们当做导航请求进行处理。
// All regular routes use the Universal engine
server.get('*', (req, res) => {
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});
安全的提供静态文件
单独的 server.use()
会处理所有其它 URL,比如对 JavaScript 、图片和样式表等静态资源的请求。
要保证客户端只能下载那些允许他们访问的文件,你应该把所有面向客户端的资源文件都放在 /dist
目录下,并且只允许客户端请求来自 /dist
目录下的文件。
下列 Node.js Express 代码会把剩下的所有请求都路由到 /dist
目录下,如果文件未找到,就会返回 404 - NOT FOUND
。
// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));
在服务端使用绝对 URL 进行 HTTP(数据)请求
本教程的 HeroService
和 HeroSearchService
都委托 Angular 的 HttpClient
模块来获取应用数据。这些服务会向 api/heroes
之类的相对 URL 发送请求。在服务端渲染的应用中,HTTP URL 必须是绝对的(比如,https://my-server.com/api/heroes
)。这意味着当在服务器上运行时,URL 必须以某种方式转换为绝对 URL,而在浏览器中运行时,它们是相对 URL。
如果你正在使用 @nguniversal/*-engine
包之一(比如 @nguniversal/express-engine
),就会自动为帮你做这件事。你无需再做任何事情来让相对 URL 能在服务器上运行。
如果出于某种原因,你没有使用 @nguniversal/*-engine
包,你可能需要亲自处理它。
建议的解决方案是将完整的请求 URL 传给 renderModule()
或 renderModuleFactory()
的 options
参数(具体取决于你在服务器上渲染 AppServerModule
的目的)。此选项的侵入性最小,因为它不需要对应用进行任何更改。这里的“请求 URL” 是指当应用在服务器上渲染时的地址。比如,如果客户端请求了 https://my-server.com/dashboard
并且要在服务器上渲染该应用以响应该请求,那么 options.url
应设置为 https://my-server.com/dashboard
。
现在,作为在服务端渲染应用的一部分,每次发送 HTTP 请求时,Angular 都可以使用这里提供的 options.url
正确地将请求 URL 解析为绝对 URL。
实用脚本
脚本 | 详情 |
npm run dev:ssr |
此命令类似于 ng serve ,它在开发期间提供实时重新加载,但使用服务器端渲染。该应用程序以监视模式运行并在每次更改后刷新浏览器。这个命令要比实际的 ng serve 命令慢。 |
ng build && ng run app-name:server |
此命令会在生产模式下构建服务器脚本和应用程序。当你要构建用于部署的项目时,请使用此命令。 |
npm run serve:ssr |
注意:此命令启动服务器脚本,用于通过服务器端渲染在本地为应用程序提供服务。它使用由 ng run build:ssr 创建的构建工件,因此请确保你也运行了该命令。 |
npm run prerender |
此脚本可用于预先渲染应用程序的页面。 |