# 一、客户端渲染与服务端渲染
# 1.1 什么是客户端渲染
react在客户端执行,消耗客户端性能。客户端渲染,页面初始加载的
HTML页面中无网页展示内容,需要加载执行JavaScript文件中的React代码,通过JavaScript渲染生成页面,同时,JavaScript代码会完成页面交互事件的绑定,详细流程可参考下图
客户端渲染流程
浏览器发送请求–>服务器返回HTML–>浏览器发送bundle.js请求–>服务器返回bundle.js–>浏览器执行bundle.js中的react代码完成渲染

# 1.2 什么是服务端渲染
react在服务端执行,消耗服务端性能。我们所说的服务端渲染,只是首次加载页面的时候,后面都是通过前端路由进行客户端渲染
- 用户请求服务器,服务器上直接生成
HTML内容并返回给浏览器。服务器端渲染来,页面的内容是由Server端生成的。一般来说,服务器端渲染的页面交互能力有限,如果要实现复杂交互,还是要通过引入JavaScript文件来辅助实现
服务端渲染流程
浏览器发送请求–>服务器运行React代码生成页面–>服务器返回页面
# 1.3 什么是同构
一套react代码,在服务端执行一次,在客户端也执行一次。在服务端执行同构
renderToString只是返回界面展示,并不能绑定事件,需要在客户端再次执行js代码绑定事件
服务器运行React代码渲染出HTML–>发送HTML给浏览器–>浏览器接收到内容展示–>浏览器加载js文件–>Js中的React代码在浏览器端重新执行–>JS中的React代码接管页面操作
路由同构
让路由在服务端、客户端各跑一遍
# 1.4 使用SSR优劣势
一般情况下,当我们使用
React编写代码时,页面都是由客户端执行JavaScript逻辑动态挂DOM生成的,也就是说这种普通的单页面应用实际上采用的是客户端渲染模式。在大多数情况下,客户端渲染完全能够满足我们的业务需求,那为什么我们还需要SSR这种同构技术呢
CSR项目的TTFP(Time To First Page)时间比较长,参考之前的图例,在CSR的页面渲染流程中,首先要加载HTML文件,之后要下载页面所需的JavaScript文件,然后JavaScript文件渲染生成页面。在这个渲染过程中至少涉及到两个HTTP请求周期,所以会有一定的耗时,这也是为什么大家在低网速下访问普通的React或者Vue应用时,初始页面会有出现白屏的原因CSR项目的SEO能力极弱,在搜索引擎中基本上不可能有好的排名。因为目前大多数搜索引擎主要识别的内容还是HTML,对JavaScript文件内容的识别都还比较弱。如果一个项目的流量入口来自于搜索引擎,这个时候你使用 CSR 进行开发,就非常不合适了
SSR的产生,主要就是为了解决上面所说的两个问题。在React中使用 SSR 技术,我们让 React 代码在服务器端先执行一次,使得用户下载的 HTML 已经包含了所有的页面展示内容,这样,页面展示的过程只需要经历一个HTTP请求周期,TTFP时间得到一倍以上的缩减
- 同时,由于
HTML中已经包含了网页的所有内容,所以网页的SEO效果也会变的非常好。之后,我们让React代码在客户端再次执行,为HTML网页中的内容添加数据及事件的绑定,页面就具备了React的各种交互能力
但是,
SSR这种理念的实现,并非易事。我们来看一下在React中实现SSR技术的架构图:

- 使用
SSR这种技术,将使原本简单的React项目变得非常复杂,项目的可维护性会降低,代码问题的追溯也会变得困难 - 所以,使用
SSR在解决问题的同时,也会带来非常多的副作用,有的时候,这些副作用的伤害比起SSR技术带来的优势要大的多。从个人经验上来说,我一般建议大家,除非你的项目特别依赖搜索引擎流量,或者对首屏时间有特殊的要求,否则不建议使用SSR
# 二、SSR的实现本质
SSR 之所以能够实现,本质上是因为虚拟 DOM 的存在
SSR的工程中,React代码会在客户端和服务器端各执行一次。你可能会想,这没什么问题,都是 JavaScript 代码,既可以在浏览器上运行,又可以在Node环境下运行。但事实并非如此,如果你的React代码里,存在直接操作DOM的代码,那么就无法实现SSR这种技术了,因为在Node环境下,是没有DOM这个概念存在的,所以这些代码在Node环境下是会报错的- 在
React框架中引入了一个概念叫做虚拟 DOM,虚拟 DOM 是真实 DOM 的一个 JavaScript 对象映射,React 在做页面操作时,实际上不是直接操作 DOM,而是操作虚拟 DOM,也就是操作普通的 JavaScript 对象,这就使得 SSR 成为了可能。在服务器,我可以操作 JavaScript 对象,判断环境是服务器环境,我们把虚拟DOM映射成字符串输出;在客户端,我也可以操作 JavaScript 对象,判断环境是客户端环境,我就直接将虚拟 DOM 映射成真实 DOM,完成页面挂载
# 三、SSR中服务器端路由和客户端路由区别
实现
React的SSR架构,我们需要让相同的React代码在客户端和服务器端各执行一次。大家注意,这里说的相同的React代码,指的是我们写的各种组件代码,所以在同构中,只有组件的代码是可以公用的,而路由这样的代码是没有办法公用的,大家思考下这是为什么呢?其实原因很简单,在服务器端需要通过请求路径,找到路由组件,而在客户端需通过浏览器中的网址,找到路由组件,是完全不同的两套机制,所以这部分代码是肯定无法公用。我们来看看在 SSR 中,前后端路由的实现代码
# 3.1 客户端路由
const App = () => {
return (
<Provider store={store}>
<BrowserRouter>
<div>
<Route path='/' component={Home}>
</div>
</BrowserRouter>
</Provider>
)
}
ReactDom.render(<App/>, document.querySelector('#root'))
客户端路由代码非常简单,大家一定很熟悉,
BrowserRouter会自动从浏览器地址中,匹配对应的路由组件显示出来
# 3.2 服务器端路由
const App = () => {
return
<Provider store={store}>
<StaticRouter location={req.path} context={context}>
<div>
<Route path='/' component={Home}>
</div>
</StaticRouter>
</Provider>
}
Return ReactDom.renderToString(<App/>)
服务器端路由代码相对要复杂一点,需要你把
location(当前请求路径)传递给StaticRouter组件,这样StaticRouter才能根据路径分析出当前所需要的组件是谁。(PS:StaticRouter是React-Router针对服务器端渲染专门提供的一个路由组件。)
- 通过
BrowserRouter我们能够匹配到浏览器即将显示的路由组件,对浏览器来说,我们需要把组件转化成DOM,所以需要我们使用ReactDom.render方法来进行DOM的挂载。而StaticRouter能够在服务器端匹配到将要显示的组件,对服务器端来说,我们要把组件转化成字符串,这时我们只需要调用ReactDom提供的renderToString方法,就可以得到App组件对应的HTML字符串。 - 对于一个
React应用来说,路由一般是整个程序的执行入口。在SSR中,服务器端的路由和客户端的路由不一样,也就意味着服务器端的入口代码和客户端的入口代码是不同的 - 我们知道,
React代码是要通过Webpack打包之后才能运行的,实际上是源代码打包过后生成的代码。上面也说到,服务器端和客户端渲染中的代码,只有一部分一致,其余是有区别的。所以,针对代码运行环境的不同,要进行有区别的Webpack打包
# 四、服务端和客户端打包配置
# 4.1 客户端 Webpack 配置
{
entry: './src/client/index.js',
output: {
filename: 'index.js',
path: path.resolve(__dirname, 'public')
},
module: {
rules: [{
test: /\.js?$/,
loader: 'babel-loader'
},{
test: /\.css?$/,
use: ['style-loader', {
loader: 'css-loader',
options: {modules: true}
}]
},{
test: /\.(png|jpeg|jpg|gif|svg)?$/,
loader: 'url-loader',
options: {
limit: 8000,
publicPath: '/'
}
}]
}
}
# 4.2 服务器端 Webpack 配置
{
target: 'node',
entry: './src/server/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'build')
},
externals: [nodeExternals()],
module: {
rules: [{
test: /\.js?$/,
loader: 'babel-loader'
},{
test: /\.css?$/,
use: ['isomorphic-style-loader', {
loader: 'css-loader',
options: {modules: true}
}]
},{
test: /\.(png|jpeg|jpg|gif|svg)?$/,
loader: 'url-loader',
options: {
limit: 8000,
outputPath: '../public/',
publicPath: '/'
}
}]
}
};
# 五、SSR 中异步数据的获取 + Redux 的使用
客户端渲染中,异步数据结合 Redux 的使用方式遵循下面的流程
- 创建
Store - 根据路由显示组件
- 派发
Action获取数据 - 更新
Store中的数据 - 组件
Rerender
而在服务器端,页面一旦确定内容,就没有办法 Rerender 了,这就要求组件显示的时候,就要把 Store 的数据都准备好,所以服务器端异步数据结合 Redux 的使用方式
服务器端异步数据结合 Redux流程
- 创建
Store - 根据路由分析
Store中需要的数据 - 派发
Action获取数据 - 更新
Store中的数据 - 结合数据和组件生成
HTML,一次性返回
# 5.1 服务器端渲染的流程
创建
Store:这一部分有坑,要注意避免,大家知道,客户端渲染中,用户的浏览器中永远只存在一个Store,所以代码上你可以这么写
const store = createStore(reducer, defaultState)
export default store;
# 六、处理styled-components样式
# 七、Node 只是一个中间层
