什么是 CSR & SSR ?
CSR
(Client Side Rendering)就是在浏览器从服务器中获取到的只是一个带有空 div 标签的 html 文件,然后执行 js 文件生成 dom 和操作 dom,日常中开发的后台管理类的系统大多都是 CSR 的模式。
SSR
(Server Side Rendering)是在服务端已经完成渲染工作,浏览器从服务器获得的是完整的网页的 dom 字符串。不同于以前通过后端模板等方案生成页面,现在的 React、Vue、Svelte 等优秀框架都有 SSR 的解决方案。
为什么要使用 SSR?
关于这个问题尤大在 Vue SSR 指南中也给出了答案。
SSR 对比 CSR 的优点:
- 更好的 SEO:对 SSR 的应用搜索引擎可以直接获取完全渲染的页面,但是在 CSR 的应用中搜索引擎获取到的只是一个空标签。
- 更快的内容到达时间。
当然 SSR 对比 CSR 也有一些缺点:
- 引用成本高:SSR 还需要使用 Node.js 作为服务器,对于代码构建和部署也要求更高,加大了开发成本
- 更多的服务器负载。
- 传统开发思路受限:在开发的时候要区分是 Node.js 环境还是浏览器环境,部分生命周期在服务器端也不会生效。
所以在选择技术栈的时候可以根据项目需求去选择,如果对 SEO 和加载速度有要求的时候就可以使用 SSR。反之如果没有 SEO 或首屏加载优化等需求,使用 SSR 也可能是一种负担。
more:
如果单纯的想对个别数据变动不频繁的页面做 SEO,可以考虑预渲染的方案,毕竟 SSR 的成本是相对较高的。
- 预渲染:预渲染就是把页面生成静态的 html 结构,然后进行静态部署。实现方案一般是通过一个无头浏览器打开系统,等到 js 执行完毕,dom 渲染完毕后通过
XMLSerializer
API 进行处理最后生成静态 html 字符串。
SSR 构建流程
下图来自 Vue 官网,详细展示了 SSR 项目的构建流程,虽然 React 和 Vue 的实现方式略有不同,但是整体思路也是如此:
- Store、Router、Components、app.js 这些模块(以下简称web模块)是公共的,在 SSR 的情况下这些模块既要在客户端使用,也要在服务端使用。
- web 模块被 Cliententry 引用通过 webpack 打包作为静态资源使用。
- web 模块被 Serverentry 引用通过 webpack 打包被 Node.js 服务 调用,用于请求页面的时候,进行组件渲染返回 html 字符串。
- 最终 Node.js Server 返回的 html 字符串和 js 加载生成的 html 在浏览器端进行同构。
由上图也可以初步新建如下的文件目录:
.
├── build
│ ├── base.config.js
│ ├── client.config.js
│ └── server.config.js
├── entry
│ ├── server-entry.jsx
│ └── client-entry.jsx
├── server
│ └── app.js
└── web├── components│ └── Test.jsx├── index.jsx└── pages└── Index.jsx
- build:webpack 构建的目录
- entry:入口文件的目录
- server:Node.js 服务器的目录
- web:前端代码和资源目录
webpack 构建服务端 bundle
webpack 构建主要是把 web 目录下的文件分别通过 client 和 server 的配置打包成两份代码,这里不再介绍客户端代码的打包,主要介绍一下服务端代码的打包。
const WebpackChain = require('webpack-chain');
const nodeExternals = require('webpack-node-externals');
const {resolvePath,isDev
} = require('./utils');
module.exports = {getServerConfig: function() {const chain = new WebpackChain();chain.entry('server').add(resolvePath('entry/server-entry.js')).end().output.path(resolvePath('dist/server')).filename('[name].js').libraryTarget('commonjs2').end().when(isDev, function(chain) {chain.watch(true);}).target('node').externals(nodeExternals({allowlist: [/.(css|less|sass|scss)$/]}));return chain.toConfig();}
}
可以看到,服务器端打包和客户端的打包有一点区别:
- target: ‘node’,target 设置为
node
,webpack 将在类 Node.js 环境编译代码。(使用 Node.js 的require
加载 chunk,而不加载任何内置模块,如fs
或path
)。每个target都包含各种 deployment(部署)/environment(环境)特定的附加项,以满足其需求。 - output 的 libraryTarget 设置为 ‘commonjs2’ 打包输出的代码将在 Node.js 环境下运行。
- 使用 nodeExternals 后,代码将不会被 webpack 打包,因为服务端的代码自带 node_modules,可以直接去 node_modules 中获取。
React 服务端渲染 API
在开始编写代码之前,我们需要了解react实现服务端渲染必须的几个 API,参考 ReactDOMServer
- renderToString 把 React 元素渲染 html 字符串。
function Comp () {return ( <div>123</div>)
}
renderToString(<Comp />); // 返回 <div>123</div>
- renderToNodeStream 把 React 元素渲染成 html,和 renderToString 不同的是,该方法返回一个可输出 HTML 字符串的
可读流
- hydrate 如果您调用
ReactDOM.hydrate()
已经具有此服务器渲染标记的节点,React 将保留它并仅附加事件处理程序,从而使您获得非常出色的首次加载体验。在 SSR 应用中使用hydrate
替代render
ReactDOM.h