React SSR 实现原理和源码解析

代码请参考:learn-react-ssr

什么是SSR?

SSR 是服务端渲染。它是后端模版渲染和前端(SPA)单页应用的结合。

它有 2 种常见的模式:

  • 完全使用后端路由渲染模版
  • 后端只渲染首次访问的内容,后面交给单页应用托管

这两者的主要区别是:后端路由的使用程度不同。在NodeJS的SSR框架中(比如NextJS),使用的都是后一种。

SSR 后端渲染的原理

后端渲染的逻辑主要参考 server/router/index.js 文件中针对 get(*) 所有 GET 请求的处理。

处理逻辑如下图所示:

高清流程图:https://whimsical.com/EsGj2yfbkHriPuhQq9hDMg

这里面有2个部分需要特别注意。一个是 getInitialProps,另一个是将React组件对象渲染成字符串并输出到模版文件中。

getInitialProps 方法

getInitialProps 方法是react组件类上的静态方法。以List组件为例,在方法内发起了异步请求:

e6c9d24egy1h2rdwpxsx9j20wc0oen06.jpg

getInitialProps 方法返回的数据,会在组件渲染前,传给组件。因此,在后端渲染时,要执行当前组件的 getInitialProps 方法,并且将结果通过 ssrData 参数传给组件。

e6c9d24egy1h2rhctalv0j20wa0gw76q.jpg

这里的 getInitialProps 是在后端渲染时执行的,所以使用的是 NodeJS 环境,可以使用 fs、http 等库。

渲染 React 组件对象

从上图看到,这里用到了 React.createElement() 方法来获取组件渲染后的html代码。然后将模版文件的占位符,替换成组件html代码。

为了让 react 单页应用能在前端正常运行,用webpack对前端目录 /client 进行打包,并且将模版文件中对应的占位符替换为打包后的JS文件地址。

有了这些,访问页面后,通过浏览器查看当前页面元素,就能看到被替换后的模版的 html 全部内容。而不是像 SPA 那样,只有 JS 引用信息,没有当前页面的 HTML 信息。

注意:这里把 ssrData 以及 ssrPath 放到了前端运行时的全局对象 window 上,这个是为了在前端无缝切换成SPA。下面会提到。

前端加载原理

前端加载分为2个部分:

  • 首屏渲染。也就是加载前面提到的 React.createElement() 渲染结果,本质上就是一个纯 html 文件。
  • 切换为单页应用。从纯 html 文件切换成 SPA,此时页面状态(state)和路由(router)都由前端应用接管。

首屏渲染

流程图如下:

e6c9d24egy1h2ricy94bmj20cm0l6dg5.jpg

本质上就是后端将渲染后的html文件,返回给前端。流程图中提到的替换模版内容,在上一部分已经说过了原理。

切换 SPA

流程图如下:

e6c9d24egy1h2riev8mjvj20oe0s00tq.jpg

当加载完首屏的HTML文件,就会加载HTML文件中的JS文件。这些JS文件就是SPA应用打包的结果。加载JS后,整个页面就会变成SPA应用。

为了不丢失首屏渲染的状态和路由信息,ssrDatassrPath在「后端渲染React组件对象」时,挂在 window 对象上的。并且在前端SPA应用中,直接将 window 上的 ssrDatassrPath 传给了深层组件。如下图所示:

e6c9d24egy1h2rij0d202j20x40hydi8.jpg

在Router类的实现中,会检查当前路径是否是 ssrPath(如下图所示):

  • 如果相等,那么当前页面是首次渲染的页面,那么它的 getInitialProps 方法是在NodeJS环境下运行的无法在前端运行,需要将运行结果 ssrData 传下去
  • 如果不想等,那么当前页面一定不会是首次渲染的页面,不需要传 ssrData

e6c9d24egy1h2rimolo4bj211d0u042q.jpg

ssrData传递下去后,组件类会将 ssrData 和组件状态进行合并。组件定义在 client/router/pages 中,通过查看继承关系,可以看到这些组件均继承自 Base 类。

在页面切换路由时,Base 类的构造函数,以及componentWillMount() 函数均会执行。

在 Base 类的构造函数中,会检查是否有 ssrData 从 Router 传递过来。如果有,那么就是首次渲染的页面,需要将 ssrData 赋值给 state。

e6c9d24egy1h2rhgvnawjj20m80mgjsy.jpg

当用户进入页面时,会触发 Base 类的 componentWillMount() 函数。如果有 ssrData,那么当前页面是首次渲染的页面,getInitialProps方法在NodeJS环境下运行,已经在构造函数中,读取并且赋值给了state;如果没有 ssrData,那么就需要在浏览器环境下执行 getInitialProps 方法,并且将结果赋值给 state。

e6c9d24egy1h2riz3pknzj21b40g476q.jpg

从而实现了不同情况下(SPA路由跳转、首屏渲染到SPA应用)的状态处理。

这里的 getInitialProps 是运行在浏览器环境。

特别说明:getInitialProps

老版的Next.js中,getInitialProps 既可以在nodejs环境,也可以在浏览器环境运行,内部会自动处理(类似本文的实现)。

新版的Next.js中,使用 getStaticPropsgetServerSideProps 替换了 getInitialProps。语义更好,直接指明了运行环境。文档如下:

e6c9d24egy1h2rjoppw3gj21340a2q4m.jpg

参考链接