1. package.json

{
  "name": "react-server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "npm-run-all --parallel dev:**",
    "dev:build:client": "webpack --config config/webpack.client.js --watch",
    "dev:build:server": "webpack --config config/webpack.server.js --watch",
    "dev:start": "nodemon --watch dist --exec node \"./dist/bundle.js\""
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.0.0",
    "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
    "@babel/preset-env": "^7.22.20",
    "@babel/preset-react": "^7.22.15",
    "@babel/preset-stage-0": "^7.8.3",
    "@babel/preset-typescript": "^7.23.0",
    "@reduxjs/toolkit": "^1.9.7",
    "@types/react": "^18.2.27",
    "@types/react-dom": "^18.2.12",
    "antd": "^5.10.0",
    "autoprefixer": "^9.7.3",
    "axios": "^1.5.1",
    "babel-core": "^7.0.0-bridge.0",
    "babel-loader": "7",
    "clean-webpack-plugin": "^3.0.0",
    "cross-env": "^7.0.3",
    "css-loader": "5.0.0",
    "eslint-loader": "^4.0.2",
    "file-loader": "^5.0.2",
    "happypack": "^5.0.1",
    "html-webpack-plugin": "^3.2.0",
    "less": "^3.10.3",
    "less-loader": "5.0.0",
    "lodash": "^4.17.15",
    "mini-css-extract-plugin": "^0.8.0",
    "moment": "^2.24.0",
    "node-sass": "^9.0.0",
    "nodemon": "^3.0.1",
    "npm-run-all": "^4.1.5",
    "optimize-css-assets-webpack-plugin": "^5.0.3",
    "postcss": "^8.4.31",
    "postcss-loader": "^3.0.0",
    "postcss-pxtorem": "5.0.0",
    "react-activation": "^0.12.4",
    "react-redux": "^8.1.3",
    "recoil": "^0.7.7",
    "redux": "^4.2.1",
    "redux-persist": "^6.0.0",
    "sass": "^1.69.3",
    "sass-loader": "5.0.0",
    "style-loader": "^1.0.1",
    "terser-webpack-plugin": "^2.2.2",
    "thread-loader": "^4.0.2",
    "typescript": "^5.2.2",
    "url-loader": "^3.0.0",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.9.0",
    "webpack-merge": "^4.2.2",
    "webpack-node-externals": "^3.0.0",
    "webpack-parallel-uglify-plugin": "^1.1.2",
    "yarn": "^1.22.19"
  },
  "dependencies": {
    "express": "^4.18.2",
    "path": "^0.12.7",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}

2. 新建.babelrc文件

{
    "presets": [
        "@babel/preset-react", 
        "@babel/preset-typescript"
    ],
    "plugins": []
}

3. 新建tsconfig.json文件

{
    "compilerOptions": {
      "target": "es5",
      "lib": [
        "dom",
        "dom.iterable",
        "esnext"
      ],
      "allowJs": true,
      "skipLibCheck": true,
      "esModuleInterop": true,
      "allowSyntheticDefaultImports": true,
      "strict": true,
      "forceConsistentCasingInFileNames": true,
      "module": "esnext",
      "moduleResolution": "node",
      "resolveJsonModule": true,
      "isolatedModules": true,
      "noEmit": true,
      "jsx": "react-jsx",
      "noImplicitAny": false
    },
    "include": [
      "src"
    ]
  }

4. config目录

paths.js

const path = require('path');

const srcPath = path.resolve(__dirname, '..', 'src');
const distPath = path.resolve(__dirname, '..', 'dist');

module.exports = {
    srcPath,
    distPath
}

webpack配置文件

(1) webpack.base.js提取公共配置代码

module.exports = {
    // 打包的规则
    module: {
        rules: [
            {
                test: /\.[jt]sx?$/, // 检测文件类型
                loader: 'babel-loader', // 注意下载babel-loader babel-core
                exclude: /node_modules/, // node_modules目录文件不编译
            }
        ]
    }

}

(2) webpack.server.js服务器配置

使用webpack-merge合并公共配置代码

const nodeExternals = require('webpack-node-externals');
const { distPath, srcPath } = require('./paths');
const path = require('path');
const merge = require('webpack-merge');
const config = require('./webpack.base.js');

const serverConfig = {
    mode: 'production', // 也可以写development
    target: 'node', // 告诉webpack打包的代码是服务器端文件
    entry: './src/server/index.js',
    output: { // 打包生成的文件应该放到哪儿去
        filename: 'bundle.js',
        path: distPath,
    },
    resolve: {
        extensions: ['.tsx', '.ts', '.jsx', '.js'],
        // 针对npm中的第三方模块优先采用jsnext中指向的es6模块语法的文件
        mainFields: ['jsnext:main', 'brower', 'main'],
        alias: {
            "components": path.resolve(srcPath, "components"), //配置样式简单的路径
            "@": srcPath // 把 src 这个常用目录修改为 @
        },
    },
    externals: [
        /**
         * 1、如何让webpackexternals不影响测试环境?
            由于webpackexternals将部分库文件排除在打包范围之外,这样在某些情况下可能会影响单元测试的运行,可以使用webpack-node-externals来排除node_modules目录下的所有依赖项。
         */
        nodeExternals(),
    ],
}

module.exports = merge(config, serverConfig);

(3) webpack.client.js客户端配置

使用webpack-merge合并公共配置代码

const { distPath, srcPath, publicPath } = require('./paths');
const path = require('path');
const merge = require('webpack-merge');
const config = require('./webpack.base.js');

const clientConfig = {
    mode: 'production', // 也可以写development
    entry: './src/client/index.js',
    output: { // 打包生成的文件应该放到哪儿去
        filename: 'index.js',
        path: publicPath,
    },
    resolve: {
        extensions: ['.tsx', '.ts', '.jsx', '.js'],
        // 针对npm中的第三方模块优先采用jsnext中指向的es6模块语法的文件
        mainFields: ['jsnext:main', 'brower', 'main'],
        alias: {
            "components": path.resolve(srcPath, "components"), //配置样式简单的路径
            "@": srcPath // 把 src 这个常用目录修改为 @
        },
    },
}

module.exports = merge(config, clientConfig);

5. Home组件

import React from 'react'

const Home = () => {
    return <div>home !!!!</div>
}

export default Home

6. src/server/index.js

添加script标签是因为模板字符串渲染成dom, onClick等事件没有反应, 所以script标签再同构一下

import express from 'express';
import Home from './containers/home';
import React from 'react';
import { renderToString } from 'react-dom/server'

var app = express();

// 虚拟dom是真实dom的一个JavaScript对象的映射
const content = renderToString(<Home />)
/**
 * 客户端渲染
 * react代码在浏览器上执行, 消耗的是用户浏览器的性能
 * 
 * 服务器渲染
 * react代码在服务器上执行, 消耗的是服务器端的性能(或者资源)
 * 报错信息查询网站: stackoverflow.com
 */

// 只要是静态文件, 都到public目录找
app.use(express.static('public'));

app.get('/', function(req, res) {
    res.send(
        `
        <html>
            <head>
                <title>ssr</title>
            </head>
            <body>
                <div id="root">${content}</div>
                <script src="/index.js"></script>
            </body>
        </html>
        `
    )
})


var server = app.listen(2000);

7. src/client/index.js

import React from "react";
import ReactDOM from "react-dom";
import Home from '../containers/home';

ReactDOM.hydrate(<Home />, document.getElementById('root'));

8. 因为npm-run-all, 所以执行yarn dev就能运行代码并监听组件是否修改了

修改home组件, 刷新浏览器就行了

react简单的服务器渲染示例-LMLPHP

11-07 05:43