一、 Express + React 的服务端渲染
实质上是通过 Express 建立路由,当用户发起请求时,Express 会把打包后的 React 文件 (如 build/index.html) 转换为字符串,然后将这个页面 (字符串形式) 发回用户。
在此过程中,Express 能通过更改页面 (字符串) 来注入一些信息。
React 有 renderToString 和 hydrateRoot 等 api 来帮助服务端渲染。
二、创建项目
首先,需要新建一个 React 项目,这里不做赘述
接着,转到项目中创建 server 文件夹,在文件夹中新建 index.js
具体项目结构如下 (当然,你也可以不在 src 内创建 server)

↑这里的 webpack.config.js 是用于打包的,上面的.babelrc 也是用于打包的
为什么需要这些额外的打包文件?
虽然使用 create-react-app 创建的 react 项目拥有自己的打包命令,但是,我们创建了 express 项目,并在 express 的文件中引入了 React 的格式
const LimitPageString = renderToString(<Limit/>);
Express 和 React 使用两种不同的格式 「module」 和 「commonjs」,因为项目的运行环境是 NodeJs,NodeJS 中,目前有两种标准的模块引入模式,一种是旧的 CommonJS(CJS),另外一种是现代的 ESModule(ESM) 。
有的时候,我们不得不混用这两种引入模式 (一些第三方库仅支持 ESM),这时候就会产生一些坑,比如如果尝试 require(CJS) 一个 ESM 文件时,就会报错。
//可能会出现
const App = require('./build/index').default; // 引入你的 React 应用 Error: Cannot find module '../build/index'
//或
const LimitPageString = renderToString(<Limit/>); 在 「<」 这里报错
//等各种各样的问题
所以我们需要将 React 打包成 commonjs 格式
三、 package.json 配置

主要是框出来的,nodemon 是为了方便查看运行状态,并加入代码热更新 (文件改变时,项目会自动重启),你也可以用 「node」 这个 node 自带的命令
接下来导入一些包
- 首先是 antd 的 pro-comment 的 (假如你用了的情况下),由于在 24 年 8 月左右,这个包的 loah 改为 loah-es,虽然换了个轻量的包 (loah-es),但是这个包的格式由 commonjs 变为 module,但是 pro-comment 仍然使用 commonjs,所以会使 nodejs 报错,所以需要让 pro-comment 退回"2.7.12"版本
- 接着导入一些打包时需要的包
//首先卸载,避免一些版本冲突
npm uninstall @babel/cli @babel/core @babel/preset-env @babel/preset-react babel-cli
//然后再次下载
npm install --save-dev @babel/cli @babel/core @babel/preset-env @babel/preset-react

四、配置.babelrc 和 webpack.config.js
// .babelrc
{
// 预设配置数组,用于指定 Babel 应使用的预设
"presets": [
// 使用 @babel/preset-env 预设,以便于编写符合 ES6+标准的代码,同时保证向后兼容
"@babel/preset-env",
// 使用 @babel/preset-react 预设,用于转换 React 特有的 JSX 语法,使之能在不支持该语法的环境中运行
"@babel/preset-react"
],
// 插件配置数组,目前未指定任何插件
"plugins": [
],
// 禁止 Babel 输出紧凑的代码,设置为 false 后将生成更易读的代码格式,便于调试
"compact": false
}
// webpack.config.js(别忘了下载你没有的包)
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const WebpackBar = require('webpackbar');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TranspilePlugin = require('transpile-webpack-plugin');
const IS_PROD = process.env.NODE_ENV;
const config = {
// Start mode / environment
mode: IS_PROD ? 'production' : 'development',
// Entry files
entry: path.resolve(__dirname, 'src/index'),
// Output files and chunks
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name]-[chunkhash:8].js',
},
// Resolve files configuration
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.scss'],
},
// Module/Loaders configuration
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: 'babel-loader',//用.babelrc 文件来转换格式
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
{
test: /\.module.(sass|scss)$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[path][name]__[local]--[hash:base64:5]',
},
sourceMap: true,
},
},
'sass-loader',
],
},
{
//svg
test: /\.svg$/,
use: [
{
loader: '@svgr/webpack',
options: {
babel: false,
},
},
],
}
],
},
// Plugins
plugins: [
new WebpackBar(),
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.resolve(__dirname, 'public/index.html'),
minify: IS_PROD,
}),
new MiniCssExtractPlugin({
filename: 'styles-[chunkhash:8].css',
}),
],
// Webpack chunks optimization
optimization: {
splitChunks: {
cacheGroups: {
default: false,
venders: false,
vendor: {
chunks: 'all',
name: 'vender',
test: /node_modules/,
},
styles: {
name: 'styles',
type: 'css/mini-extract',
chunks: 'all',
enforce: true,
},
},
},
},
// DevServer for development
devServer: {
port: 3000,
historyApiFallback: true,
},
// Generate source map
devtool: 'source-map',
};
module.exports = config;
五、配置 express
编辑 server/index.js
const path = require('path');
// ignore `.scss` imports
require('ignore-styles');
// 将引入的包转换格式
require('@babel/register')({
configFile: path.resolve(__dirname, '../../.babelrc'),
extensions: ['.js', '.jsx', '.ts', '.tsx'],
});
// 引入具体路由设置
require('./express.js');
编辑 express.js
const fs = require('fs');
const path = require('path');
const express = require('express');
const { renderToString } = require('react-dom/server');
const { get } = require('../util/request');
const React = require('react');
const {Application} = require('../App');
const ApplicationString= renderToString(<Application/>);
// create express application
const app = express();
const indexHTML = fs.readFileSync(
path.resolve(__dirname, '../../dist/index.html'),
{ encoding: 'utf8' }
);
const appHtml = async (req, res) => {
// set header and status
res.contentType('text/html');
res.status(200);
const initialData = JSON.parse(await getInitialData());
//第一种插入数据 (替换 String)
return res.send(indexHTML
.replace(`<script>window.__LINK_ID__ = ''</script>`, `<script>window.__LINK_ID__ = "${initialData.linkId}"</script>`)
.replace(`<script>window.__LINK_KEY__ = ''</script>`, `<script>window.__LINK_KEY__ = "${initialData.linkKey}"</script>`));
}
//第二种插入界面
return res.send(indexHTML.replace(<div id="root"></div>,ApplicationString));
// serve static assets
app.get(
/\.(js|css|map|ico)$/,
express.static(path.resolve(__dirname, '../../dist'))
);
// for any other requests, send `index.html` as a response
app.use('/*',(req, res) => {
appHtml(req, res);
});
// run express server on port 9000
app.listen(9000, () => {
console.log('Express server started at http://localhost:9000');
});
六、配置 React 的 index.js
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import ReactDOM from 'react-dom/client';
// 获取需要预先存储的内容
import Application from './App'
// 获取服务器传递的初始数据
const linkId = window.__LINK_ID__;
const linkKey = window.__LINK_KEY__;
let initialData = {
linkId: linkId,
linkKey: linkKey
}; // 这个数据应该是从服务器端传递过来的
initialData = JSON.stringify(initialData);
const isLimit = window.__IS_LIMIT__;
delete window.__LINK_ID__; // 清除这个全局变量
delete window.__LINK_KEY__;
// 开始渲染应用
hydrateRoot(document.getElementById('root'), <Application initialData={initialData} isLimit={isLimit} />);
七、启动项目
先使用 「npm run webpack:build」 打包,然后使用 " npm run ssr"启动项目

Comments NOTHING