实现Express+React的服务端渲染(ssr)
作者:shizhishi |
发布时间: |
分类:前端进阶 |
声明:个人技术博客 • 非官方 • 仅供技术学习交流
实质上是通过Express建立路由,当用户发起请求时,Express会把打包后的React文件(如build/index.html)转换为字符串,然后将这个页面(字符串形式)发回用户。 在此过程中,Express能通过更改页面(字符串)来注入一些信息。 React有renderToString和hydrateRoot等...
### 一、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(
* );
```
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();在“所以我们需要将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();
// 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(`window.__LINK_ID__ = ''`, `window.__LINK_ID__ = "${initialData.linkId}"`)
.replace(`window.__LINK_KEY__ = ''`, `window.__LINK_KEY__ = "${initialData.linkKey}"`));
}
//第二种插入界面
return res.send(indexHTML.replace(,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'), );
```
### 七、启动项目
先使用 “npm run webpack:build”打包,然后使用 " npm run ssr"启动项目