[TOC]
# Server-Side Rendering


# 什么時候適合SSR?
React 在有 node 中間層的時候比較適合做 SSR,其實是否 SSR 應該是業務決定的,比如如果你需要做 SEO 那你就需要 SSR,比如新聞網站,內容類網站;對于不需要 SEO 的系統,比如后端系統,webapp,都是不需要 SSR 的。
同構的出發點不是 “為了做同構,所以做了”, 而是回歸業務,去解決業務場景中 SEO、首屏性能、用戶體驗 等問題,驅動我們去尋找可用的解決方案。在這樣的場景下,除了同構本身,我們還需要考慮的是:
* 高性能的 Node Server
* 可靠的 同構渲染服務
* 可控的 運維成本
* 可復用的 解決方案
* ...
簡單歸納就是,我們需要一個 企業級的同構渲染解決方案。
>網絡調到 3G,查看差異:

注意??:對于 單頁面 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 節點,整個流程結束。

## 服務器端開發一個頁面
后端一般都會使用模版(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)