如何为非Create React App的React类组件项目配置热重载?
问题
刚加入采用React类组件+TypeScript技术栈的公司,当前项目无热重载功能:修改CSS、状态或代码后,Webpack全量编译耗时约25秒,需手动刷新页面,开发效率极低。团队计划逐步迁移至Create React App,但代码量庞大,迁移周期长达数年,现需在过渡阶段解决热重载问题。项目非Create React App初始化搭建,本人有1.5年行业经验,对配置操作较陌生,此次为自主学习并优化团队开发体验。
项目现有配置文件
Package.json
{ "name": "web-front-end", "version": "1.22.19", "description": "HTML5 implementation of the <our company name>. Written in Typescript, compiled to JavaScript, bundled with Webpack, executing on NodeJS, using React, adopting FLUX patterns.", "private": true, "scripts": { "babel": "babel built/client/js/client.js -o built/client/js/client.js", "clean": "rm -rf node_modules && rm -rf built && rm -rf coverage && echo 'clean complete'", "reset": "yarn clean && yarn cache clean && yarn install --frozen-lockfile", "compile": "yarn compile-client && yarn compile-server && yarn babel", "debug-client": "NODE_ENV=debug webpack --config webpack.dev.js && echo 'client compile complete'", "compile-client": "NODE_ENV=production webpack --config webpack.prod.js && echo 'production client compile complete'", "compile-client-dev": "NODE_ENV=development webpack --config webpack.dev.js && echo 'development client compile complete'", "compile-client-dev-watch": "NODE_ENV=development webpack --config webpack.dev-watch.js", "compile-server": "tsc -p src/server && echo 'server compile complete'", "compile-client-watch": "NODE_ENV=development webpack --config webpack.dev-watch.js", "compile-server-watch": "tsc -p src/server --watch", "validate": "yarn --ignore-engines && tslint './src/**/*.{ts,tsx}' './test/**/*.{ts,tsx}' && echo 'validate complete'", "start-server": "node ./built/server/ServerApp.js", "test": "tslint --project test && tslint --project src/app && tsc -p test --jsx react && node ./node_modules/jest/bin/jest.js --config=jest.test.config.json --coverage --verbose", "lint-fix": "tslint --fix --project src && tslint --fix --project test", "prepare": "patch-package" }, "dependencies": { "@emotion/react": "^11.9.3", "@emotion/styled": "^11.9.3", "@mui/icons-material": "^5.8.4", "@mui/material": "^5.8.4", "@mui/styles": "^5.8.4", "@stripe/stripe-js": "1.11.0", "assert": "^2.0.0", "axios": "^0.27.2", "blob-stream": "0.1.3", "buffer": "^6.0.3", "compression": "1.7.3", "compression-webpack-plugin": "^10.0.0", "connected-react-router": "^6.9.2", "cors": "2.8.4", "device-uuid": "1.0.4", "dotenv": "6.0.0", "express": "4.16.3", "file-saver": "^2.0.5", "html-to-image": "^1.10.8", "https-browserify": "^1.0.0", "js-cookie": "2.2.0", "node-zip": "1.1.1", "path-to-regexp": "2.4.0", "pug": "2.0.3", "query-string": "6.1.0", "react": "17.0.0", "react-copy-to-clipboard": "5.0.1", "react-dom": "^17.0.2", "react-form-validator-core": "0.4.4", "react-hook-form": "^7.36.0", "react-inlinesvg": "2.3.0", "react-jss": "8.5.1", "react-lazy-load": "3.0.13", "react-markdown": "4.0.4", "react-material-ui-form-validator": "^3.0.1", "react-player": "1.14.2", "react-redux": "^7.2.6", "react-router": "5.2.1", "react-router-dom": "5.3.0", "react-share": "2.4.0", "react-speech-recognition": "^3.10.0", "react-stripe-elements": "2.0.1", "react-svgmt": "^1.2.0", "react-virtualized": "9.22.3", "react-youtube": "7.9.0", "recompose": "0.27.1", "rect": "1.2.1", "redux": "4.0.0", "redux-thunk": "2.3.0", "regenerator-runtime": "^0.13.9", "shortid": "2.2.15", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", "svg-to-pdfkit": "0.1.7", "url": "^0.11.0", "utf8": "3.0.0" }, "devDependencies": { "@babel/cli": "^7.17.6", "@babel/core": "^7.17.8", "@babel/types": "^7.17.0", "@types/blob-stream": "0.1.29", "@types/code": "4.0.4", "@types/detect-browser": "3.0.0", "@types/jest": "25.2.3", "@types/js-cookie": "2.2.1", "@types/lab": "11.1.0", "@types/react": "17.0.14", "@types/react-dom": "17.0.14", "@types/react-inlinesvg": "1.0.0", "@types/react-redux": "^7.1.20", "@types/react-router": "4.4.5", "@types/react-virtualized": "9.21.18", "@types/shortid": "0.0.29", "@types/utf8": "2.1.6", "awesome-typescript-loader": "3.4.1", "copy-webpack-plugin": "4.5.1", "detect-browser": "3.0.1", "fork-ts-checker-webpack-plugin": "^7.2.13", "jest": "^27.5.1", "patch-package": "6.1.2", "postinstall-postinstall": "2.0.0", "process": "^0.11.10", "source-map-loader": "0.2.3", "source-map-support": "0.4.18", "ts-jest": "27.1.2", "ts-loader": "^9.3.1", "ts-mockito": "2.3.0", "ts-node": "^10.8.1", "tslint": "^6.0.1", "tslint-eslint-rules": "5.4.0", "tslint-react": "^5.0.0", "typescript": "4.1.5", "webpack": "^5.73.0", "webpack-bundle-analyzer": "^3.0.3", "webpack-cli": "^4.10.0", "webpack-merge": "^5.8.0" }, "resolutions": { "@types/prop-types": "15.7.5", "@types/unist": "2.0.0", "@types/react-transition-group": "2.0.8", "@types/react": "17.0.14", "@types/react-dom": "17.0.14" }, "repository": { "type": "git", "url": "our repo link" }, "author": "our company name", "license": "MIT" }
Webpack.common.js
const path = require("path"); const webpack = require('webpack'); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); module.exports = { entry: "./src/app/Main.ts", cache: false, optimization: { providedExports: true, usedExports: true, removeAvailableModules: true, removeEmptyChunks: true, mergeDuplicateChunks: true, chunkIds: 'named', }, plugins: [ new webpack.ProvidePlugin({process: "process/browser"}), new ForkTsCheckerWebpackPlugin( { typescript: { configFile: './src/app/tsconfig.json' } }), ], module: { rules: [ { test: /\.tsx?$/i, loader: "ts-loader", }, { test: /\.(png|svg|jpg|jpeg|gif)$/i, type: "asset/resource", generator: { filename: 'assets/[hash][ext]', }, }, ], }, resolve: { alias: { core: path.join(__dirname, "src", "core"), client: path.join(__dirname, "src", "client"), server: path.join(__dirname, "src", "server"), '<our company name>-typescript-common': path.resolve(__dirname, 'node_modules/<our company name>-typescript-common/'), }, extensions: [".webpack.js", ".web.js", ".ts", ".tsx", ".js", ".jsx"], fallback: { "util": require.resolve('util'), "os": false, "fs": false, "path": false, "zlib": false, "buffer": require.resolve('buffer'), "http": require.resolve('stream-http'), "https": require.resolve('https-browserify'), "url": require.resolve('url'), "stream": require.resolve('stream-browserify'), }, }, output: { path: path.resolve(__dirname, "built"), filename: "client/js/client.js", clean: true, }, };
入口文件(Main.ts)
import { render } from 'react-dom'; import { AppWrapper } from './view/App/AppWrapper'; declare var window: any; class CompanyName { constructor() { window.dataLayer = window.dataLayer || []; window._hsq = window._hsq || []; const rootHtmlElement = document.body.appendChild(document.createElement('div')); render(new AppWrapper({ lang: window.appConfig.LANGUAGE, appConfig: window.appConfig, startupTimestamp: window.sts}).render(), rootHtmlElement); } } // tslint:disable-next-line:no-unused-expression new CompanyName();
webpack.dev-watch.js
const { merge } = require("webpack-merge"); const common = require("./webpack.dev.js"); const path = require('path'); module.exports = merge(common, { watch: true, optimization: { minimize: false, mangleExports: false, mangleWasmImports: false }, watchOptions: { ignored: [ path.posix.resolve(__dirname, './node_modules'), path.posix.resolve(__dirname, './rfg'), path.posix.resolve(__dirname, './patches'), path.posix.resolve(__dirname, './built'), ], poll: 1500, aggregateTimeout: 1000, }, });
解决方案
针对React类组件+TypeScript+Webpack5的项目,配置热重载需以下步骤:
1. 安装必要依赖
执行命令安装Webpack热重载相关包:
yarn add --dev webpack-dev-server react-refresh @pmmmwh/react-refresh-webpack-plugin react-refresh-typescript
webpack-dev-server:提供开发服务器,支持热重载和实时编译@pmmmwh/react-refresh-webpack-plugin:React官方推荐的热重载插件,兼容类组件react-refresh:插件依赖的核心包react-refresh-typescript:适配TypeScript的转换器
2. 修改Webpack开发配置(webpack.dev.js)
假设webpack.dev.js基于webpack.common.js合并,添加以下配置:
const { merge } = require("webpack-merge"); const common = require("./webpack.common.js"); const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); const webpack = require('webpack'); module.exports = merge(common, { mode: 'development', devtool: 'cheap-module-source-map', // 生成清晰的source map,便于调试 plugins: [ new webpack.HotModuleReplacementPlugin(), // 启用模块热替换 new ReactRefreshWebpackPlugin(), // 启用React热重载 ], module: { rules: [ { test: /\.tsx?$/i, loader: "ts-loader", options: { transpileOnly: true, // 只转译不做类型检查,提升编译速度(类型检查由fork-ts-checker-plugin处理) getCustomTransformers: () => ({ before: [require('react-refresh-typescript')()], // 添加React Refresh的TypeScript转换器 }), }, exclude: /node_modules/, }, ], }, });
3. 更新webpack.dev-watch.js
替换原有watch配置为webpack-dev-server配置,利用其内置的热重载能力:
const { merge } = require("webpack-merge"); const devConfig = require("./webpack.dev.js"); module.exports = merge(devConfig, { devServer: { static: { directory: './built', // 指向编译后的静态文件目录 }, hot: true, // 启用热重载 open: false, // 是否自动打开浏览器 port: 3000, // 开发服务器端口 client: { overlay: { errors: true, // 编译错误时显示全屏覆盖层 warnings: false, // 警告不显示覆盖层 }, }, historyApiFallback: true, // 支持React Router的SPA路由 }, });
4. 修改入口文件(Main.ts)
调整React渲染逻辑,适配热重载:
import { render } from 'react-dom'; import { AppWrapper } from './view/App/AppWrapper'; declare var window: any; // 创建根节点(优先使用已有root节点,避免重复创建) const rootHtmlElement = document.getElementById('root') || (() => { const div = document.createElement('div'); div.id = 'root'; document.body.appendChild(div); return div; })(); function renderApp() { window.dataLayer = window.dataLayer || []; window._hsq = window._hsq || []; // 类组件使用JSX语法渲染,确保React正确管理生命周期 render(<AppWrapper lang={window.appConfig.LANGUAGE} appConfig={window.appConfig} startupTimestamp={window.sts} />, rootHtmlElement); } renderApp(); // 热重载逻辑:模块更新时重新渲染 if (module.hot) { module.hot.accept('./view/App/AppWrapper', () => { renderApp(); }); }
5. 更新package.json的启动脚本
添加开发服务器启动命令:
"scripts": { // ... 原有脚本 "start-dev": "NODE_ENV=development webpack serve --config webpack.dev-watch.js", }
6. 优化编译速度(可选)
- 在
webpack.common.js中设置cache: true,开启Webpack缓存,避免重复编译未修改的模块 - 保持
fork-ts-checker-webpack-plugin配置,让类型检查与Webpack编译并行,不阻塞构建
学习
相关产品推荐
相关产品推荐

