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

发布时间 2023-05-05 21:17:45作者: eagle.supper

介绍

尽管Angular Universal项目的目标是能够在服务器上无缝渲染Angular应用程序,但您应该考虑一些不一致之处。首先,服务器和浏览器环境之间存在明显的差异。在服务器上渲染时,应用程序处于短暂或“快照”状态。应用程序被完全渲染一次,返回完整的HTML,而整个过程中的产生的状态被销毁,直到下一次渲染开始, 再重新计算这些状态。接下来,服务器环境本质上不具有与浏览器相同的功能(也有可能服务器拥有而浏览器没有的功能)。例如,服务器没有任何cookie的概念。您可以将此功能和其他功能polyfill,但没有完美的解决方案来弥合这种差异。在后面的部分中,我们将介绍潜在的缓解措施,以减少在服务器上渲染时的错误范围。
还请注意SSR的目标:提高应用程序的初始渲染时间。这意味着,应该避免或充分防范任何可能在初始渲染中降低应用程序速度的情况。同样,我们将在后面的部分中回顾如何实现这一点。

"window is not defined"

使用Angular Universal时最常见的问题之一是服务器环境中缺少浏览器全局变量。这是因为Angular Universal项目使用domino作为服务器DOM渲染引擎。因此,服务器上不存在或不支持某些功能。这包括window和document全局对象、cookie、某些HTML元素(如canvas)以及其他一些元素。没有详尽的列表,所以请注意,如果您看到这样的错误,其中没有定义以前可访问的全局对象,很可能是因为该全局无法通过domino获得。

Fun fact: Domino stands for "DOM in Node"

如何解决上述问题

策略1:Injection

通常,所需的全局可以通过依赖注入(DI)通过Angular平台获得。
例如,我们可以通过@Inject(DOCUMENT)获得document对象。此外,还可以通过DOCUMENT对象获取window和location对象。例如:


// example.service.ts
import { Injectable, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';

@Injectable()
export class ExampleService {
  constructor(@Inject(DOCUMENT) private _doc: Document) {}

  getWindow(): Window | null {
    return this._doc.defaultView;
  }

  getLocation(): Location {
    return this._doc.location;
  }

  createElement(tag: string): HTMLElement {
    return this._doc.createElement(tag);
  }
}

但是我们不要期望这种方法能解决所有问题. localStorage就是一个例外, localStorage是一个经常被请求的API,它无法在浏览器以外良好的工作。如果您需要编写自己的库组件,而且使用到localstorage, 请考虑使用某种方法让其在服务器上和浏览器上提供相似的功能(这就是Angular CDK和Material所做的, 可以作为一种参考)。

策略2:Guard

如果我们不能从Angular platform注入所需的适当全局值,那么我们可以浏览器代码的调用的地方包装一层Guard,只要您不需要在服务器上访问该代码。例如,全局窗口元素的调用通常是为了获取窗口大小或其他一些视觉元素。然而,在服务器上,没有“Screen”的概念,因此很少需要此功能。
您可以在网上或其他地方阅读到,建议使用isPlatformBrowser或isPlatformServer。这种指南不太合适的方法。这是因为您最终会在应用程序代码中创建特定于平台的if-else分支代码。这不仅不必要地增加了应用程序的复杂度,而且还增加了必须维护的复杂性。通过依赖注入(DI)将代码分离为单独的特定于平台的模块和实现,您的业务代码可以保留关于业务逻辑的内容,而特定与平台的差异部分可以留给特定与平台的模块来处理, 并逐个案例逐个案例的完善抽象出来的Guard层。下面是一个例子:


// window-service.ts
import { Injectable } from '@angular/core';

@Injectable()
export class WindowService {
  getWidth(): number {
    return window.innerWidth;
  }
}


// server-window.service.ts
import { Injectable } from '@angular/core';
import { WindowService } from './window.service';

@Injectable()
export class ServerWindowService extends WindowService {
  getWidth(): number {
    return 0;
  }
}
// app-server.module.ts
import {NgModule} from '@angular/core';
import {WindowService} from './window.service';
import {ServerWindowService} from './server-window.service';

@NgModule({
  providers: [{
    provide: WindowService,
    useClass: ServerWindowService,
  }]
})

如果您有一个由第三方提供的组件,该组件与在各Angular platform上的行为不兼容,那么除了基本应用程序模块外,您还可以为浏览器和服务器创建两个单独的模块。基本应用程序模块将包含您所有的平台无关代码,浏览器模块将包含所有特定于浏览器的代码, 而服务器模块包含所有特定于服务器的代码. 而如果该组件对浏览器友好, 那么浏览器模块不必写太大代码,反之亦然。为了避免编辑过多的模板代码,可以创建一个无操作组件来放置库组件。下面是一个例子:


// example.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'example-component',
  template: `<library-component></library-component>`, // this is provided by a third-party lib
  // that causes issues rendering on Universal
})
export class ExampleComponent {}


// app.module.ts
import {NgModule} from '@angular/core';
import {ExampleComponent} from './example.component';

@NgModule({
  declarations: [ExampleComponent],
})


// browser-app.module.ts
import {NgModule} from '@angular/core';
import {LibraryModule} from 'some-lib';
import {AppModule} from './app.module';

@NgModule({
  imports: [AppModule, LibraryModule],
})


// library-shim.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'library-component',
  template: '',
})
export class LibraryShimComponent {}


// server.app.module.ts
import { NgModule } from '@angular/core';
import { LibraryShimComponent } from './library-shim.component';
import { AppModule } from './app.module';

@NgModule({
  imports: [AppModule],
  declarations: [LibraryShimComponent],
})
export class ServerAppModule {}

策略3:Shims

如果以上所有策略都不能符合要求,并且您只需要访问某种浏览器功能,那么您可以修补服务器环境的Global变量,以包括所需的全局。例如:

// server.ts
global['window'] = {
  // properties you need implemented here...
};

这种策略可以应用于任何浏览器环境有而服务环境未定义的元素。当你这样做的时候请小心,因为玩全局变量通常被认为是一种反模式。

fun fact:同样是功能补丁,shim在以及存在的各Angular platform上永远不受支持。而polyfill是计划被支持的功能补丁,或者在较新版本的platform上以及被支持的功能

应用程序速度慢,甚至无法渲染

Angular Universal渲染过程很简单,但也可以被善意或无意识的代码阻止或减慢。
首先,渲染过程的一些异步进程。当对platform-server (Angular Universal平台)发出渲染请求时,将执行单一路线导航。当导航完成时,也就是说所有Zone.js宏任务都完成了,无论当时处于什么状态(完整或不完整)的DOM都会返回给用户。

A Zone.js macrotask is just a JavaScript macrotask that executes in/is patched by Zone.js

这意味着,如果有一个进程(如microtask)需要占用一定数量的CPU时间片来才能完成,或者存在一个非常耗时的 HTTP连接请求,则渲染过程将无法完成或者需要相当长的时间。宏任务包括对全局变量(如 setTimeout 和 setInterval)以及 Observables 的调用。调用它们而不取消它们,或者让它们在服务器上运行的时间超过所需的时间可能会导致渲染效果欠佳。

如果您还不知道微任务和宏任务的差别,可能值得复习JavaScript事件循环并学习微任务和宏任务之间的区别。这里有一个很好的参考。

HTTP,Firebase,WebSocket等在渲染之前不会完成

与上面关于等待宏任务完成的部分类似,另一方面是平台不会等待微任务完成才完成渲染。在 Angular Universal 中,我们修补了 Angular HTTP 客户端,将其转换为宏任务,以确保给定渲染的任何所需的 HTTP 请求都已完成。但是,这种类型的补丁可能并不适合所有微任务,因此建议您对如何进行进行最佳判断。您可以查看代码参考,了解 Universal 如何包装任务以将其转换为宏任务,或者您可以简单地选择更改给定任务的服务器行为。

参考文档

Important Considerations when Using Angular Universal