当前位置: 首页>后端>正文

Angular universal服务器端渲染与预渲染

  • 1. 前言
  • 2. 什么是Angular universal
  • 3. 为什么需要SSR(服务器端渲染)
  • 4. Angular Universal如何解决FCP和SEO问题
  • 5. 开启SSR
  • 6. 开启客户端水合(Client Hydration)
  • 7. 使用 Universal 构建和运行
  • 8. Prerender 预渲染静态 HTML
    • 8.1. 预渲染路径配置
  • 9. SEO 优化
    • 关键词与描述的优化
    • 内部跳转优化
    • 样式文件打包
    • 添加robots.txt
    • 自动生成sitemap
  • 10. 使用Nginx部署
    • 10.1. 安装Nginx
    • 10.2. 安装Node
    • 10.3. 安装PM2
    • 10.4. 配置并启动pm2
    • 10.5. 配置Nginx反向代理
  • 11. troubleshooting
    • 问题1
    • 问题2
    • 问题3
    • 问题4
  • 12. 后记
  • 13. 参考文档

1. 前言

当初选择将应用做成SPA(单页应用)的时候主要是觉得用户体验非常丝滑, 当时也知道SPA很难做SEO, 还是毅然决然的选择做成SPA应用. 当时还是Angularjs 1.X的时候, 就觉得Angular的理念跟自己对前端的看法特别契合, 后来将框架升级到Angular 11继而13, 虽然费了很多时间和精力, 但是收获非常多, 由于本文的重点是SSR与prerendering,所以这里不赘述原因了. 之前也了解到Angular Universal是做服务器端渲染的套件(SSR), 乘最近有空刚好将其引入到项目. 实现地过程中虽然遇到问题, 但是还是有些小兴奋的感觉, 一来解决了首次访问应用时白屏的问题, 二来将当初打算舍弃的SEO能力也找了回来, 而且整个对引入SSR 实现SEO的过程还是相当轻松的, 特写此文, 以防遗忘, 也希望给后来者有所帮助.

2. 什么是Angular universal

Angular universal是一个用于服务器端渲染Angular应用程序的框架. 它允许在服务器上生成HTML, 以便在浏览器中更快地呈现应用程序, 这对于提高应用程序的性能和搜索引擎优化(SEO)非常有用. 在使用Angular Universal时, 应用程序的初始加载时间可能会增加, 但是在浏览器中呈现应用程序的速度会更快, 因为大部分工作已经在服务器上完成了.

3. 为什么需要SSR(服务器端渲染)

在深入了解Angular universal之前, 我们需要了解一下SSR(服务器端渲染), 一项技术的出现并流行, 一定是它解决了一类问题或者是解决了一些痛点. 那么SSR(Server Side Rendering)的出现解决了哪些痛点呢相对于MPA(Multiple Page Application)风格来说,SPA这种架构风格有很多的优点,但是也存在非常明显两个的缺点, 而SSR技术的出现就是为了解决这两个痛点的. 根据本文的主题SPA的优缺点列举如下, 比如:

  • 更快的用户体验
  • 更好的交互性
  • 更好的可维护性

SPA架构风格也存在两个非常大的痛点:

  • 初始加载时间较长

未经优化的SPA应用在进行首次内容绘制(FCP - First Contentful Paint)时会存在非常严重的首页白屏问题, 由于用户首次访问SPA时需要加载大量的JS代码, 而且要经过解析后才能开始渲染页面, 整个过程需要耗费大量的时间, 往往会大大超过用户原意等待时间3秒, 而且随着应用的功能增加问题越来越严重.

  • SEO不友好

SPA不利于搜索引擎的抓取, 由于搜索引擎使用网络爬虫来索引网页, 这些爬虫依赖HTML内容来理解网页的结构和内容. 然而, 在SPA中, 内容是由JavaScript动态生成的, 这意味着发送到浏览器的初始HTML文件通常为空或包含非常少的内容. 这使得搜索引擎难以正确索引页面, 因为它们可能无法看到由Javascript生成的内容. 强如google这样的索引擎虽然可以索引SPA网页内容, 但是这并不总是可靠的. 如果你想要确保SPA网页内容能被搜索引擎正确索引, 最好使用服务器端渲染来生成HTML内容并将其发送到浏览器.

而SSR就是为解决以上两个痛点而是的.

4. Angular Universal如何解决FCP和SEO问题

Angular Universal允许我们为Angular应用程序进行服务器端渲染. 这意味着我们可以在服务器上生成HTML内容并将其发送到浏览器, 而不是在浏览器中使用Javascript动态生成内容. 这样, 搜索引擎可以看到页面的全部内容并正确索引它, 从而解决SPA应用程序的SEO问题.

此外, Angular Universal还可以解决FCP(首次内容绘制)性能问题. 由于SPA应用程序的初始HTML文件通常为空或者包含非常少的内容, 因此它们需要大量的JavaScript代码来生成内容. 这会导致长时间的初始加载时间, 从而影响FCP性能. 使用Angular Universal进行服务器端渲染可以解决这个问题, 因为它可以在服务器上生成完整的HTML内容并将其发送到浏览器, 从而在让浏览器快速展现页面轮廓, 减少白屏时间, 于此同时浏览器会加载整个应用所需的Javascript代码, 从而提高整体的用户体验. 读到这里读者可能会有一个疑惑, 那就是SPA不又变成MPA了吗其实不然, HTML内容只是为了减少用户等待时间, 在Javascript完全加载完成之前,给用户展示页面内容, 一旦Javascript加载完成, 前端页面会被重新绘制, 然后完全接管用户的交互任务. 所以在javascript加载完成后页面有一个非常短的闪烁, 那就是Javascript重新绘制First Content的过程. 当然这样的闪烁动作对要求苛刻的系统来说也是不可接受的, Angular也有相应的解决办法, 后面文章会讲到.

那么具体来讲, Angular Universal是如何进行服务器端渲染的呢

我们知道Javascript是可以通过Node.js在在服务器端执行的, 每错Angular Universal正式通过Express这个Node.js应用程序框架提供的强大功能和工具在后端处理请求, 再将生成的HTML页面发送到前端或者喂给爬虫, 这样前端就能快速渲染页面, 爬虫也能得到某个页面完整的HTML内容, 从而正确地为页面建立索引.

当然这并不是完美地解决FCP和SEO问题, 因为Node.js执行javascript仍然需要时间和占用大量的服务器资源, 一旦请求增多, 频率加快, express将会称为瓶颈, 是一个非常影响TTFB(Time to first byte)指标的问题.

那么怎样进一步解决该问题呢, 这里有两种思路:

  • 一种是采用数据库缓存例如Redis或Memcached, 将页面缓存起来, 当下次访问相同页面时从缓存调取, 从而避免express重新执行javascript从而提高性能. 而此方案不是本文讲解的重点, 如果要采用此方案, 请自行百度或google了解相关详情.

  • 另外一种方案叫Prerender技术. 它在构建时就为应用程序的每个页面生成静态HTML文件, 而不是依赖JavaScript在运行时动态生成内容. 当用户请求页面时, 这些静态HTML文件可以直接发送到用户浏览器, 从而提高首次内容呈现(FCP)的时间, 并使搜索引擎更容易爬取和索引内容.

5. 开启SSR

您可以使用@nguniversal/express-engine 依赖包在Angular应用程序中启用服务器端渲染,如下:


# 进入项目根目录
# 添加express-engine 
ng add @nguniversal/express-engine 

说明: Angular Universal需要Node.js的Active LTS版本或维护LTS版本。有关信息,请参阅版本兼容性指南以了解当前支持的版本。

该命令更新应用程序代码以启用SSR,并向项目结构中添加额外的文件。

该命令会新增以下三个文件, 并修改以下一些文件

src/main.server.ts       // <-- * server-side application configuration (standalone app only)
 src/app/app.server.module.ts     // <-- * server-side application module (NgModule app only)
server.ts               // <-- * express web server

angular-universal-demo/angular.json

注意: 此处的angular.json有一些手动更改, 主要在"serve-ssr"这个节点, 主要是为了解决Angular的一个bug.
bug详情请参考这个issue report - Congiguration 'development' is not set in the workspace


--- a/angular-universal-demo/angular.json
+++ b/angular-universal-demo/angular.json
@@ -36,7 +36,7 @@
         "build": {
           "builder": "@angular-devkit/build-angular:browser",
           "options": {
-            "outputPath": "dist/angular-universal-demo",
+            "outputPath": "dist/angular-universal-demo/browser",
             "index": "src/index.html",
             "main": "src/main.ts",
             "polyfills": "src/polyfills.ts",
@@ -152,6 +152,73 @@
               "devServerTarget": "angular-universal-demo:serve:dev"
             }
           }
+        },
+        "server": {
+          "builder": "@angular-devkit/build-angular:server",
+          "options": {
+            "outputPath": "dist/angular-universal-demo/server",
+            "main": "server.ts",
+            "tsConfig": "tsconfig.server.json",
+            "optimization": false,
+            "sourceMap": true,
+            "extractLicenses": false
+          },
+          "configurations": {
+            "production": {
+              "outputHashing": "media",
+              "fileReplacements": [
+                {
+                  "replace": "src/environments/environment.ts",
+                  "with": "src/environments/environment.prod.ts"
+                }
+              ],
+              "optimization": true,
+              "sourceMap": false,
+              "extractLicenses": true
+            },
+            "dev": {
+              "fileReplacements": [
+                {
+                  "replace": "src/environments/environment.ts",
+                  "with": "src/environments/environment.dev.ts"
+                }
+              ]
+            }
+          },
+          "defaultConfiguration": "production"
+        },
+        "serve-ssr": {
+          "builder": "@nguniversal/builders:ssr-dev-server",
+          "options": {
+              "browserTarget": "angular-universal-demo:build",
+              "serverTarget": "angular-universal-demo:server"
+          },
+          "configurations": {
+            "production": {
+              "browserTarget": "angular-universal-demo:build:production",
+              "serverTarget": "angular-universal-demo:server:production"
+            }
+          }
+        },
+        "prerender": {
+          "builder": "@nguniversal/builders:prerender",
+          "options": {
+            "routes": [
+              "/"
+            ]
+          },
+          "configurations": {
+            "production": {
+              "browserTarget": "angular-universal-demo:build:production",
+              "serverTarget": "angular-universal-demo:server:production"
+            },
+            "development": {
+              "browserTarget": "angular-universal-demo:build:development",
+              "serverTarget": "angular-universal-demo:server:development"
+            }
+          },
+          "defaultConfiguration": "production"
         }

angular-universal-demo/package.json


--- a/angular-universal-demo/package.json
+++ b/angular-universal-demo/package.json
@@ -9,7 +9,11 @@
     "buildProd": "ng build --configuration production",
     "test": "ng test",
     "lint": "ng lint",
-    "e2e": "ng e2e"
+    "e2e": "ng e2e",
+    "dev:ssr": "ng run angular-universal-demo:serve-ssr",
+    "serve:ssr": "node dist/angular-universal-demo/server/main.js",
+    "build:ssr": "ng build && ng run angular-universal-demo:server",
+    "prerender": "ng run angular-universal-demo:prerender"
   },
   "private": true,
   "dependencies": {
@@ -18,24 +22,27 @@
     "@angular/cdk": "13.3.1",
     "@angular/platform-browser-dynamic": "13.3.1",
+    "@angular/platform-server": "13.3.1",
     "@angular/router": "13.3.1",
     "@swimlane/ngx-charts": "19.1.0",
     "@swimlane/ngx-datatable": "20.0.0",
+    "@nguniversal/express-engine": "^13.1.0",
     "@angular/cli": "13.3.1",
     "@angular/compiler-cli": "13.3.1",
     "@angular/language-service": "13.3.1",
+    "@nguniversal/builders": "^13.1.0",
+    "@types/express": "^4.17.0",

app.module.ts

--- a/angular-universal-demo/src/app/app.module.ts
+++ b/angular-universal-demo/src/app/app.module.ts
@@ -33,7 +33,7 @@ const DEFAULT_PERFECT_SCROLLBAR_CONFIG: PerfectScrollbarConfigInterface = {

 @NgModule({
   imports: [
-    BrowserModule,
+    BrowserModule.withServerTransition({ appId: 'serverApp' }),
     BrowserAnimationsModule,
     FormsModule,
     HttpClientModule,

Angular 会把 appId 值(它可以是任何字符串)添加到服务端渲染页面的样式名中,以便在客户端应用启动时可以找到并移除它们。

app.routing.ts

--- a/angular-universal-demo/src/app/app.routing.ts
+++ b/angular-universal-demo/src/app/app.routing.ts
@@ -19,10 +19,10 @@ export const routes: Routes = [
 @NgModule({
     imports: [
         RouterModule.forRoot(routes, {
-            preloadingStrategy: PreloadAllModules, 
-            relativeLinkResolution: 'legacy',
-            // useHash: true
-        })
+    preloadingStrategy: PreloadAllModules,
+    relativeLinkResolution: 'legacy',
+    initialNavigation: 'enabledBlocking'
+})
     ],
     exports: [
         RouterModule

“enabledBlocking”-在创建根组件之前开始初始化导航。引导程序将被blocked,直到初始导航完成。此值是服务器端渲染工作所必需的。


--- a/angular-universal-demo/src/main.ts
+++ b/angular-universal-demo/src/main.ts
@@ -9,5 +9,15 @@ if (environment.production) {
   enableProdMode();
 }

-platformBrowserDynamic().bootstrapModule(AppModule)
+function bootstrap() {
+     platformBrowserDynamic().bootstrapModule(AppModule)
   .catch(err => console.error(err));
+   };
+
+
+if (document.readyState === 'complete') {
+  bootstrap();
+} else {
+  document.addEventListener('DOMContentLoaded', bootstrap);
+}

6. 开启客户端水合(Client Hydration)

客户端水合是在客户端上恢复服务器端呈现的应用程序的过程。这包括重用服务器呈现的DOM结构、持久化应用程序状态、传输服务器已经检索到的应用程序数据以及其他进程。
您可以通过修改app.module.ts文件来启用水合

如果是Angular 11以上16 以下的版本, 要达到Hydration的效果可参考如下配置, 需要引入BrowserTransferStateModule.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { BrowserTransferStateModule } from '@angular/platform-browser';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'serverApp' }),
    BrowserTransferStateModule,
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

如果Angular 16为了达到Hydration的效果, 可以参考如下配置:
。从@angular/platform-browser导入provideClientHydration函数,并将函数调用添加到AppModule的providers部分,如下所示。


import {provideClientHydration} from '@angular/platform-browser';
// ...

@NgModule({
  // ...
  providers: [ provideClientHydration() ],  // add this line
  bootstrap: [ AppComponent ]
})
export class AppModule {
  // ...
}

7. 使用 Universal 构建和运行

构建SSR

npm run build:ssr

构建完应用之后,启动服务器

npm run serve:ssr

或者构建同时启动服务器

npm run dev:ssr

8. Prerender 预渲染静态 HTML

经过上面的步骤后,如果我们通过npm run build:ssr 构建项目,你会发现在 dist/<your project>/browser 下面只有 index.html 文件,打开文件查看,发现其中还有 <app-root></app-root> 这样的元素,也就是说你的网页内容并没有在 html 中生成。这是因为 Angular 使用了动态路由,比如 /product/:id 这种路由,而页面的渲染结果要经过 JS 的执行才能知道,因此,Angular 使用了 Express 作为 Web 服务器,能在服务端运行时根据用户请求(爬虫请求)使用模板引擎生成静态 HTML 界面。

而 prerender(npm run prerender)会在构建时生成静态 HTML 文件。比如我们做企业官网,只有几个页面,那么我们可以使用预渲染技术生成这几个页面的静态 HTML 文件,避免在运行时动态生成,从而进一步提升网页的访问速度和用户体验。

8.1. 预渲染路径配置

需要进行预渲染(预编译 HTML)的网页路径,可以有几种方式进行提供:

  1. 通过命令行的附加参数:

ng run <app-name>:prerender --routes /product/1 /product/2

这里有个需要注意的地方, 即使我只指定了两个路径, Angular universal还是会使用guess-parser解析routes猜测可能的路径帮助渲染一堆路径, 实际上这种猜测有些鸡肋, 根本不会准确, 没有太大帮助. 此时可以使用 --no-guess-routes选项将其关闭.


ng run <app-name>:prerender --no-guess-routes --routes /product/1 /product/2

或者修改angular.json, 将guessRoutes设置为false

"prerender": {
    "builder": "@nguniversal/builders:prerender",
    "options": {
      "routes": [
        "/product/1",
        "/product/2"
      ],
      "guessRoutes": false
    }
  1. 如果路径比较多,比如针对 product/:id 这种动态路径,则可以使用一个路径文件:

routes.txt

/products/1
/products/23
/products/145
/products/555

然后在命令行参数指定该文件:

ng run <app-name>:prerender --routes-file routes.txt

或者在angular.json中指定routes-file


"prerender": {
          "builder": "@nguniversal/builders:prerender",
          "options": {
            "guessRoutes": false,
            "routesFile": "routes-to-prerender.txt" // 在这里指定routes-file
          },
  1. 在项目的 angular.json 文件配置需要的路径
 "prerender": {
   "builder": "@nguniversal/builders:prerender",
   "options": {
     "routes": [ // 这里配置
       "/",
       "/main/home",
       "/main/service",
       "/main/team",
       "/main/contact"
     ]
   },

配置完成后,重新执行预渲染命令(npm run prerender 或者使用命令行参数则按照上面<1><2>中的命令执行),编译完成后,再打开 dist/<your project>/browser 下的 index.html 会发现里面没有 <app-root></app-root> 了,取而代之的是主页的实际内容。同时也生成了相应的路径目录以及各个目录下的 index.html 子页面文件。

9. 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 ExampleComponent implements OnInit {
 
  constructor(
    private _title: Title,
    private _meta: Meta,
  ) { }
 
  ngOnInit() {
    this._title.setTitle('<此页的标题>');
    this._meta.addTags([
      { name: 'keywords', content: '<此页的 keywords,以英文逗号隔开>' },
      { name: 'description', content: '<此页的描述>' }
    ]);
  }
}

内部跳转优化

这个是指应用内部页面跳转尽量使用a标签,而不是使用别的标签加(click)事件进行跳转。

样式文件打包

另外一个需要注意的地方是,如果网站的样式很多很复杂,那么网站发布的时候angular.json中extractCss需要设置为true,即单独打包一个独立的样式文件,而不是将样式全部包含在发布后的index.html中。

全部包含在index.html中会造成抓取保存的静态html文件过大,百度不能正确保存快照(百度限制了缓存文件大小,好像是100k)。

添加robots.txt

在网站优化过程中,有些时候,网站中有重要及私密的内容,站长并不希望某些页面被蜘蛛抓取,比如后台的数据,测试阶段的网站,还有一种很常见的情况,搜索引擎抓取的大量没有意义的页面。

robots.txt是一个纯文本文件,用于声明该网站中不想被蜘蛛访问的部分,或指定蜘蛛抓取的部分,当蜘蛛访问一个站点时,它会首先检查该站点是否存在,robots.txt,如果找到,蜘蛛就会按照该文件中的内容来确定抓取的范围,如果该文件不存在,那么蜘蛛就会沿着链接直接抓取。即,只有在需要禁止抓取某些内容是,写robots.txt才有意义.

robots配置方法如下:

  • 在 project_root/src 路径下创建robots.txt文件,里面输入你的robots配置,如果不懂,可以百度robots的语法,修改后保存即可提交。

下面是一个简单的robots.txt的例子

User-agent: *
Disallow:
Sitemap: http://your_domain/sitemap.xml

还需要修改angular.json文件, 这样robots.txt文件才能被访问到

 "build": {
    ......
    "assets": [
      "src/favicon.ico",
      "src/robots.txt",
      "src/sitemap.xml",
      "src/assets"
    ],

自动生成sitemap

安装工具ngx-sitemap

npm install ngx-sitemap --save-dev

在prerendering后运行以下命令, 就可以生成sitemap.xml


$./node_modules/.bin/ngx-sitemap ./dist/angular-universal-demo/browser htts://your_domain
sitemap.xml successfully created in './dist/angular-universal-demo/browser/'

10. 使用Nginx部署

整个topo结构是这样的, 首先要使用pm2将服务器端渲染程序运行起来, 然后通过Nginx将请求反向代理到pm2.

这样PM2就会实际处理所有请求, 对于已经prerender过的页面PM2会直接去browser文件夹中去取, 对于没有prerender的页面, 首先会在服务器端渲染然后传送到浏览器端.
页面到达流量器端后, 首先页面有一个基本的静态呈现, 与此同时会继续向后端请求javascript, 直到javascript下载完成后, 将会进行CSR(客户端渲染), 如果没有使用到hydration技术, 此时页面会有一个比较明显的闪烁, 如果使用了hydration技术此时会CSR渲染会比较平滑地替代SSR渲染的页面, 除了页面被重新渲染意外, 前端路由也会被javascript接管. 此时如果用户不刷新页面, 整个应用就运行在SPA模式下了.

对于搜索引擎爬虫, 由于它只读取页面文本内容, 不会去执行javascript代码, 所以相对于浏览器访问, 爬虫只是爬取那一页的内容, 不会有javascript下载过程, 更不会有CSR, 以及hydration的过程.

整个部署过程是这样的, 首先安装Nginx, node以及PM2. 然后启动PM2, Nginx反向代理到PM2.

10.1. 安装Nginx

参考我的博客 鹏叔的技术博客-nginx安装教程

10.2. 安装Node

参考我的博客 鹏叔的技术博客-安装并配置nodejs

10.3. 安装PM2

安装pm2 来支持SSR

npm install -g pm2

10.4. 配置并启动pm2

为了能在任意位置都能执行pm2, 我们将pm2添加到PATH下, 添加的方法是在/usr/bin下创建一个软连接


ln -s /usr/local/node-v14.17.5-linux-x64/lib/node_modules/pm2/bin/pm2 /usr/bin/pm2

我们顺便查看一下pm2的版本并确保软连接创建有效

pm2 --version
[PM2] Spawning PM2 daemon with pm2_home=/root/.pm2
[PM2] PM2 Successfully daemonized
5.3.0

接下来, 我们就可以使用PM2启动服务器端渲染了,
这里我们假设已经将程序部署到了服务器的/var/your_app/webapp目录, 结构如下

tree -L 1 /var/your_app/webapp 
/var/your_app/webapp
├── browser
└── server

启动SSR


pm2 start --name app_ssr /var/your_app/webapp/server/main.js

注意: 使用angular cli默认生成的distFolder部署到服务器, 会出现index文件找不到.
这里可以修改以下让其相对于main.js查找index而不是process.cwd()

vi server.ts


const parentFolder: string = join(__dirname, '../');
const distFolder: string = join(parentFolder, 'browser');

设置开机自启动

# 保存要在机器重新启动时重新生成的列表
pm2 save

# 生成开机自启动服务
pm2 startup

# enable pm2开机自启
systemctl enable pm2-root

另外一些有用的pm2命令

# 查看进程
pm2 list
# 关闭prcess
pm2 stop process_name
# 删除进程
pm2 delete process_name

至此PM2配置完成, ssr默认会监听在4000端口, 可以通过如下命令查找端口号

grep "process.env\[\"PORT\"\]" /var/your_app/webapp/server/main.js

其他一些有用的pm2命令

查看日志某个任务的日志


pm2 log the_process_name

10.5. 配置Nginx反向代理

修改/etc/nginx/conf.d/defaut.conf将之前的配置由如下


location / {
        root   /var/your_app/webapp/;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html;
    }

改成


 location / {
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://127.0.0.1:4000/;
    }

11. troubleshooting

问题1

描述: 当执行npm run dev:ssr 系统抛出如下错误 Configuration 'development' is not set in the workspace.

原因: 原因实际上是一个bug, Angular开发人员未考虑到, 我们的配置于他们期望的有差异.
详细原因和解决方案请参考this issue
也请注意, 我贴出来的ng add @nguniversal/express-engine自动配置的angular.json的serve-ssr部分是有修改过的, 目的就是为了解决这个问题.

问题2

描述: 当执行'npm run dev:ssr' 系统抛出如下错误 ReferenceError: window is not defined

原因: 问题的原因是在Nodejs运行时, 没有window, document, navigator等等浏览器端对象.

可以参考我的博客使用Angular Universal时的重要注意事项,
里面提到了三种解决该问题的策略, 这里我选择了 策略3:Shims, 相应的修改如下:


import 'zone.js/dist/zone-node';
 import { join } from 'path';
-
+import "localstorage-polyfill";
 import { AppServerModule } from './src/main.server';
 import { APP_BASE_HREF } from '@angular/common';
-import { existsSync } from 'fs';
-
+import { existsSync,readFileSync } from 'fs';
+import { createWindow } from "domino";
 // The Express app is exported so that it can be used by serverless Functions.
 export function app(): express.Express {
   const server = express();
   const distFolder = join(process.cwd(), 'dist/angular-universal-demo/browser');
   const indexHtml = existsSync(join(distFolder, 'index.original.html')) 'index.original.html' : 'index';
 
+  applyDomino(indexHtml)
   // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
   server.engine('html', ngExpressEngine({
     bootstrap: AppServerModule,
   }));
 
@@ -35,26 +36,41 @@ export function app(): express.Express {
   });
 
   return server;
 }
 
+function applyDomino(indexHtml: string): void {
+
+  const win = createWindow(indexHtml)
+
+  console.log("applying mock window ")
+  global["localStorage"] = localStorage;
+  
+  // Polyfills
+  (global as any).window = win;
+  (global as any).document = win.document;
+  (global as any).navigator = win.navigator;
+  (global as any).location = win.location;
+  
+}
+

问题3

描述: 当执行npm run dev:ssr 或者 npm run prerender时, 频繁的输出警告 Warning: Flex Layout loaded on the server without FlexLayoutServerModule

原因分析: FlexLayout 需要在js运行时确定屏幕大小等等信息, 在server side由于没有screen的概念所以需要模拟浏览器端的行为, FlexLayout专门为ssr提供了一套Module来模拟浏览器端的行为, 所以最好在服务器端程序引入FlexLayoutServerModule, 也即在app.server.module.ts中引入FlexLayoutServerModule

//app.server.module.ts
import {NgModule} from '@angular/core';
import {FlexLayoutServerModule} from '@angular/flex-layout/server';

@NgModule({
  imports: [
    ... other imports here
    FlexLayoutServerModule,
  ]
})

以及定义在SSR配置渲染时模拟的屏幕大小, 修改app.module.ts

//app.module.ts to simulate breakpoints
@NgModule({
  imports: [
  ... other imports here
  FlexLayoutModule.withConfig({ssrObserveBreakpoints: ['xs', 'lt-md']})
  ]
})

问题4

描述: 当执行npm run dev:ssr 或者 npm run prerender时, 系统抛出如下错误ReferenceError: XMLHttpRequest is not defined

原因分析: 原因是我的代码中调用了ajax这个rxjs operator import { ajax, AjaxResponse } from 'rxjs/ajax';其底层实现需要XMLHttpRequest,
而在Sever side javascript环境中没有引入xmlhttprequest这个包.

解决办法: 解决办法可以有多种, 我选择了将ajax这个rxjs operator全部替换成了HttpClient call, 例如

concatMap((remoteServerUri: string) => {
        const headers = new HttpHeaders({
          Authorization: "Basic " + idToken,
        });
        return this.httpClient.get<any>(remoteServerUri + url, { headers: headers });
      }),

参考文章 How to resolve window is not defined on npm run serve:ssr

12. 后记

本技术博客原创文章位于鹏叔的技术博客空间 - gitlab安装升级及迁移, 要获取最近更新请访问原文.

更多技术博客请访问: 鹏叔的技术博客空间

13. 参考文档

Server-side rendering (SSR) with Angular Universal

Important Considerations when Using Angular Universal

2022 前端性能优化最佳实践

使用Angular Universal时的重要注意事项

Angular服务器渲染常遇的坑

Angular SSR 探究

Angular 7 SSR 之后使用 node + nginx 部署在linux

Better Approach for Styles for SSR (Angular Universal)

Feature Request: ability to produce and load CSS via link tag in index.html


https://www.xamrdz.com/backend/3k91934306.html

相关文章: