这是一个即时短课程的系列笔记。本笔记系列进度已更新到:https://github.com/dangjingtao/react-ssr
ssr把原来在客户端做的渲染工作摆到了服务端。在减少了客户端性能压力的同时,增加了服务端的工作量。那作为一个前端开发者,如何去优化服务端的性能呢?
一个最常见的思路就是负载均衡。如果把我们的服务端比作一个饭馆,用户请求服务就是到饭馆吃饭。那么当前用户,服务端,接口层的的关系是这样的:
在当前的服务端代码中,饭馆能提供什么,上菜速度怎样,全由厨房决定。随着用户的增加,用户A说:我要吃饺子,用户B说,我要吃馄炖,用户C说我要吃汤圆——此时服务器的性能将急剧下降。
但如果我把饭馆定义为一个"用户吃饭的地方",思路将会开拓许多。我们可以在这家饭馆附近租金便宜的地方开设几间厨房。三个用户在点菜之后,饭馆将它转发到对应的厨房去做菜。厨房A专门受理饺子的订单,厨房B专门受理馄炖的订单,厨房C专门受理汤圆的订单。做好之后送回饭馆。那么饭馆就节省了在闹市开设厨房的租金。同时也满足了更多的用户需求。
假设饭馆的名气越来越大,来吃饭的用户越来越多,饭馆已经坐不下了。如何更好地满足用户的需求呢?
在此介绍的另外一个思路就是,降级渲染。饭馆无法容纳更多人,可以考虑把食材做成速冻产品,让用户拿回去自己煮着吃。换成专业一点的语言就是:如果用户量到达一定的阀值,就放弃ssr,重回csr。
首先要实现完整的csr。可以安装html-webpack-plugin
webpack插件。
npm i html-webpack-plugin -S
然后在webpack.client.js配置如下:
const path=require('path');
// 引入插件
const HtmlWebpackPlugin =require('html-webpack-plugin');
module.exports={
mode:'development',
entry:'./client/index.js',
output:{
filename:'bundle.js',
path:path.resolve(__dirname,'public')
},
// 新增插件
plugins:[
new HtmlWebpackPlugin({
filename:'index.csr.html', // 定义渲染的html
template:'src/index.csr.html', // 模版
inject:true
})
],
module:{
rules:[
{
test:/\.js$/,
loader:'babel-loader',
exclude:/node_modules/,
options:{
presets:['@babel/preset-react',['@babel/preset-env']]
}
},
{
test:/\.css$/,
use:['style-loader','css-loader']
}
]
}
}
接下来就在src下新建index.csr.html
<!-- scr 模版,和ssr基本一致 -->
<html>
<head>
<meta charset="UTF-8">
<title>react ssr->csr</title>
<body>
<div id="root"></div>
</body>
</head>
</html>
执行npm start
,你会发现在public文件夹下多出了一个index.csr.html。内容和ssr渲染的一毛一样:
<!-- scr 模版,和ssr基本一致 -->
<html>
<head>
<meta charset="UTF-8">
<title>react ssr->csr</title>
<body>
<div id="root"></div>
<script type="text/javascript" src="bundle.js"></script></body>
</head>
</html>
就是多了一个bundle引入,正是我们想要的。
然后回到服务端,定义一个渲染html模板的方法:
import path from 'path';
import fs from 'fs';
// 开启csr方法
const csrRender=(res)=>{
// 读取scr下的html模板,直接返回
// 读取当前工作目录下的,index.csr.html
const filename=path.resolve(process.cwd(),'public/index.csr.html');
// 同步读取
const html=fs.readFileSync(filename,'utf-8');
return res.send(html);
}
然后在请求方法中直接判断,以请求参数_model作为flag:
app.get('*', (req, res) => {
// 各种开启条件
if(req.query._mode=='csr'){
csrRender(res);
}
// ...
}
访问http://localhost:9000/about?_mode=csr
,就会发现网络请求走的scr路径(title由react ssr变成了 react ssr->csr)
此时浏览器有一个报错:
Warning: Expected server HTML to contain a matching <div> in <div>.
是因为在纯客户端应用时,不能使用注水方法。
// 客户端 client/index.js
if(window.__context){
// 服务端渲染,注水:不需render
ReacDom.hydrate(Page, document.querySelector('#root'));
}else{
ReacDom.render(Page, document.querySelector('#root'));
}
由此,降级逻辑完成。
上节留了一个坑,就是样式。我们在webpack里配置样式,直接是以style插入到html代码中的,所以你只要写了样式,就必定是影响全局的。
css-loader可以配置模块化的样式,在webpack中配置rules:
// webpack.server.js
{
test: /\.css$/,
use: [
'isomorphic-style-loader',
{
loader:'css-loader',
options:{
modules:true
}
}
]
}
// webpack.client.js
{
test:/\.css$/,
use:[
'style-loader',
{
loader:'css-loader',
options:{
modules:true
}
}]
}
然后尝试测试一下,新建index.css:
.title{
color:red;
}
.container{
background: #f5f5f5;
}
然后在index组件中具名引入:
import styles from '../style/index.css';
function Index(){
// ...
return <div className={styles.container}>
<h1 className={styles.title}>react ssr</h1>
{/* ... */}
</div>
}
那么样式就出来了。