react简单的服务器渲染示例

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, "containers/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, "containers/components"), //配置样式简单的路径
            "@": srcPath // 把 src 这个常用目录修改为 @
        },
    },
}

module.exports = merge(config, clientConfig);

5. src/containers/组件页面目录

routes.tsx

复制代码
import React from 'react'
import { Route } from 'react-router-dom'
import Home from './containers/home'
import Login from './containers/login'


export default (
    <div>
        <Route path='/' component={Home} exact />
        <Route path='/login' component={Login} exact />
    </div>
)

home/index.tsx

复制代码
import React from 'react'

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

export default Home

login/index.tsx

复制代码
import React from 'react'
import Header from '../components/header';

const Login = () => {
    return <div>
        <Header />
        
        <div>login</div>
        </div>
}

export default Login;

components/公共组件目录

header/index.tsx

复制代码
import React from 'react'
import {Link} from 'react-router-dom'

// 同构--同一套react代码, 在服务器执行一次, 在客户端再执行一次
const Header = () => {
    return <div>
        <Link  to="/">HOME</Link>
        &nbsp;&nbsp;&nbsp;&nbsp;
        <Link to="/login">LOGIN</Link>
    </div>
}

export default Header

6. src/server/服务端代码目录

index.js

复制代码
import express from 'express';
import React from 'react';
import {render} from './utils'

var app = express();


/**
 * 客户端渲染
 * react代码在浏览器上执行, 消耗的是用户浏览器的性能
 * 
 * 服务器渲染
 * react代码在服务器上执行, 消耗的是服务器端的性能(或者资源)
 * 报错信息查询网站: stackoverflow.com
 */

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

// * => 任意路径都能走到下列的方法
app.get('*', function (req, res) {   
    res.send(render(req))
})


var server = app.listen(2000);

utils.js

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

复制代码
import React from 'react';
import { renderToString } from "react-dom/server"
import { StaticRouter } from 'react-router-dom'
import Routes from '../routes'

export const render = (req) => {
     // 虚拟dom是真实dom的一个JavaScript对象的映射
     const content = renderToString((
        <StaticRouter location={req.path} context={{}}>
            {Routes}
        </StaticRouter>
    ))
    return (
        `
        <html>
            <head>
                <title>ssr</title>
            </head>
            <body>
                <div id="root">${content}</div>
                <script src="/index.js"></script>
            </body>
        </html>
        `
    )
}

7. src/client/客户端代码目录

index.js

复制代码
import React from "react";
import ReactDOM from "react-dom";
import Home from '../containers/home';
import {BrowserRouter} from 'react-router-dom'
import Routes from '../routes'

const App = () => {
    return <BrowserRouter>
        {Routes}
    </BrowserRouter>
}
ReactDOM.hydrate(<App />, document.getElementById('root'));

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

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

相关推荐
lllsure5 小时前
Linux 日志管理
linux·运维·服务器
教练、我想打篮球5 小时前
123 safari 浏览器中下载 URLEncoder.encode 的中文名称的文件, safari 未进行解码, 其他浏览器正常
前端·http·safari
haluhalu.6 小时前
Linux系统下进程池设计与实现详解
linux·运维·服务器
虹梦未来6 小时前
【运维】Ubuntu2404使用新风格更新镜像源
运维·服务器
小白x6 小时前
Echarts常用配置
前端
hello_Code6 小时前
css和图片主题色“提取”
前端
小杨梅君6 小时前
Vue3与iframe通信方案详解:本地与跨域场景
前端·vue.js
IT_陈寒6 小时前
Redis高频踩坑实录:5个不报错但会导致性能腰斩的'隐秘'配置项
前端·人工智能·后端
CoolerWu6 小时前
2025 · 我与 AI / Vibe Coding 的一年
前端·后端
张风捷特烈6 小时前
AI 四格笑话爆火,我做了什么?
前端·aigc