前言
SSR是 Server-Side Rendering(服务器端渲染)的缩写,在普通的SPA中,一般是将框架及网站页面代码发送到浏览器,然后在浏览器中生成和操作DOM(这里也是第一次访问SPA网站在同等带宽及网络延迟下比传统的在后端生成HTML发送到浏览器要更慢的主要原因。为了良好的用户体验和前后端分离,也可以将SPA应用打包到服务器上,在服务器上渲染出HTML发送到浏览器,能很大程度上解决首屏慢的问题,还能获得更好的SEO。
写在前边
该服务框架的目的是为了让业务页面更方便快捷的接入服务器端渲染,且多人协同开发时有良好的代码结构,清晰、便于维护。
一、横向对比
目前常用的SSR前端框架是基于react的nextjs和基于vue的nuxtjs,本文主要介绍基于nextjs的前端渲染模板来介绍,我们日常接触到的前端+nodejs服务端开发无非三种模式:
1、CSR( Client-Side Rendering)即客户端渲染,倾向于静态页面开发,首屏数据的请求、获取和渲染是在客户端加载前端文件之后实现的,因此页面在用户看来会有闪屏和一段时间的空白,体验较差;
2、伪SSR模式即服务端返回的是未经渲染的html静态文件和首屏数据,也就是说页面(一般指首屏)还是在服务端加载,但无需额外发起请求获取渲染数据,这样确实解决了闪屏的问题,但还会存在较长时间的白屏,特别是在前端工程打包未做chunk优化时,体验一般(这也是我们目前采取的开发模式);
3、SSR(Server-Side Rendering)即是本文要介绍的模式,服务端输出的是已经通过nextjs渲染引擎渲染好的前端页面和首屏数据,客户端不会出现白屏、闪屏和加载慢的问题,首屏展示的时间完全取决于服务接口返回耗时,大大缩短了首屏耗时,体验最好,并且能获得更好的SEO。
二、框架搭建
搭建这个服务框架前,也做过一系列的思考和比较,如何即达到服务端渲染的功能目的,又能高效构建、自由扩展且规范开发;在基于目前的技术栈的前提下,提出了本文要介绍的nestjs+nextjs的解决方案,在保证组内成员接入上手简单的同时,也满足了上述要求。Nest 是一个用于构建高效,可扩展的 Node.js 服务器端应用程序的框架。它使用渐进式 JavaScript,内置并完全持 TypeScript(但仍然允许开发人员使用纯 JavaScript 编写代码)并结合了 OOP(面向对象编程),FP(函数式编程)和 FRP(函数式响应编程)的元素。
实现中,Nest的框架OOP模式也为实现服务端渲染带来了一定的麻烦,这也是搭建此框架最难的一点,下面会介绍如何解决。
1、普通的express服务+nextjs的开发模式
// express + next:
const serve = express() // 实例化Serve
const app = next({dev: true}) //实例化next对象
serve.get(‘’, (req. res)=>{
return app.render(req, res, ’url’, data) // 服务直接返回next render出的页面
})
这种开发方式弊端很多,例如文件散乱、耦合度高、路由混乱,服务端代码无工程化管理和输出,多成员开发维护困难等。
2、Nest + nextjs
Nest的面向对象编程、代码工程化等虽然能解决上述弊端,但也会让各个module如何公用同一个next对象造成了很大麻烦,在每个controller文件里都const app = next()一次,这显然很不靠谱;在踩了很多坑后,解决方式:通过nest-next的node-module重写Nest的@Render()装饰器,在服务端的app.module文件里import实例化的next对象,具体实现逻辑可以查看源码,实现如下:
import { RenderModule } from 'nest-next';
import Next from 'next';
@Module({
imports: [
RenderModule.forRootAsync(
Next({
dev: process.env.NODE_ENV == 'development'
})
),
ContentModule // 业务module
],
controllers: [],
providers: [],
})
3、代码打包和发布
这里又会牵扯到另一个问题,由于该服务是由nest和nextjs两种框架耦合而来,所以执行的build指令以及打包配置等都需要严格区分开,避免混乱;这里就不细展开了,主要是webpack相关的打包知识(有兴趣可以私下交流),也是踩了很多坑和不断的调试实现了服务的前端代码和服务代码本地开发调试、本地热更新、发布前的前端代码打包、服务端代码打包以及二者production模式下的配合。
三、线上表现
从目前已经接入服务的兴趣收集页面的Agies监控数据来看,在三秒开率(原来60%-90%,提升到现在100%左右)和首屏耗时(原来2000ms-3000ms,提升到现在100ms以内)两个首屏渲染的主要指标上,相对于之前的模式提升很显著。
其中DOM_Ready的时长较长是由于业务页面加载了太多icon的cdn资源,需要业务方自己优化。
未接入SSR服务页面性能
CSR业务下发到页面曝光的漏斗:
pv:60%,uv:67.3%
资讯兴趣收集页面
1、主要指标:
2、各种网络环境下的表现:
搜索兴趣收集页面
1、主要指标:
2、各种网络环境下的表现:
兴趣收集业务下发到页面曝光的漏斗:
pv:80%,uv:92.6%
四、服务接入
1、工程的目录结构:
2、申明业务模块
module
通过Nest提供的构建api自动构建module:nest g module moduleName
根目录下执行,会在src目录生成对应module文件夹,以及自动引用到app.module,只需关注业务代码
controller/service
自动构建service:nest g service moduleName
自动构建controller:nest g controller moduleName
根目录下执行,会在src目录下对应module文件夹里生成对应文件、引用到module逻辑和jest测试用例模板,只需关注业务代码
3、业务服务端接入
代码如下:(对应上图的content.controller.ts)
import { Controller, Get, Render, Req, Res, Param, Query } from '@nestjs/common';
import { ContentService } from './content.service';
@Controller('content')
export class ContentController {
constructor(private readonly ContentService: ContentService) {}
// 未加Render修饰器的即为正常的ajax请求
@Get()
ajax() {
console.log('index', 'xxx')
return 'this is index'
}
// 添加了Render修饰器的return的即为next渲染好的前端页面
@Render('content') // index文件不需要写路径,对应views/content/index.js页面
@Get('index')
index(@Param() param, @Query() query, @Req() req: Request, @Res() res: Response) {
const data = this.ContentService.getData() // 获取业务数据
return {}
}
@Render('content/main') // 非index文件需要写路径,对应views/content/main.js页面
@Get('main')
main(@Param() param, @Query() query, @Req() req: Request, @Res() res: Response) {
const data = this.ContentService.getData() // 获取业务数据
return {}
}
}
4、前端页面数据获取
服务端return的数据(例如{ data }),通过在前端代码中申明getServerSideProps方法的参数获取,该function为固定写法,return出的props会作用到该页面的props属性上,使用起来很方便。
// 获取nestjs返回的数据: ctx.query
export async function getServerSideProps(ctx) {
console.log('ctx', ctx.query)
const query = ctx.query
return { props: query };
}
最后,对于该服务有不理解的地方、好的优化的点以及建议,欢迎联系交流。