[TOC]
# Server-Side Rendering
![CSR](https://img.kancloud.cn/7e/f7/7ef701ff3069a35bafe3a8285f46379f_1271x989.png)
![SSR](https://img.kancloud.cn/3a/56/3a564b0bc4e6d7b2c483332ed0b1caca_948x619.png)
# 什么时候适合SSR?
React 在有 node 中间层的时候比较适合做 SSR,其实是否 SSR 应该是业务决定的,比如如果你需要做 SEO 那你就需要 SSR,比如新闻网站,内容类网站;对于不需要 SEO 的系统,比如后端系统,webapp,都是不需要 SSR 的。
同构的出发点不是 “为了做同构,所以做了”, 而是回归业务,去解决业务场景中 SEO、首屏性能、用户体验 等问题,驱动我们去寻找可用的解决方案。在这样的场景下,除了同构本身,我们还需要考虑的是:
* 高性能的 Node Server
* 可靠的 同构渲染服务
* 可控的 运维成本
* 可复用的 解决方案
* ...
简单归纳就是,我们需要一个 企业级的同构渲染解决方案。
>网络调到 3G,查看差异:
![ssr](https://img.kancloud.cn/fb/fb/fbfb439565c42ff6bb5928783853d695_700x650.gif)
注意⚠️:对于 单页面 SPA 首页白屏时间长,不利于 SEO 优化的问题。
目前主流的解决方案:服务端渲染 SSR 和 **预渲染技术 prerender**(参见 Vue 手册)。
# 服务端渲染 与 同构
> you have heard a lot of smart people talking about “Isomorphic” or “universal” applications. Some people call it server side rendering (SSR).
>
所谓同构,通俗的讲,就是一套 React 代码在服务器上运行一遍,到达浏览器又运行一遍。服务端渲染完成**页面结构**,浏览器端渲染完成**事件绑定**。
服务端渲染主要侧重**架构层面的实现**,而同构更侧重**代码复用**。
# 理论上的性能优点
使用 CSR 渲染的话,页面很容易白屏。相反,如果你使用 SSR 渲染的话,白屏就不(那么)容易出现啦。尽管大家都知道,使用 CSR(在很大程度上)就意味着页面白屏,不过大多数人还是会使用下面的这种方式来规避(白屏)风险(在服务器返回所有数据之前,给页面添加 loading 图,然后在所有数据到达之后,把 loading 图撤掉)
不过对于使用 SSR 方式渲染出的 HTML 页面来说,用户是可以在这些操作(指的是下载 React、构建虚拟 DOM、绑定事件)完成之前就能看到页面。
反观使用 CSR 方式渲染出的 HTML 页面,你必须等到上面的这些操作(指的是下载 React、构建虚拟 DOM、绑定事件)都完成,virtual-dom 转换成(浏览器)页面上的真实 dom 之后,用户才能看到页面。
## SSR、CSR 两种渲染方式共同点:
* 都需要下载 React 的
* 都需要经历虚拟 DOM 构建过程
* 都需要(给页面元素)绑定事件来增强页面的可交互性
## SSR 问题
1. [在使用 SSR 方式渲染 HTML 页面的过程中,浏览器获取第一个字节的时间(Time To First Byte)要长于用 CSR 渲染 HTML 页面所获取的时间](https://imququ.com/post/transfer-encoding-header-in-http.html),(为啥呢)?
这是因为在你使用 SSR 方式渲染页面的过程中,你服务器需要花更多的时间来渲染出(浏览器所需要的)HTML 结构,(最后才将渲染好的 HTML 结构作为响应返回),而不像 CSR 那样,服务器只需要返回字节相对较少的 Json 数据(relatively empty respons)。
2. SSR 方式渲染 HTML 页面的过程中,服务器的吞吐量会明显少于用 CSR 渲染 HTML 页面时服务器的吞吐量。尤其是当你在服务端使用 react 的时候,(你会发现,是否使用 react 的服务端渲染特性,服务器吞吐量往往也是我们考虑的因素),这是因为 react 对服务器吞吐量的影响太大啦。
`ReactDOMServer.renderToString`具有以下特点:
* 同步方法
* (属于 CPU 独享型),[在调用过程中,会绑定 CPU](http://www.tuicool.com/articles/fiuURnZ)
* 会阻塞(hold)整个事件循环流程
(换句话说),在 `ReactDOMServer.renderToString` 没有执行完之前,服务器是(绝)不可能处理其它请求的。(啥?你说我讲的太抽象啦,完全听不懂),(那好吧,不妨)让我们做个假设(Let’s say ),在使用 SSR 渲染 HTML 页面的过程中,执行`ReactDOMServer.renderToString`就花了 500ms,这意味你现在每秒最多只能处理两个请求。(如果有同学对这方面感兴趣的话,可以重点关注一下)
# 原理
node server 接收客户端请求,
得到当前的 req url path, 然后在已有的路由表内查找到对应的组件,
拿到需要请求的数据,将数据作为 props 、context 或者 store 形式传入组件,
然后基于 react 内置的服务端渲染 api renderToString() or renderToNodeStream() 把组件渲染为 html字符串或者 stream 流 ,
在把最终的 html 进行输出前需要将数据注入到浏览器端 (注水),
server 输出 (response) 后浏览器端可以得到数据 (脱水),浏览器开始进行渲染和节点对比,
然后执行组件的 componentDidMount 完成组件内事件绑定和一些交互,
浏览器重用了服务端输出的 html 节点,整个流程结束。
![](https://img.kancloud.cn/a6/52/a652ca7923c03814c2471b9092c48d16_1280x662.png)
## 服务器端开发一个页面
后端一般都会使用模版(ejs 模板引擎),先看一个 `node ejs` 的栗子:
```
// ejs index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>react ssr <%= title %></title>
</head>
<body>
<%= data %>
</body>
</html>
```
```
const ejs = require('ejs');
const http = require('http');
http.createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, {
'Content-Type': 'text/html'
});
// 渲染文件 index.ejs
ejs.renderFile('./views/index.ejs', {
title: 'react ssr',
data: '首页'},
(err, data) => {
if (err ) {
console.log(err);
} else {
res.end(data);
}
})
}
}).listen(8080);
```
上面渲染出的页面就是有页面结构的,也是我们常说的服务器端页面开发!
# 实战详解
## 前端搭建开发环境
推荐 自定义 webpack ➕ babel形式,CRA 没有 SSR 的配置,所以不推荐,与其他后端可能融合起来费劲!
> [cra-ssr 的 typescript 版本](https://github.com/leidenglai/cra-ssr-ts)
## 服务端实现组件的渲染
`react-dom` 这个库中刚好实现了编译虚拟 DOM 的方法:
```
import { renderToString } from 'react-dom/server';
...
```
但是你会发现问题:
### 事件绑定无效!
当然了,事件,样式这些东西,是浏览器干的事,服务端当然没办法了!唯一的方式就是让浏览器去拉取 JS 文件执行,让 JS 代码来控制。
用 webpack 将按照平常打包,将前端项目编译打包成 js 文件引入到页面中!好的现在浏览器已经可以接管页面的事件等等。
### 路由问题
需要将服务端的路由逻辑执行一遍。
这一需要注意下,在客户端我们采用 `BrowserRouter` 来配置路由,在服务端采用 `StaticRouter` 来配置路由。
1. 客户端配置
```
copyimport React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from "react-router-dom";
import Router from '../router';
function ClientRender() {
return (
<BrowserRouter >
<Router />
</BrowserRouter>
)
}
```
2. 服务端配置
```
copyimport React from 'react';
import { StaticRouter } from 'react-router'
import Router from '../router.js';
function ServerRender(req, initStore) {
return (props, context) => {
return (
<StaticRouter location={req.url} context={context} >
<Router />
</StaticRouter>
)
}
}
export default ServerRender;
```
服务器端路由代码相对要复杂一点,需要你把 location(当前请求路径)传递给 StaticRouter 组件,这样 StaticRouter 才能根据路径分析出当前所需要的组件是谁。(PS:StaticRouter 是 React-Router 针对服务器端渲染专门提供的一个路由组件。)
通过 BrowserRouter 我们能够匹配到浏览器即将显示的路由组件,对浏览器来说,我们需要把组件转化成 DOM,所以需要我们使用 `ReactDom.render` 方法来进行 DOM 的挂载。
而 StaticRouter 能够在服务器端匹配到将要显示的组件,对服务器端来说,我们要把组件转化成字符串,这时我们只需要调用 ReactDom 提供的 `renderToString` 方法,就可以得到 App 组件对应的 HTML 字符串。
对于一个 React 应用来说,路由一般是整个程序的执行入口。在 SSR 中,服务器端的路由和客户端的路由不一样,也就意味着服务器端的入口代码和客户端的入口代码是不同的。
### 多级路由渲染
```
// 增加renderRoutes方法 import { renderRoutes } from 'react-router-config';
```
## 服务端处理css,图片,字体等静态资源
### webpack-manifest-plugin
该插件会生成 一个 `manifest` 文件 大概结构是这样:
```
{
"page1.css": "page1.css",
"page1.js": "page1.js"
}
```
```
const manifest = require('./public/manifest.json');
/**
* 处理链接
* @param {*要进行服务器渲染的文件名默认是build文件夹下的文件} fileName
*/
function handleLink(fileName, req, defineParams) {
...
obj.script = `<script src="${manifest[`${fileName}.js`]}"></script>`;
...
}
```
所以 通过外联的方式,链接样式,可能会造成页面渲染闪现。
### asset-require-hook
使用 asset-require-hook 过滤掉一些类似 `import logo from "./logo.svg";` 这样的资源代码。因为我们服务端只需要纯的 HTML 代码,不过滤掉会报错。这里的 name,我们是去掉了 hash 值的:
```
require("asset-require-hook")({
extensions: ["svg", "css", "less", "jpg", "png", "gif"],
name: '/static/media/[name].[ext]'
});
require("babel-core/register")();
require("babel-polyfill");
require("./app");
```
### webpack-isomorphic-tools
实现方法有多种,我这里使用`webpack-isomorphic-tools`插件来实现,之后会做介绍。
### isomorphic-style-loader
针对第一种使用内联样式,直接把样式嵌入到页面中,需要用到 css–loader 和 style-loader, css-loader 可以继续用,但是 style-loader 由于存在一些跟浏览器相关的逻辑,所以无法在服务器端继续用了,但好在早就有了替代插件,isomorphic-style-loader,此插件用法跟 style-loader 差不多,但是同时支持在服务器端使用
**isomorphic-style-loader**会将导入 css 文件转换成一个对象供组件使用,其中一部分属性是类名,属性的值是类对应的 css 样式,所以可以直接根据这些属性在组件内引入样式,除此之外,还包括几个方法,SSR 需要调用其中的 `_getCss` 方法以获取样式字符串,传输到客户端
鉴于上述过程(即将 css 样式汇总及转化为字符串)是一个通用流程,所以此插件项目内主动提供了一个用于简化此流程的 HOC 组件:withStyles.js
此组件所做的事情也很简单,主要是为 isomorphic-style-loader 中的两个方法:`_insertCss` 和 `_getCss` 提供了一个接口,以 Context 作为媒介,传递各个组件所引用的样式,最后在服务端和客户端进行汇总,这样一来,就能够在服务端和客户端输出样式了
## 数据管理
对于复杂的项目,加上全局状态管理 `Redux` ?
服务器渲染中其顺序是同步的,因此,要想在渲染时出现首屏数据渲染,必须得提前准备好数据。
在服务端通过预取数据交给状态管理 redux 的 store,插入到 `window.__INIT_STORE__`作为初始 store,在客户端拿 `window.__INIT_STORE__`作为初始 store (数据注水),connect 组件得到数据填充(数据脱水):
* 提前获取数据
* 初始化 store
* 根据路由显示组件
* 结合数据和组件生成 HTML,一次性返回
```
window.__INIT_STORE__ = ${JSON.stringify(initStore)}
```
1. 后端渲染出得数据都是同一份?
```
const getStore = (req) => {
return createStore(reducer, defaultState);
}
export default getStore;
```
把这个函数执行就会拿到一个新的 store, 这样就能保证每个用户访问时都是用的一份新的 store。
还可以结合 [react-frontload](https://github.com/davnicwil/react-frontload) 做异步数据获取时的展现;
`react-router` 包里面提供了 [react-router-config](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-config) 主要用于静态路由配置。
提供的 `matchRoutes` API 可以根据传入的 url 返回对应的路由数组。我们可以通过这个方法在服务端直接访问到对应的 React 组件。 如果要从路由中直接获取异步方法,我看了很多类似的同构方案,
~~~
// matchedRoutes 是当前路由对应的所有需要显示的组件集合
matchedRoutes.forEach(item => {
// 组件上的 loadData 帮助服务器端的 Store 获取到这个组件所需的数据。
if (item.route.loadData) {
const promise = new Promise((resolve, reject) => {
item.route.loadData(store).then(resolve).catch(resolve);
})
promises.push(promise);
}
})
Promise.all(promises).then(() => {
// 生成 HTML 逻辑
})
~~~
这里,我们使用 Promise 来解决这个问题,我们构建一个 Promise 并发请求,等待所有的 Promise 都执行结束后,也就是所有 `store.dispatch` 都执行完毕后,再去生成 HTML。这样的话,我们就实现了结合 Redux 的 SSR 流程。
在客户端获取数据,使用的是我们最习惯的方式,通过 `componentDidMount` 进行数据的获取。这里要注意的是,`componentDidMount` 只在客户端才会执行,在服务器端这个生命周期函数是不会执行的。所以我们不必担心 `componentDidMount` 和 **loadData** 会有冲突,放心使用即可。
> 参考代码 [sanyuan0704/react-ssr/server/index.js](https://github.com/sanyuan0704/react-ssr/blob/master/my_ssr/src/server/index.js)
### 当服务端获取数据之后
其实当服务端获取数据之后,客户端并不需要再发送 Ajax 请求了,而客户端的 React 代码仍然存在这样的浪费性能的代码。怎么办呢?
```
componentDidMount() {
//判断当前的数据是否已经从服务端获取
//要知道,如果是首次渲染的时候就渲染了这个组件,则不会重复发请求
//若首次渲染页面的时候未将这个组件渲染出来,则一定要执行异步请求的代码
//这两种情况对于同一组件是都是有可能发生的
if (!this.props.list.length) {
this.props.getHomeList()
}
}
```
### fetch 同构
可以使用 `isomorphic-fetch`、`axios` 或者 `whatwg-fetch + node-fetch` 等库来实现支持双端的 `fetch 数据请求`,这里推荐使用 `axios` 主要是比较方便。
## Webpack 服务器端配置
```
{
"presets": ["@babel/preset-react",
["@babel/preset-env",{
"targets": {
"browsers": [
"ie >= 9",
"ff >= 30",
"chrome >= 34",
"safari >= 7",
"opera >= 23",
"bb >= 10"
]
}
}]
],
"plugins": [
[
"import",
{ "libraryName": "antd", "style": true }
]
]
}
```
这份配置由**服务端和客户端**共用,用来处理 `React` 和转义为 `ES5` 和浏览器兼容问题。
```
// 服务端配置
const serverConfig = {
target: 'node',
entry: {
page1: './web/render/serverRouter.js',
},
resolve,
output: {
filename: '[name].js',
path: path.resolve(__dirname, './app/build'),
libraryTarget: 'commonjs'
},
mode: process.env.NODE_ENV,
externals: [nodeExternals()],
module: {
rules: [
{
test: /\.(jsx|js)?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.(css|less)$/,
use: [
{
loader: 'css-loader',
options: {
importLoaders: 1
}
},
{
loader: 'less-loader',
}
]
}
]
}
};
```
1. `target: 'node'` 由于输出代码的运行环境是node,源码中依赖的 node 原生模块没必要打包进去;
2. `externals: [nodeExternals()]` `webpack-node-externals` 的目的是为了防止 `node_modules`目录下的第三方模块被打包进去;服务端使用 `CommonJS` 规范,而且服务端代码也并不需要构建,因此,对于 `node_modules` 中的依赖并不需要打包,所以借助 `webpack` 第三方模块 `webpack-node-externals` 来进行处理,经过这样的处理,两份构建过的文件大小已经相差甚远了。
3. `{ test: /\.css/, use:["ignore-loader"] }` 忽略掉依赖的 css 文件,css 会影响服务端渲染性能,又是做服务端渲染不重要的部分;
4. `libraryTarget: "commonjs2",` 以 commonjs2规范导出渲染函数,以供给采用nodejs编写的http服务器代码调用。
## 优化项目
## 实现按需加载
主要使用的是 `react-loadable` 包来实现按需加载,在 `SSR` 增加这个配置相对比较繁琐,但是官网基本已经给出详细的步骤[详细配置流程](https://github.com/jamiebuilds/react-loadable)。
### HTML 模板
这里以 nunjucks 模版引擎示例,在渲染 `HTML` 时,会将有 `< >` 进行安全处理,因此,我们还需对我们传入的数据进行过滤处理。
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>koa-React服务器渲染</title>
{{ link | safe }}
</head>
<body>
<div id='app'>
{{ html | safe }}
</div>
</body>
<script>
window.__INIT_STORE__ = {{ store | safe }}
</script>
{{ script | safe }}
</html>
```
# API
## renderToString
将 `React` 元素渲染到其初始 `HTML` 中。 该函数应该只在服务器上使用。 `React` 将返回一个 `HTML` 字符串。 您可以使用此方法在服务器上生成 `HTML` ,并在初始请求时发送标记,以加快网页加载速度,并允许搜索引擎抓取你的网页以实现 `SEO` 目的。
## renderToStaticMarkup
类似于 `renderToString` ,除了这不会创建 `React` 在内部使用的额外 `DOM` 属性,如 `data-reactroot`。 如果你想使用 `React` 作为一个简单的静态页面生成器,这很有用,因为剥离额外的属性可以节省一些字节。
但是如果这种方法是在浏览访问之后,会全部替换掉服务端渲染的内容,因此会造成页面闪烁,所以并不推荐使用该方法。
*****
`data-reactroot`简单的说就是`react 组件`的一个唯一标示 id, 具体可以去 google 下
**对于服务端渲染而言**
* 使用`renderToStaticMarkup`渲染出的是不带`data-reactid`的纯`html`在前端`react.js`加载完成后, 之前服务端渲染的页面会抹掉之前服务端的重新渲染(可能页面会闪一下). 换句话说 前端`react`就根本就不认识之前服务端渲染的内容,`render`方法会使用`innerHTML`的方法重写`#react-target`里的内容
* 而使用`renderToString`方法渲染的节点会带有`data-reactid`属性, 在前端`react.js`加载完成后, 前端`react.js`会认识之前服务端渲染的内容, 不会重新渲染`DOM 节点`, 前端`react.js`会接管页面, 执行`componentDidMout``绑定浏览器事件`等 这些在服务端没完成也不可能执行任务。
> https://reactjs.org/docs/react-dom-server.html#rendertostaticmarkup
## ReactDOM.hydrate()
`ReactDOM.hydrate()`与 `render()` 相同,但是它对 由 `ReactDOMServer` 渲染出 HTML 内容的容器 进行补充水分(附加事件侦听器), React 将尝试将事件侦听器附加到现有标记。
使用`ReactDOM.render()` 对服务器渲染的容器进行水合是因为速度较慢,因此已被弃用,并将在 React 17 中被删除,因此请使用`hydrate() `代替。
> [react 中出现的 "hydrate" 这个单词到底是什么意思? ](https://www.zhihu.com/question/66068748)
> [hydrate() and render() in React 16](https://stackoverflow.com/questions/46516395/whats-the-difference-between-hydrate-and-render-in-react-16)
## renderToNodeStream()
React 16 最新发布的东西,它支持直接渲染到节点流。
渲染到流可以减少你的内容的第一个字节`(TTFB)`的时间,在文档的下一部分生成之前,将文档的开头至结尾发送到浏览器。返回一个 可读的流 (`stream`) ,即输出 `HTML` 字符串。这个 流 (`stream`) 输出的 `HTML` 完全等同于 `ReactDOMServer.renderToString` 将返回的内容。
当内容从服务器流式传输时,浏览器将开始解析 HTML 文档。速度是 `renderToString` 的`三倍`
我们也可以使用上述 `renderToNodeSteam` 将其改造下:
```
let element = React.createElement(dom(req, defineParams));
ctx.res.write('
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>koa-React服务器渲染</title>
</head><body><div id="app">');
// 把组件渲染成流,并且给Response
const stream = ReactDOMServer.renderToNodeStream(element);
stream.pipe(ctx.res, { end: 'false' });
// 当React渲染结束后,发送剩余的HTML部分给浏览器
stream.on('end', () => {
ctx.res.end('</div></body></html>');
});
```
## renderToStaticNodeStream()
类似于 `renderToNodeStream` ,除了这不会创建 `React` 在内部使用的额外 `DOM` 属性,如 `data-reactroot` 。 如果你想使用 `React` 作为一个简单的静态页面生成器,这很有用,因为剥离额外的属性可以节省一些字节。
这个 流 (`stream`) 输出的 `HTML` 完全等同于 `ReactDOMServer.renderToStaticMarkup` 将返回的内容。
# SEO 解决
```
<title>测试</title>
<meta name="Description" content="测试"/>
<meta name="Keywords" content="测试"/>
```
1. title
用于浏览器标签栏的显示标题、网页爬虫爬取后的标题。
最好控制在 30 字内。
2. description
用于描述网站信息,会显示在搜索结果中。
简要概述网站主要内容
3. keywords
用于匹配搜索引擎的搜索结果。
最好控制在 3 个左右。
三者的合理设置都有利于 SEO
## React-Helmet 的使用
```
npm i -S react-helmet
```
```
// 拿到 helmet 对象,然后在 html 字符串中引入
const helmet = Helmet.renderStatic();
// 模版中渲染
${helmet.title.toString()}
${helmet.meta.toString()}
```
> [网站的 TDK 该怎么设置?它有什么作用?](https://github.com/haizlin/fe-interview/issues/1009)
# 部署
[React 服务端渲染 + pm2 自动化部署](https://juejin.im/post/5b55e6a96fb9a04fcf59d754)
# 总结
实际上SSR开发通常是在一个项目基础上改,而不是重新搭建一个项目,比较很多人拿它当做优化,而不是重构。
通常来说我们一个项目按照 SPA 模式开发,针对特定页面做 SSR 的修改,修改之后的项目既可以SPA 也可以 SSR,只不过SPA 模式时对应页面获取不到数据,因为获取数据的方法均被修改。
所谓同构,其实就是服务端借助客户端的JS去渲染页面,没有影响到客户端的JS,还是正常打包,客户端做代码分割也不会受影响。
SSR 实现方式并不唯一,还有很多其他的方式, 比如 `next.js`, `umi.js`, 但是原理相似。
# 其他工具
## 大量文本操作
[cheerio](https://github.com/cheeriojs/cheerio)
## 业界生态
[Egg + React + SSR 服务端渲染](http://ykfe.net/guide/#%E5%88%9D%E8%A1%B7)
https://imajs.io/
https://catberry.org/
[Easy-team](https://www.yuque.com/easy-team)
[next.js](https://github.com/zeit/next.js):轻量级的同构框架
[react-server](https://react-server.io/):React 服务端渲染框架
[razzle](https://github.com/jaredpalmer/razzle):通用服务端渲染框架
[beidou](https://github.com/alibaba/beidou):阿里自己的同构框架,基于 eggjs, 定位是企业级同构框架
除了开源框架,底层方面 React16 重构了 SSR, react-router 提供了更加友好的 SSR 支持等等,从某种程度上来说,同构也是一种趋势,至少是方向之一。
# 参考
[DrReMain/egg-react-ssr/app/controller/home.js](https://github.com/DrReMain/egg-react-ssr/blob/master/app/controller/home.js)
[ykfe/egg-react-ssr/ssr-with-ts/src/app/controller/page.ts](https://github.com/ykfe/egg-react-ssr/blob/dev/example/ssr-with-ts/src/app/controller/page.ts)
[如何搭建一个高可用的服务端渲染工程](https://www.infoq.cn/article/Ugb49pzllUvbzeCdD6xw)
[从头开始,彻底理解服务端渲染原理 (8 千字汇总长文)](https://juejin.im/post/5d1fe6be51882579db031a6d#heading-21)
[从零开始 React 服务器渲染(SSR)同构😏(基于 Koa)](https://juejin.im/post/5c627d9b6fb9a049f23d3e38#heading-21)
[打造高可靠与高性能的 React 同构解决方案](https://github.com/alibaba/beidou/blob/master/packages/beidou-docs/articles/high-performance-isomorphic-app.md)
[Server-Side Rendering with React, Redux, and React-Router](https://itnext.io/server-side-rendering-with-react-redux-and-react-router-fa5b67d4965e)
[【长文慎入】一文吃透 React SSR 服务端渲染和同构原理](https://juejin.im/post/5d7deef6e51d453bb13b66cd#heading-30)