Angular 服务器端渲染 (SSR) 与 Angular Universal

2024 年 08 月 29 日 | 阅读 9 分钟

典型的 Angular 应用程序在浏览器中执行,响应用户操作在 DOM 中渲染页面。Angular Universal 在服务器上执行,生成静态应用程序页面,然后该页面在客户端进行引导。这意味着应用程序通常渲染得更快,使用户在应用程序完全交互之前就有机会看到应用程序的布局。

您可以使用 Angular CLI 轻松为应用程序准备服务器端渲染。CLI 模式 @nguniversal/express-engine 遵循必要的步骤,如下所述。

Angular Universal 需要一个活跃的 LTS 或维护 LTS 版本的 Node.js。通过查看 package.json 文件中的 engine 属性来了解当前支持的版本。

注意:下载已准备好的示例代码,该代码在 Node.js® Express 服务器中运行。

Universal 教程

英雄之旅教程是本指南的基础。

在此示例中,Angular CLI 使用 Ahead-of-Time (AOT) 编译器编译并打包 Universal 版本。Node.js Express Web 服务器根据客户端请求编译带有 Universal 的 HTML 页面。

要创建服务器端应用程序模块 app.server.module.ts,请运行以下 CLI 命令。

该命令创建以下文件夹结构。

标记为 * 的文件是新的,不在原始教程示例中。

Universal 实战

要开始在本地系统上使用 Universal 渲染您的应用程序,请使用以下命令。

content_copynpm run dev:ssr

打开浏览器并导航到 https://:4200/ 。您应该会看到熟悉的英雄之旅仪表板页面。

使用 RouterLinks 进行导航可以正常工作,因为它们使用原生的锚点 (<a>) 标签。您可以从仪表板页面导航到英雄页面,然后再返回。您可以单击它来显示仪表板页面上英雄的详细信息页面。

如果您降低网络速度,导致客户端脚本下载时间变长(说明如下),您将看到

  • 您无法添加或删除英雄。
  • 仪表板页面上的搜索框被忽略。
  • 返回详细信息页面,保存按钮无法工作。

除 RouterLinkClick 之外的用户事件不受支持。您应该等待事件运行或通过引导完整的客户端应用程序来缓冲,并使用 preboot 等库,这些库允许您在加载客户端脚本后重放这些事件。

从服务器渲染的应用程序到客户端应用程序的过渡在开发机器上很快发生,但您应该始终在真实场景中测试您的应用程序。

为了更清楚地看到过渡,您可以按如下方式模拟慢速网络

  • 打开 Chrome Dev Tools 并转到 Network 选项卡。
  • 在菜单栏最右侧找到 Network Throttling 下拉菜单。
  • 尝试其中一个“3G”速度。

服务器提供的应用程序仍然可以快速启动,但完整的客户端应用程序可能需要几秒钟才能加载。

为什么要使用服务器端渲染?

创建应用程序的 Universal 版本主要有三个原因。

  • 通过搜索引擎优化 (SEO) 促进网络爬虫
  • 提高移动和低功耗设备的性能
  • 快速显示第一页,实现首次内容绘制 (FCP)

促进网络爬虫 (SEO)

Google、Bing、Facebook、Twitter 和其他社交媒体网站依赖网络爬虫来索引您的应用程序内容,并使该内容可以在网上搜索。这些网络爬虫可能无法像人类用户那样导航和索引您高度交互的 Angular 应用程序。

Angular Universal 可以生成一个稳定的应用程序版本,该版本无需 JavaScript 即可轻松搜索、链接和导航。Universal 还提供网站预览,因为每个 URL 都会返回一个完全渲染的页面。

提高移动和低功耗设备的性能

有些设备不支持 JavaScript 或执行 JavaScript 的速度非常慢,导致用户体验无法接受。在这些情况下,您可能需要一个服务器渲染的、无 JavaScript 的应用程序版本。尽管这个版本有限,但对于那些否则无法使用该应用程序的人来说,这可能是唯一实用的选择。

快速显示第一页。

快速显示第一页对于用户参与度很重要。加载更快的页面性能更好,即使只是微小的改变(例如 100ms)。您的应用程序可能需要快速启动才能吸引这些用户,以免他们决定做其他事情。

使用 Angular Universal,您可以生成一个看起来像完整应用程序的应用程序登录页。这些页面是纯 HTML,即使禁用了 JavaScript 也能显示。页面不处理浏览器事件,但它们支持使用 RouterLink 进行站点导航。

在实际应用中,您会呈现一个静态版本的登录页来吸引用户的注意力。同时,您会在其后加载完整的 Angular 应用程序。用户几乎立即从登录页获得性能体验,并在加载完整的应用程序后获得完整的交互体验。

Universal Web 服务器

Universal Web Server 使用 Universal Template Engine 渲染的静态 HTML 来响应应用程序页面请求。服务器接收并响应来自客户端(通常是浏览器)的 HTTP 请求,并提供脚本、CSS 和图像等静态资源。它可以响应数据请求,直接响应或作为代理到单独的数据服务器。

本指南的示例 Web 服务器基于流行的 Express 框架。

注意:任何 Web 服务器技术都可以提供 Universal 应用程序,只要它能够调用 Universal 的 readerModule() 函数。这里讨论的原理和决策点适用于任何 Web 服务器技术。

Universal 应用程序使用 Angular platform-server 包(而不是 platform-browser),它提供了 DOM、XMLHttpRequest 和其他不依赖于浏览器的底层功能的服务器实现。

在本指南的示例中,服务器(本指南示例中的 Node.js Express)将客户端对应用程序页面的请求发送到 NgUniversal ngExpressEngine。在底层,它调用 Universal 的 ReaderModule() 函数,提供缓存和其他有用工具。

renderModule() 函数的输入是一个模板 HTML 页面(通常是 index.html)、一个包含组件的 Angular 模块,以及如何确定显示哪些组件。路由来自客户端对服务器的请求。

每个请求都会产生相应的视图以响应请求的路由。renderModule() 函数在模板的 <app> 标签内渲染视图,为客户端创建一个完成的 HTML 页面。

绕过浏览器 API

由于 Universal 应用程序不在浏览器中执行,服务器上可能会缺少某些浏览器 API 和功能。

例如,服务器端应用程序无法引用仅限浏览器的全局对象,如 window、document、navigator 或 location。

Angular 在这些对象上提供了一些可注入的抽象,例如 places 或 documents;它可能足以替代这些 API。如果 Angular 没有提供,则可以编写新的抽象,在服务器端(也称为 shimming)委托给浏览器 API 和替代实现,而在浏览器中也这样做。

同样,服务器端应用程序不能仅依赖用户点击按钮来显示组件,而无需鼠标或键盘事件。应用程序必须根据传入的客户端请求来确定要渲染的内容。这是一个使应用程序可运行的好论点。

Universal 模板引擎

server.ts 文件中的重要部分是 ngExpressEngine() 函数。

server.ts

初始:Universal 应用程序 ngExpressEngine() 函数是 Universal ReaderModule() 函数的包装器,它将客户端请求转换为服务器渲染的 HTML 页面。它接受一个带有以下属性的对象

Bootstrap:在服务器上渲染应用程序时引导应用程序的根 NgModule 或 NgModule 工厂。例如,对于 app 来说,它是 AppServerModule。它是 Universal 服务器端渲染器和 Angular 应用程序之间的桥梁。

Additional Providers:这是可选的,它允许您指定在服务器上渲染应用程序时应用的依赖提供程序。当您的应用程序需要仅当前运行的服务器实例可以确定的信息时,可以执行此操作。

ngExpressEngine() 函数返回一个 promise 回调,该回调在渲染页面时解析。由引擎决定如何处理该页面。此引擎的 promise 回调将渲染的页面返回给 Web 服务器,并通过 HTTP 响应将其转发给客户端。

注意:这些包装器有助于隐藏 renderModule() 函数的复杂性。Universal 存储库为不同的后端技术提供了更多包装器。

例如,--application 可以提供应用程序配置,例如 document、description 或 location。

过滤请求 URL

当使用 Universal Express 模式时,下面描述的基本行为会自动处理。这有助于理解底层行为或在不使用模式的情况下复制它。

Web 服务器应将应用程序页面请求与其他类型的请求分开。

拦截对路由地址 / 的请求并不那么简单。浏览器可能会请求应用程序路由,例如 /dashboard、/hero 或 /details:12。如果服务器只是呈现了应用程序,每次单击链接都会作为路由器的导航 URL 到达服务器。

幸运的是,应用程序路由有一些共同点:它们的 URL 没有文件扩展名。所有静态资源请求都有文件扩展名(例如 main.js 或 /node_modules/zone.js/bundles/zone.umd.js)。(数据请求也没有扩展名,但很容易识别,因为它们总是以 /api 开头。)

因为我们使用路由,所以我们可以轻松识别三种请求类型并以不同的方式处理它们。

  • 数据请求:URL 以 /api 开头的请求。
  • 应用导航:没有文件扩展名的请求 URL。
  • 静态资源:所有其他请求。

Node.js Express 服务器是一个中间件管道,它一个接一个地过滤和处理请求。您可以使用 server.get() 调用配置 Node.js Express 服务器管道,如下所示,用于数据请求。

注意:此示例服务器不处理数据请求。

教程中的“内存 Web API”模块是一个演示和开发工具,它拦截所有 HTTP 调用并模拟远程数据服务器的行为。在实际应用中,您会删除该模块并在此处注册您的 Web API 中间件。

以下代码会过滤掉没有扩展名的请求 URL,并将其视为导航请求。

安全地提供静态文件。

单个 server.use() 将所有其他 URL 视为对 JavaScript、图像和样式文件等静态资源的请求。

为了确保客户端只能下载他们被允许查看的文件,请将所有面向客户端的资源文件放在 /dist 文件夹中,并且只响应来自 /dist 文件夹的文件请求。

以下 Node.js Express 代码将所有剩余请求路由到 /dist,如果找不到文件,则返回 404 - NOT FOUND 错误。

在服务器上使用绝对 URL 进行 HTTP(数据)请求。

在服务器端渲染的应用中,HTTP URL 必须是绝对的(例如,https://my-server.com/api/heroes)。这意味着在服务器上运行时 URL 必须以某种方式转换为绝对 URL,而在浏览器中运行时则保持相对。

如果您使用的是 @nguniversal/*-engine 包之一(如 @nguniversal/express-engine),则可以自动处理。您无需执行任何操作即可使相对 URL 在服务器上正常工作。

如果您出于某种原因未使用 @nguniversal/*-engine 包,您可能需要自己处理。

推荐的解决方案是将完整的请求 URL 传递给 renderModule() 或 renderModuleFactory() 的 options 参数(取决于您用于在服务器上渲染 AppServerModule 的方法)。此选项的侵入性最小,因为它不需要对应用程序进行任何更改。此处,“请求 URL”是指应用程序在服务器上响应的请求的 URL。例如,如果客户端请求了 https://my-server.com/dashboard,而您正在服务器上提供应用程序来响应该请求,则存在选项。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 创建的构建工件,因此请确保您已运行该命令。

请注意,serve:ssr 不用于在生产环境中提供您的应用程序,仅用于在本地测试服务器端渲染的应用程序。

  • npm run prerender

此脚本可用于预渲染应用程序的页面。


下一主题Angular AWS