我们知道React等前端框架默认都是SPA+CSR (单页应用+客户端渲染),所谓客户端渲染就是服务端返回:空页面 + JS bundle文件,在客户端浏览器再通过JS bundle对指定的DOM节点进行页面渲染(生成html、css)。
这种CSR模式有两个主要问题
1:在加载完js bundle文件前,页面是空白的
2:搜索引擎不友好
所以,电商应用都会选择SSR模式(服务端渲染)。
那么如何实现React下的SSR呢? 大致分为两类:使用第三方框架(如NextJS)、使用React API自己实现SSR。
本文讨论如何用原生React API实现SSR。
//建立项目文件夹
mkdir my-react-ssr
//建立子文件夹
cd my-react-ssr
mkdir public src build server
cd src
mkdir server
//建立代码文件
touch publc/index.html
touch src/App.js
touch server/index
安装依赖:
npm init -y
npm install express --save
npm install react react-dom --save
npm install @babel/core @babel/preset-env @babel/preset-react babel-loader --save-dev
npm install webpack webpack-cli webpack-node-externals --save-dev
npm install clean-webpack-plugin --save-dev
npm install npm-run-all nodemon --save-dev
//以下部分为typescript准备
npm install @types/react @types/react-dom @babel/preset-typescript --save-dev
配置webpack
touch webpack.config.js
touch webpack.server.js
touch .babelrc.json
至此我们完成了配置和依赖,最后是代码部分
public/index.html 该文件相当于页面模板,其中的css、js目前都是hardcode,后面的文章再讨论如何动态生成该模板。
<!DOCTYPE html>
<html lang="en">
<head>
<title>React SSR Demo</title>
<link rel="stylesheet" href="https://res.ebdcdn.com/static/css/global.1622171660.css"></link>
<link rel="stylesheet" href="https://res.ebdcdn.com/static/css/_ed3f20,collections.css,collections-studio.css"></link>
</head>
<body>
<div id="root"></div>
<script src="app.bundle.js" async></script>
</body>
</html>
src/server/index.js 该处使用express作为webserver,根据html模板和react组件生成html,注意:这里使用的renderToStaticMarkup,而非renderToString
import path from 'path';
import fs from 'fs';
import React from 'react';
import express from 'express';
import ReactDOMServer from 'react-dom/server';
//import Home from '../components/Home';
import ProdList from '../components/ProdList';
const PORT = 3006; //process.env.PORT || 3006;
const app = express();
app.get('/', (req, res) => {
//const app = ReactDOMServer.renderToString(<Home />);
const app = ReactDOMServer.renderToStaticMarkup(<ProdList />);
const indexFile = path.resolve('./public/index.html');
fs.readFile(indexFile, 'utf8', (err, data) => {
if (err) {
console.error('Something went wrong:', err);
return res.status(500).send('Oops, better luck next time!');
}
return res.send(
data.replace('<div id="root"></div>', `<div id="root" name="root123" attr1='001'>${app}</div>`)
);
});
});
app.use(express.static('./build'));
app.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`);
});
src/App.js 该处代码中的hydrate意在当js bundle下载完成后,需要与页面现有的html(服务端renderToStaticMarkup生成的)进行调和
import React from 'react';
import ReactDOM from 'react-dom';
//import Home from './components/Home';
import ProdList from "./components/ProdList";
//ReactDOM.hydrate(<Home />, document.getElementById('root'));
ReactDOM.hydrate(<ProdList />, document.getElementById('root'));
.bablerc.json
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
package.json 中的scripts部分
"scripts": {
"build:client": "webpack --config webpack.config.js --mode=production",
"build:server": "webpack --config webpack.server.js --mode=development",
"start": "nodemon ./server/index.js",
"all": "npm-run-all --parallel build:* start"
},
webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const path = require('path');
module.exports = {
entry: {app: './src/app.js'},
module: {
rules: [
{
test: /\.(ts|js)x?$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript",
],
},
}
}
// {
// test: /\.tsx?$/,
// use: 'ts-loader',
// exclude: [/node_modules/, /tests/],
// },
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
path: path.join(__dirname, 'build'),
filename: '[name].bundle.js'
},
plugins: [
new CleanWebpackPlugin()
]
};
webpack.server.js
const path = require('path');
const nodeExternals = require('webpack-node-externals');
module.exports = {
entry: {
index: './src/server/index.js',
},
target: 'node',
externals: [nodeExternals()],
output: {
path: path.resolve('server'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
}
]
}
};
虽然这样也能实现SSR,但有几个明显的缺陷
1:express对路由支持太差
2:webconfig等配置、模板文件中的bundle文件需要手动修改
3:还没有实现css的加载
上述问题会在后续文章中逐一解决。