首页 > 其他分享 >基于 Angular Universal 引擎进行服务器端渲染的前端应用 State Transfer 故障排查案例

基于 Angular Universal 引擎进行服务器端渲染的前端应用 State Transfer 故障排查案例

时间:2023-11-17 20:23:14浏览次数:40  
标签:const 服务器端 渲染 Transfer Universal TransferState Angular 客户端

笔者之前这篇掘金文章一个 SAP 开发工程师的 2022 年终总结:四十不惑 提到,我目前的团队,负责开发一款基于 Angular 框架的电商 Storefront 应用。

这个 Storefront 是一个开源的、基于 Angular 和 Bootstrap 并为 SAP Commerce Cloud 构建的 Angular 应用程序。

图1:Spartacus Storefront 的 home page

我们都知道,在电商领域里,搜索引擎优化 (Search Engine Optimization,SEO) 对任何一个 Storefront 来说都是至关重要的,它可以使电商网站更容易被搜索引擎检索到。

然而,迄今为止,许多搜索引擎的爬虫在解析和索引网站内容时,还没有办法完全解析 Angular 这种单页面应用(SPA-Single Page Application) 在浏览器端渲染的 HTML 内容。因此在电商领域,使用 Angular + Universal 引擎来开启应用的服务器端渲染,几乎成了一种标配,我们团队负责开发的 Spartacus 也不例外。

最近我在工作中处理了几例客户反馈的关于 Angular 应用在服务器端渲染下的 State Transfer 故障的处理,特将其中之一摘录出来供广大 Angular 开发同仁参考。

什么是 Angular Universal

Angular Universal 是 Angular 的服务端渲染(Server-Side Rendering,SSR)解决方案。

传统的 Angular 应用都是单页应用(SPA),所有的视图渲染都在客户端完成。当用户访问一个 SPA 网站时,服务器只会发送一个包含整个应用代码的 JavaScript 文件,然后在用户的浏览器中运行这个 JavaScript 文件来生成网页内容。这就意味着,用户在访问网页的初期可能会遇到一个空白页面,需要等待 JavaScript 文件下载、解析和运行完成后才能看到完整的网页内容。

相比之下,服务端渲染的应用,在服务器上进行渲染,完成网页静态内容 HTML 的生成工作 ,然后将这个 HTML 发送给用户。这样,用户在访问网页的初期就能看到完整的网页内容,不需要等待 JavaScript 文件下载、解析和运行。这种方式可以提高首屏加载速度,改善用户体验,同时对于搜索引擎优化 SEO 也更友好。

Angular Universal 就是 Angular 提供的一种服务端渲染解决方案。它通过在服务器上运行 Angular 应用来生成静态 HTML,然后将这个 HTML 发送给用户。当用户在浏览器中接收到这个 HTML 后,Angular 会接管网页,将其升级为一个完整的 SPA。下图是 Angular Universal 官方文档的截图:

图2:Angular Universal 官方文档

下图是 Spartacus 应用没有开启服务器端渲染的效果,在 Chrome 开发者工具 Network 标签页里,我们能观察到,cx-storefront 这个元素里只有 loading... 这个占位符

图3:CSR(Client Side Render)模式下的 Spartacus 首页渲染请求

再来比较 Spartacus 开启了服务器端渲染之后的效果。显然,下图绿色高亮区域里的 HTML 内容,就是在服务器端完成渲染并返回到客户端的静态内容。

图4:SSR(Server Side Render)模式下的 Spartacus 首页渲染请求

Spartacus 服务器端渲染的入口逻辑定义在 server.ts 文件内:


import { APP_BASE_HREF } from '@angular/common';
import { ngExpressEngine as engine } from '@nguniversal/express-engine';
import {
  defaultSsrOptimizationOptions,
  NgExpressEngineDecorator,
  SsrOptimizationOptions,
} from '@spartacus/setup/ssr';
import { Express } from 'express';
import { existsSync } from 'fs';
import { join } from 'path';
import 'zone.js/node';
import { AppServerModule } from './src/main.server';

const express = require('express');

const ssrOptions: SsrOptimizationOptions = {
  timeout: Number(
    process.env['SSR_TIMEOUT'] ?? defaultSsrOptimizationOptions.timeout
  ),
};

const ngExpressEngine = NgExpressEngineDecorator.get(engine, ssrOptions);

export function app() {
  const server: Express = express();
  const distFolder = join(process.cwd(), 'dist/storefrontapp');
  const indexHtml = existsSync(join(distFolder, 'index.original.html'))
    ? 'index.original.html'
    : 'index';

  server.set('trust proxy', 'loopback');

  server.engine(
    'html',
    ngExpressEngine({
      bootstrap: AppServerModule,
    })
  );

  server.set('view engine', 'html');
  server.set('views', distFolder);

  server.get(
    '*.*',
    express.static(distFolder, {
      maxAge: '1y',
    })
  );

  server.get('*', (req, res) => {
    res.render(indexHtml, {
      req,
      providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],
    });
  });

  return server;
}

function run() {
  const port = process.env['PORT'] || 4000;

  // Start up the Node server
  const server = app();
  server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
  });
}

// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = (mainModule && mainModule.filename) || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
  run();
}

export * from './src/main.server';

其中下图第 62 行高亮的代码块,就是 Angular Universal 引擎在服务器端渲染 HTML 页面的入口和核心。

图5:Spartacus 调用 Angular Universal 引擎在服务器端渲染的入口代码

为什么 Angular 服务器端渲染应用需要 State Transfer

要回答这个问题,我们首先要弄清楚什么是 State Transfer.

在 Angular Universal 中,State Transfer 主要是指在服务器端渲染完成后,将服务器端的状态传递给客户端的过程。这样可以避免客户端重新获取和计算已经在服务器端获取和计算过的数据,从而提高应用的性能。

具体来说,State Transfer 是通过 TransferState 服务来实现的。TransferState 服务提供了一种在服务器端和客户端之间共享状态的方式。在服务器端,你可以将一些数据存储到 TransferState 中,然后在客户端,你可以从 TransferState 中取出这些数据。

举个例子,假设你的 Angular 应用需要从服务器获取一些数据然后显示在视图中。在没有使用 Angular Universal 的情况下,当用户打开网页时,浏览器首先需要下载和运行 JavaScript 代码,然后 JavaScript 代码会向服务器发送请求获取数据,最后再将数据显示在视图中。这个过程可能会比较慢,因为需要等待 JavaScript 代码下载和运行,以及等待服务器响应数据请求。

但是,如果你使用了 Angular Universal 和 TransferState 服务,那么这个过程就会快很多。当服务器接收到用户的请求时,它会运行 Angular 应用,并向服务器发送数据请求,然后将获取的数据存储到 TransferState 中并生成视图,最后将视图和 TransferState 一起发送给客户端。当客户端接收到服务器的响应时,它不需要再向服务器发送数据请求,而是直接从 TransferState 中取出数据,然后将数据显示在视图中。这样就大大减少了首次加载页面的时间。

以上就是 Angular Universal 中的 State Transfer 工作的概要介绍。下面我们看看这个机制在 Spartacus 工作中的实际例子。

以 Spartacus product category 页面为例,相对 url 为:

/electronics-spa/en/USD/Open-Catalogue/Cameras/Digital-Cameras/c/575

当在 CSR 模式下渲染时,返回请求页面的 Size 连 1KB 都不到,原因之前已经说了,cx-storefront 元素内只有一个 loading... 的占位符,其内容是当 Angular 客户端 Bootstrap 之后,在浏览器里完成填充的。

图6:Spartacus 产品 category 页面在 CSR 模式下的返回结果

再看相同的页面在 Spartacus 开启了服务器端渲染后的行为。整个请求的 size 达到了 288 kb.

图7:Spartacus 产品 category 页面在 SSR 模式下的返回结果

究其原因,本章节介绍的 State Transfer 就贡献了很大一部分的数据规模。

我们在 SSR 模式下服务器返回给浏览器的 HTML response 里,根据关键字 app-state 进行搜索,找到一个 id 为 spartacus-app-statescript 标签元素。

图8:Spartacus 服务器端渲染后 HTML 里包含的 State Transfer 数据

这个 script 元素的类型为 application/json,里面包含的值就是 Angular 应用在服务器端渲染时,调用的 AJAX 请求从 API 服务器获取的业务数据,通过 State Transfer 将这些数据序列化成 JSON 格式。

我们随便在 UI 上找一些产品业务数据,都可以在 spartacus-app-state 这个 script 元素里找到对应的 State 数据。下图是一个例子:

图9:Spartacus CSR 从 spartacus-app-state script 元素中提取 state 数据

实际业务中的故障

尽管产品 Category 页面从服务器端返回的结果,从上图9能看出,已经在 spartacus-app-state 这个 script 元素里,包含了所有的产品业务数据,但是当 Angular 应用在客户端 bootstrap 并重新渲染时,我们仍然能够在 Chrome 开发者工具的 Network 面板里,观察到一个重复的 product search API 请求:

图10:在 Angular 客户端不必要的 Product search API 请求

显然这个请求是毫无必要,应该避免的:

我们在调试器里观察一下客户端发起这个请求的上下文:

发现是在 ProductSearchService 这个 Service 类里发起的请求。

于是,我们可以通过扩展这个 Service 类的方式,来修复这个故障。

我们编写下面的 TypeScript 代码:

export class CustomProductSearchService extends ProductSearchService {
  transferState = inject(TransferState, { optional: true });
  isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
  isHydrated = false;
  results$ = new Subject<ProductSearchPage>();
  override search(query: string | undefined, searchConfig?: SearchConfig) {
    if (this.isBrowser && !this.isHydrated) {
      const state = this.transferState?.get(CX_KEY, {} as StateWithProduct)!;
      const results = state[PRODUCT_FEATURE].search.results;
      this.results$.next(results);
      this.isHydrated = true;
      return;
    }
    super.search(query, searchConfig);
  }
  override getResults() {
    return merge(super.getResults(), this.results$);
  }
}

const CX_KEY = makeStateKey<StateWithProduct>('cx-state');

  图13:修复客户端渲染发出多余 API 请求的实现代码
  1. 这个故障修复的思路是,首先在 Angular 中扩展了 Spartacus 标准的ProductSearchService 服务类,然后重载(override)其 search 方法。

  transferState = inject(TransferState, { optional: true });

这一行注入了一个名为 TransferState 的服务,用于在服务器端渲染(SSR)和浏览器之间传递状态。TransferState 是 Angular Universal 的一部分,{ optional: true } 参数的意思是,如果无法找到 TransferState 服务,也不会报错。

  isBrowser = isPlatformBrowser(inject(PLATFORM_ID));

这一行检测当前代码是否在浏览器环境中运行。PLATFORM_ID 是 Angular 提供的一个令牌,它在运行时会被替换为一个特定平台的 ID,isPlatformBrowser 是一个函数,如果当前 Angular 应用运行在浏览器环境里,那么这个函数会返回 true

  isHydrated = false;

这一行声明了一个布尔类型的标志位 isHydrated,初始化为 false。这个标志位用来追踪是否已经从 TransferState 中恢复了状态。

  results$ = new Subject<ProductSearchPage>();

这一行创建了一个新的 RxJS SubjectSubject 是 RxJS 中的一种特殊类型的 Observable,它可以发出新的值,并将这些值推送给所有订阅者。

  1. 重载标准服务类的 search 方法。
  override search(query: string | undefined, searchConfig?: SearchConfig) {

这一行是 search 方法的声明,这是一个覆写了父类中的同名方法的方法。这个方法接受一个查询字符串和一个可选的搜索配置对象作为参数。

    if (this.isBrowser && !this.isHydrated) {

这一行检查当前代码是否在浏览器环境中运行,并且还没有从 TransferState 中恢复状态。

      const state = this.transferState?.get(CX_KEY, {} as StateWithProduct)!;

这一行从 TransferState 中获取状态。CX_KEY 是状态的键,如果在 TransferState 中找不到这个键,就会返回一个空对象。

      const results = state[PRODUCT_FEATURE].search.results;

这一行从恢复的状态中获取搜索结果。

      this.results$.next(results);

这确保了初始页面加载时,无需再次请求数据,直接使用服务器端渲染的数据。否则,在其他情况下,会调用父类 ProductSearchService 的 search 方法执行产品搜索。

最后,CX_KEY 是一个用于标识状态转移的键,它在服务器端和客户端之间共享,以确保状态正确转移和匹配。这个键由 makeStateKey 方法创建,用于唯一标识特定的状态。在服务器端渲染过程中,该键用于查找和提取状态,然后在客户端渲染时将其应用。

当首次加载页面时,CustomProductSearchService 的 search 方法会在服务器端执行,从服务器端渲染的状态中提取产品搜索结果。

这些搜索结果将被发送到 results$ Subject 中。

当页面在客户端加载完成后,CustomProductSearchService 的 getResults 方法被调用,合并了服务器端渲染的结果和客户端请求的结果,以确保搜索结果的一致性。

这样,用户在浏览器中浏览页面时,无需再次请求数据,而是直接使用服务器端渲染的结果。

这段代码的核心思想是通过状态转移机制,在服务器端渲染的情况下尽早提供数据,以加速页面加载并提高用户体验。在客户端渲染时,保持状态的一致性,以确保用户获得一致的数据。这对于需要 SEO 支持的 Angular 应用非常重要,因为它确保了搜索引擎爬虫能够获取完整的页面内容。

总结

本文首先介绍了电商 Web 应用开发领域引入 Angular Universal 实现服务器端渲染的必要性,接着介绍了 State Transfer 这种避免客户端渲染时重复调用 AJAX 从服务器获取业务数据的一种行业最佳实践,最后通过实际项目中一个 State Transfer 实现出现故障的案例,介绍了此类故障的分析和解决问题的详细思路,希望对广大 Angular 开发同仁有所借鉴作用。

标签:const,服务器端,渲染,Transfer,Universal,TransferState,Angular,客户端
From: https://www.cnblogs.com/sap-jerry/p/17839590.html

相关文章

  • nginx keepalive 设置避免 服务器端大量time_wait 增加tcp 连接重用
    #Formoreinformationonconfiguration,see:#*OfficialEnglishDocumentation:http://nginx.org/en/docs/#*OfficialRussianDocumentation:http://nginx.org/ru/docs/usernginx;worker_processesauto;error_log/var/log/nginx/error.log;pid/run/......
  • HTTP 响应字段 Transfer-Encoding 赋值成 chunked 的作用介绍
    Transfer-Encoding:chunked是HTTP/1.1协议中定义的一种数据传输方式。在HTTP/1.1之前,HTTP协议的响应数据通常是一次性发送的,也就是说,服务器必须把所有的响应数据准备好后,一次性发送给客户端。这种方式的缺点是,如果响应数据很大,或者数据的产生需要花费一定的时间,那么服务器......
  • 掌握Linux:查看服务器端口号的实用指南
    当你管理一个Linux服务器时,了解服务器上正在运行的服务以及它们使用的端口是至关重要的。这可以帮助你确保服务正常运行,定位问题,以及提高服务器的安全性。在这篇博客文章中,我将向你介绍如何使用Linux命令来查看服务端口号。查看所有打开的端口要查看服务器上所有打开的端口,可以使......
  • HTTP 响应字段 Transfer-Encoding 的作用介绍
    Transfer-Encoding字段是HTTP响应头部的一部分,用于指示在传输响应正文(responsebody)时所使用的传输编码方式。在HTTP通信中,响应正文可以以多种不同的编码方式传输,其中一种方式是chunked传输编码。本文将详细介绍Transfer-Encoding字段的含义和chunked传输编码,以及提供示例来解释这......
  • 客户端首屏渲染时首先拿到空的html模板,之后继续发起数据请求。而服务器端渲染只需要请
    客户端首屏渲染时首先拿到空的html模板,之后继续发起数据请求。而服务器端渲染只需要请求一次,服务器会将请求的数据放在html模板中一起返回。服务器端渲染耗费流量,局部页面的变化也需要重新请求完整的页面客户端渲染就可以采用SPA,能实现局部组件的更新,服务器端渲染回来的就是整个......
  • Spartacus 服务器端渲染(SSR)的 timeout 设置
    如下图所示,SpartacusSSRengine的几种timeout超时机制的设置:其中第122行的3_000写法,意思就是默认的3000毫秒超时时间。在官网能看到对于这些timeout字段的说明:timeout的设置是一个数字,指示SSR服务器在回退到CSR默认的渲染机制之前,尝试呈现页面的时间量(以毫秒......
  • Angular 服务器端渲染的静态 HTML 变为客户端的动态应用的过程
    首先,让我们先了解一下Angular服务器端渲染(SSR)的工作原理。当你的Angular应用启用服务器端渲染后,用户在浏览器中请求页面时,服务器会预先渲染出HTML,并且将其发送到客户端。这样做的优点是可以改善首屏加载时间,提升SEO效果,因为搜索引擎可以抓取到预渲染的HTML内容。那......
  • Angular 应用启用服务器端渲染后 Ngrx store 和 re-hydration 的交互关系
    在Angular启用服务器端渲染(Server-SideRendering,SSR)后,当浏览器端访问这个Angular应用时,会涉及到一系列过程,包括初始化、数据获取、hydration(重新注水)和与NgRxStore之间的交互。下面我将详细介绍这些步骤:初始化应用:用户在浏览器中输入应用的URL。服务器端处理请求,生......
  • Angular 服务器端渲染应用 re-hydration 过程详解
    当使用Angular启用服务器端渲染(Server-SideRendering,以下简称SSR)时,应用程序的工作方式发生了显著变化。这使得Angular应用更加友好,不仅对搜索引擎爬虫更友好,还有助于改善应用的性能和加载时间。在本文中,我们将详细介绍在浏览器端访问启用SSR的Angular应用时背后发生的事情,特别侧......
  • sse_server sent event_eventSource_websocket替代_socketio替代_服务器端事件
    eventsourcebackend#-*-coding:utf-8-*-#这段代码是使用FastAPI框架创建一个简单的服务器端事件(Server-SentEvents,SSE)的示例。以下是对代码的详细解析:#1.`importjson,random,...`:这行代码导入了需要的Python模块。#2.`event_router=APIRouter()`:这行代码创建......