企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[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)