这是一个即时短课程的系列笔记。
单页面应用(SPA)在传统的实现)上,面临着首页白屏加载时间过长,seo难以优化的难题。解决这个问题的思路之一就是ssr(服务端渲染)。
无论react或vue,代码都包括组件,store,component等。共同通向一个app.js,我们把app.js通过webpack分成两个bundle,一个是服务端的html(ssr),一个是客户端定义交互操作的js(csr),这个过程称之为同构。
react-dom提供了server的渲染api:renderToString
,它可以把react组件解析为html。因为在服务端渲染,而服务端本身是不支持jsx的。使用babel-loader,可以帮助在服务端解析jsx。
初始化npm
mkdir ssr
cd ssr
npm init -y
在项目中新建src,下面新建App.js
import React,{useState} from 'react';
function App(props){
const [count,setCount]=useState(1);
return <div>
<h1>{props.title}:react ssr</h1>
<span>{count}</span><br/>
<button onClick={()=>{setCount(count+1)}}>+</button>
</div>
}
export default <App title="djtao" />;
在项目根目录新建webpack.server.js
:
// 服务端webpack
const path=require('path');
const nodeExternals=require('webpack-node-externals');
module.exports={
target:'node',
mode:'development',
entry:'./server/index.js',//服务端要打包的入口
externals:[nodeExternals()],
output:{ // 定义输出目录和文件名
filename:'bundle.js',
path:path.resolve(__dirname,'build')
},
module:{
rules:[
{
test:/\.js$/, // 规则
loader:'babel-loader', // 使用babel-loader解析
exclude:/node_modules/,
options:{
// 支持jsx和最新的js写法
presets:['@babel/preset-react',['@babel/preset-env']]
}
}
]
}
}
然后在命令行安装用到的库:
npm i webpack webpack-cli webpack-node-externals @babel/core @babel/preset-env @babel/preset-react -D
安装完之后,开始写服务端。
在package.json增加两条指令
"scripts": {
"dev:server": "webpack --config webpack.server.js --watch",
"dev:start": "node --watch build --exec node \"./build/bundle.js\""
},
npm run dev:server
执行打包服务端的bundle。
npm run dev:start
则是启动你的node服务。
理论上不管服务端选用哪种框架,只要是node环境即可。在此处以express为例.在根目录创建server/index.js
因为已经支持babel,所以可以使用import和jsx了。
import React from 'react';
import {renderToString} from 'react-dom/server';
import express from 'express';
import App from '../src/App';
const app=express();
// 把public作为网站跟路由
app.use(express.static('public'));
app.get('/',(req,res)=>{
// react组件解析为dom
const content=renderToString(App);
// 直接返回一个html模板
res.send(`
<html>
<head>
<meta charset="UTF-8">
<title>react ssr</title>
<body>
<div id="root">${content}</div>
</body>
</head>
</html>
`)
});
// 监听9000端口
app.listen(9000,()=>{
console.log('server is runing..')
});
写完之后安装一下用到的库:
npm i react react-dom express -S
安装好后,ssr就初步完成了。
# 打包服务端bundle.js
npm run dev:server
# 运行node
npm run dev:start
这时你打开http://localhost:9000
,就看到页面了
然而你的计数器是不能用的。点击➕,始终不会有反应
想要真的能交互,离不开客户端js的加载。怎么做呢?我们也在根目录配置一个webpack.client.js——用于在浏览器执行的js:
const path=require('path');
module.exports={
mode:'development',
entry:'./client/index.js', //入口
output:{
filename:'bundle.js', //期望在public下创建bundle
path:path.resolve(__dirname,'public')
},
module:{
rules:[
{
test:/\.js$/,
loader:'babel-loader',
exclude:/node_modules/,
options:{
presets:['@babel/preset-react',['@babel/preset-env']]
}
}
]
}
}
相对于服务端的webpack.server.js,此处省去了很多node才有的配置。
在上面的代码中,我们制定了客户端js的入口,所以在根目录下创建/client/index.js
:
在这里,我们通过hydrate
(react服务端渲染方法,替代旧有的reactDom.render)完成注水工作:
// /client/index.js
import React from 'react';
import ReacDom from 'react-dom';
import App from '../src/App';
// 客户端
// 注水:不需render
ReacDom.hydrate(App,document.querySelector('#root'));
然后增加一条打包/client/index.js
的指令:
"scripts": {
"dev:client": "webpack --config webpack.client.js --watch",
"dev:server": "webpack --config webpack.server.js --watch",
"dev:start": "node --watch build --exec node \"./build/bundle.js\""
},
执行npm run dev:client
,就生成了一个public文件夹,下有你打包好的客户端bundle.js
你的应用想要使用客户端的bundle.js可以在node服务中这么写:
// ...
const app=express();
// 把public作为网站跟路由
app.use(express.static('public'));
app.get('/',(req,res)=>{
// react组件解析为dom
const content=renderToString(App);
// 直接返回一个html模板,带上你的bundle引用!
res.send(`
<html>
<head>
<meta charset="UTF-8">
<title>react ssr</title>
<body>
<div id="root">${content}</div>
<script src="bundle.js"></script>
</body>
</head>
</html>
`)
});
现在可以运行看看了,
# 分别执行客户端和服务端打包
npm run dev:server
npm run dev:client
# 启node服务
npm run dev:start
然后在9000端口,就可以看到计数器了。
如果我想支持更多的服务端渲染,比如router和redux,应该怎么操作呢?请期待下期分解。