angular10预渲染实践笔记

Posted Fighting_No1

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了angular10预渲染实践笔记相关的知识,希望对你有一定的参考价值。

angular10预渲染实践笔记

参考资料:

目的:基于Angular的服务端渲染和预渲染功能来生成多页静态页面。

理解Angular服务端渲染预渲染

Angular Universal 会在服务端通过一个被称为服务端渲染(server-side rendering - SSR)的过程生成静态的应用页面。

它可以生成这些页面,并在浏览器请求时直接用它们给出响应。 它也可以把页面预先生成为 html 文件,然后把它们作为静态文件供服务器使用。

客户端在接收html文件之前,服务端将html标签占位做动态数据填充;服务端处理好一个html字符串文件生成一个静态的html页面文件返回给客户端,客户端即会解析html,渲染呈现出UI/UX;

注意:angular服务端渲染会先在服务器端渲染好的只是静态的页面内容,然后响应客户端,而一些交互在加载完成之前是不可用的。

服务端渲染特性:

1、访问加载速度比其它任意方式几乎都快;
2、利于搜索引擎抓取,方便网站SEO
3、单个静态页只分配特定的路由
4、不用执行javascript
5、chrome中访问后,当前页右键【查看网页源代码】,对应页面内容都可在源代码中查看
6、资源只单次渲染,无任何异步加载

预渲染参照如下流程图:

注解:
1、 服务端存放了打包好的静态资源与文件(./dist/browser下的所有文件)。
2、 浏览器解析渲染返回的html内容。
3、 打包好的bundle.js / chunk.js

不需要预渲染的情况

1、生成针对特定路由的静态HTML 文件,可以是一个项目工程,也可以是一个CDN服务;所以预渲染不适合动态路由的页面项目;
2、如果页面中有动态的操作,以及频繁的数据变动,就不得不经常升级/发布页面,对运维以及开发维护不友好;不建议使用预渲染;
3、Pre-render几乎涵盖了SSR的所有优点,但如果是一个复杂操作功能的项目,且还有N多个页面,那么预渲染在开发维护阶段将会变得异常艰难,因为它生成的是一个个对应的html页面,即有N多个路由,所以在它开发构建之时编译将会变得异常缓慢;

Angular预渲染生成多个页面实践

按照Angular服务端渲染教程执行ng add @nguniversal/express-engine,自动添加服务端渲染对应的文件和配置,然后执行npm run prerender,生成静态html页面。

注意事项:

1、路由配置

(1)路由策略配置:angular路由策略只能是默认的PathLocationStrategy,不能是HashLocationStrategy(IE9只能用HashLocationStrategy)。用HashLocationStrategy,会导致找不到对应路由而渲染默认路由,表现为生成的html都是默认的路由内容。

(2)angular.json中的prerender的routes配置

  • routes :定义要预先渲染的额外路由数组。——适用于少量路由的情况。
  • routesFile :指定一个文件,其中包含要预先渲染的所有路由的列表,以换行符分隔。如果你有大量路由,则此选项很有用。
  • guessRoutes : 构建器是否应该提取路由并猜测要渲染的路径。默认为 true 。——true会渲染app-routing.module.ts中定义的所有路由,false则只渲染routes和routesFile中定义的路由。

如果要只预渲染某个特定路由哦/XX/XXX,则可如下配置:

"prerender": 
  "builder": "@nguniversal/builders:prerender",
  "options": 
    "browserTarget": "projectId:build:production",
    "serverTarget": "projectId:server:production",
    "guessRoutes": false, // 只渲染routes和routesFile中定义的路由
    "routes": [
      "/XX/XXX" // 指定的路由,可多个
    ]
  ,
  "configurations": 
    "production": 
  

(3)预渲染不支持路由参数(?后的内容),故routes中配置/XX?XXX=XXX这样的路由,执行预渲染命令会直接报错。源码中取路由参数的地方,一定要记得做空判断

(4)预渲染只能接收静态路由,如product/:id这样的动态路由,要渲染,必须指定id值,如product/1,这样才能渲染成html页面。

2、http请求

预渲染的http请求必须使用绝对路径,而非相对路径。如请求assets中的静态资源,使用相对路径的话,预渲染时运行的是server/main.js,而非browser/main.js,会因找不到资源而报错。

由于我项目的要求只是生成多个静态页面,而不是服务器渲染加快响应速度,所以我解决这个问题的方法是:静态资源host配置在环境变量universalServerUrl,默认值为http://localhost:4200,在执行预渲染前,另开个终端,执行npm run start,这样预渲染过程才能访问到本地的静态资源,确保http请求成功,生成完整内容的静态页面。

(1)解决翻译JSON文件无法获取的问题

新增个http拦截器,如果是json文件并且是服务端渲染的话,就使用静态资源的host拼接url,构成绝对路径。

import  Inject, Injectable, PLATFORM_ID  from '@angular/core';
import 
  HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
 from '@angular/common/http';
import  Observable  from 'rxjs';
import  isPlatformServer  from '@angular/common';
import  environment  from 'src/environments/environment';

@Injectable(
  providedIn: 'root'
)
export class UniversalInterceptor implements HttpInterceptor 

  constructor(@Inject(PLATFORM_ID) private platformId: object)  

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> 
    let serverSeq = req;
    if (req.url.endsWith('.json') && isPlatformServer(this.platformId)) 
      // 服务器渲染,转换url
      serverSeq = req.clone( url: environment.universalServerUrl + req.url );
    
    return next.handle(serverSeq);
  


在app.module.ts的providers中添加拦截器:

provide: HTTP_INTERCEPTORS, useClass: UniversalInterceptor, multi: true

如果是要实现服务器渲染,那么请参考这篇文章解决这个问题:小谈Angular SSR项目的国际化

(2)解决antd icon找不到的问题

我项目的antd icon使用的是动态加载的方式,会发起http请求动态引入,需要修改预渲染时的请求路径。

在app.module.ts的构造器中,如果是服务端渲染的话,则通过 NzIconService 的 changeAssetsSource() 方法来修改图标资源的位置。

export class AppModule 
  constructor(
    @Inject(PLATFORM_ID) private platformId: object,
    @Inject(APP_ID) private appId: string,
    private iconService: NzIconService) 
    const platform = isPlatformBrowser(platformId) ? 'in the browser' : 'on the server';
    console.log(`Running $platform with appId=$appId`);
    if (isPlatformServer(platformId)) 
      this.iconService.changeAssetsSource(environment.universalServerUrl);
    
  

(3)解决http请求需要token的问题

预渲染时相当于你本地启动服务,然后打开浏览器访问对应的路由,组件内部执行初始化逻辑(如http请求获取后端数据),渲染页面完毕,你将当前页面的html保存到本地。

由于后端接口请求都需要token,所以我们需要配置个token进行http请求,方便预渲染过程中http请求拿到后端数据后去进行渲染。如果没有配置token,则接口请求会报401,跳转到登录页面,渲染失败。

在环境变量中配置个universalToken变量,预渲染前,先登录获取到token,然后赋值给universalToken变量,再执行预渲染命令,就可以为接口请求带上token了。

修改http拦截器,判断平台是否为服务端,是则取universalToken环境变量作为token参数值。

export class AuthInterceptor implements HttpInterceptor 

  constructor(
    @Inject(PLATFORM_ID) private platformId: object
  )  

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> 
    let authReq = req;
    if (!req.url.endsWith('.json')) 
      // 添加token参数
      if (isPlatformBrowser(this.platformId)) 
        authReq = req.clone( setParams:  token: localStorage.getItem('token')  );
       else 
        authReq = req.clone( setParams:  token: environment.universalToken  );
      
    
    return next.handle(authReq).pipe(
      catchError((error: HttpErrorResponse) => this.handleError(error))
    );
  

在app.module.ts的providers中添加拦截器:

provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true

PS:由于不同用户获取的后端数据是不一样的,所以推荐使用个没有数据的用户的token,这样预渲染的时候,只渲染UI,而后端数据为空。页面请求的时候,页面上先显示渲染的空数据,然后再显示请求到的后端数据。

3、Web API兼容性

(1)使用Angular抽象层

服务端应用不能引用浏览器独有的全局对象,比如 window、document、navigator 或 location。但Angular 提供了一些这些对象的可注入的抽象层,比如 Location 或 DOCUMENT,它可以作为你所调用的 API 的等效替身。

比如你在视图初始化时要通过document.querySelector定位某个dom对象,可以采用如下方式实现:

import  DOCUMENT  from '@angular/common'; // 引入DOCUMENT抽象层
 
export class AppComponent implements OnInit 
  constructor(
    @Inject(DOCUMENT) private document: Document // 注入DOCUMENT抽象层,当在客户端运行时,就是浏览器的document对象
  )  
  ngInit()  // 在视图初始化时去使用document对象
    this.element = this.document.querySelector('.class');
  

(2)判断平台类型,执行不同的代码

export class AppModule 
  constructor(
    @Inject(PLATFORM_ID) private platformId: object)  // 注入PLATFORM_ID,值为server | browser
    const platform = isPlatformBrowser(platformId) ? 'in the browser' : 'on the server';
    console.log(`Running $platform with appId=$appId`);
    if (isPlatformServer(platformId)) 
      // 在服务端运行
    
  

比如在node中没有localStorage,所以预渲染会报错localStorage对象未定义。通过在源码中添加对平台类型的判断,是客户端才去localStorage中取数据,不是则取默认值,这样就能保证预渲染顺利进行,也不影响在客户端的运行。

以上是关于angular10预渲染实践笔记的主要内容,如果未能解决你的问题,请参考以下文章

angular10预渲染实践笔记

《JavaScript设计模式与开发实践》—— 代理模式

Angular isPlatformBrowser 检查 PLATFORM_ID 不会阻止服务器端预渲染

javascript设计模式与开发实践阅读笔记——代理模式

dotnet core + Angular Universal + docker => 由于错误,预渲染失败:错误:找不到模块

Angular Universal - 只为网络爬虫预渲染?