标准的 Angular 应用运行在浏览器中,它会在 DOM 中渲染页面,以响应用户的操作。 而Angular Universal 会在服务端运行,生成一些静态的应用页面,稍后再通过客户端进行启动。 这意味着该应用的渲染通常会更快,让用户可以在应用变得完全可交互之前,先查看应用的布局。
本文以蜗牛双语阅读网站为例,说明网站整个SSR改造的过程。
要创建服务端应用模块 app.server.module.ts,请在项目目录下运行 CLI 命令:
ng add @nguniversal/express-engine
命令执行后,项目中会添加几个服务端渲染相关的文件
根目录下会新增 server.ts 和 tsconfig.server.json
src目录下会新增 main.server.ts 和 app/app.server.module.ts
先看一下客户端渲染方式的调试运行效果,使用 npm run start 启动页面,查看浏览器开发工具界面的网络标签页,可以看到请求的localhost网页文档,只是引用了相关的样式和js,并没有后台数据:
查看预览窗口,也只能看到一个空白的页面:
再看服务端渲染方式的调试运行效果,使用 npm run dev:ssr 启动页面,查看浏览器开发工具界面的网络标签页,可以看到请求的localhost网页文档已经完成了后台数据的渲染:
查看预览窗口,可以看到大致的页面内容:
使用指令 npm run build:ssr 生成发布包,发布包包含两个目录,一个browser,一个server
browser目录下的文件,可以按照普通客户端渲染的方式进行部署。server目录下的文件,可以通过 node main.js 启动服务供浏览器访问。服务运行的端口可以通过配置nodejs运行的环境变量进行设置:
set PORT=8080 && node dist/mysite/server/main.js
不管是客户端渲染还是服务端渲染的部署方式,对于制定路由的页面都是路由请求后再进行渲染,如果页面内容较多或者渲染逻辑较复杂,就会消耗较长时间,对于网站用户来讲都是不好的体验。为解决这一问题,Angular Universal 提供了预先渲染的方法。
使用指令 npm run prerender 即可进行预先渲染,prerender 有很多指令参数,通常可以设置按照路由列表文件进行预渲染。修改angular.json文件中prerender节点的routesFile参数可以指定路由列表文件路径:
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
...
"projects": {
"woniuyuedu": {
"projectType": "application",
...
"architect": {
"build":{ ... },
...
"prerender": {
"builder": "@nguniversal/builders:prerender",
"options": {
"routesFile": "./render-routes.txt"
},
...
}
}
}
}
}
对应的render-routes.txt文件里只要记录想要预渲染的路由路径就可以了,每个路径占一行:
/
/read/list/1
/read/book/flipped
/read/book/the-kite-runner
...
此时运行 npm run prerender 就可以完成对配置页面的预渲染。预渲染生成的页面位于browser目录中:
按照客户端渲染方式部署整个brower目录,对于已经预渲染的路由路径,服务器将自动把预先渲染好的文件返回给浏览器,对于没有预先渲染的路由路径,则由浏览器通过js即时渲染。
顾名思义服务端渲染方式是在服务端完成,因此一些浏览器的 API 或功能将不可用。服务端无法使用浏览器中的全局对象window, document, location, localStorage等。为此Angular 提供了可注入对象,用于在服务端替换对等的对象,可以通过创建一个垫片service完成替换:
import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { DOCUMENT, isPlatformBrowser, isPlatformServer } from '@angular/common';
@Injectable({
providedIn: 'root'
})
export class SSRPolyfillService {
constructor(
@Inject(DOCUMENT) private _doc: Document,
@Inject(PLATFORM_ID) private platformId: any
) { }
// 判断当前运行环境是否是服务端
isServer(): boolean {
return isPlatformServer(this.platformId);
}
// 判断当前运行环境是否是浏览器端
isBrowser(): boolean {
return isPlatformBrowser(this.platformId);
}
// 替换全局window对象
getWindow(): Window | null {
return this._doc.defaultView;
}
// 替换全局location
getLocation(): Location {
return this._doc.location;
}
// 替换全局document对象
getDocument(): Document {
return this._doc;
}
}
对于一些需要使用localStorage的逻辑,在对应代码前添加判断,如果是浏览器端才运行相关代码,服务端需跳过。
因为在服务端已经通过请求后端数据将对应内容完成渲染,到了浏览器端执行JS代码又请求了一次后端数据,对应内容就会重新渲染,从而产生页面闪烁的效果。
通过上图可以发现浏览器拿到渲染好的localhost页面文档后,又请求了后台接口获取书籍列表。数据返回后重新渲染书籍列表就会出现页面闪烁的效果。
针对这一问题 Angular 提出了 Transfer State 的方法,将服务端已获取的数据直接迁移到浏览器端,避免因数据的重复请求而出现的页面闪烁。通过在app.server.module.ts 中加入 ServerTransferStateModule 的引入就可以实现 Transfer State 的功能。
import { NgModule } from '@angular/core';
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
@NgModule({
imports: [
AppModule,
ServerModule,
ServerTransferStateModule
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
ServerTransferStateModule通过使用http interceptor将后台数据以key-value的形式保存在页面文档中,浏览器请求后台数据时将先判断页面是否包含对应key的迁移数据,没有才进行请求。引入ServerTransferStateModule后再看页面文档,在文档最后就可以发现带有后台请求地址参数详细数据及其返回数据:
Angular Universal 通过后端渲染的方式能够很好的解决搜索引擎抓取不到完整渲染页面的问题。但是页面加载速度比较考验服务器的性能,服务器性能不好服务端渲染也就慢,浏览器拿到文档的时间就长。所以对于服务器条件不好的情况还是采用浏览器端渲染结合常用路由页面预先渲染的方式。可以减少页面加载时间。蜗牛双语阅读网站即是采用这种方式。