我来详细介绍React + webpack的热更新完整流程和细节。
整体架构
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ webpack-dev- │ │ React Fast │ │ 浏览器运行时 │
│ server │◄──►│ Refresh │◄──►│ HMR Runtime │
│ │ │ │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │ │
WebSocket连接 组件状态保持 DOM更新
1. webpack配置层面
基础HMR配置
// webpack.config.js
const webpack = require('webpack');
module.exports = {
mode: 'development',
entry: './src/index.js',
// 开发服务器配置
devServer: {
hot: true, // 启用HMR
hotOnly: true, // 仅HMR,失败时不刷新
port: 3000,
open: true,
overlay: { // 错误覆盖层
warnings: false,
errors: true
}
},
plugins: [
new webpack.HotModuleReplacementPlugin(), // HMR插件
new webpack.NamedModulesPlugin() // 显示模块相对路径
],
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-react',
'@babel/preset-env'
],
plugins: [
// React Fast Refresh的babel插件
require.resolve('react-refresh/babel')
]
}
}
}
]
}
};React Fast Refresh插件配置
// 现代配置:使用@pmmmwh/react-refresh-webpack-plugin
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = {
// ... 其他配置
plugins: [
new webpack.HotModuleReplacementPlugin(),
new ReactRefreshWebpackPlugin({
overlay: {
entry: false, // 不在入口注入overlay
module: true, // 在模块级别注入
sockIntegration: 'wds' // 使用webpack-dev-server的WebSocket
}
})
]
};2. React应用层面
入口文件配置
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
const container = document.getElementById('root');
// 渲染函数
function render(Component) {
ReactDOM.render(<Component />, container);
}
// 初始渲染
render(App);
// 热更新处理
if (module.hot) {
// 接受App组件的更新
module.hot.accept('./App', () => {
console.log('🔥 App组件已更新');
// 重新导入并渲染新的App组件
const NextApp = require('./App').default;
render(NextApp);
});
// 接受当前模块的更新
module.hot.accept();
}React组件的热更新行为
// src/App.js
import React, { useState, useEffect } from 'react';
import Counter from './components/Counter';
import UserProfile from './components/UserProfile';
function App() {
const [appState, setAppState] = useState('初始状态');
useEffect(() => {
console.log('App组件挂载/更新');
}, []);
return (
<div className="app">
<h1>React HMR 演示</h1>
<p>应用状态: {appState}</p>
{/* 修改这些组件时的不同表现 */}
<Counter />
<UserProfile />
<button onClick={() => setAppState('已修改')}>
修改应用状态
</button>
</div>
);
}
export default App;3. 热更新的详细流程
第一步:文件变化检测
// webpack监听文件变化
const chokidar = require('chokidar');
class WebpackDevServer {
setupFileWatcher() {
// 监听源文件变化
this.watcher = chokidar.watch('./src/**/*', {
ignored: /node_modules/,
ignoreInitial: true
});
this.watcher.on('change', (filePath) => {
console.log(`📁 文件变化: ${filePath}`);
// 触发webpack重新编译
this.compiler.run((err, stats) => {
if (!err && !stats.hasErrors()) {
this.sendUpdateToClients(stats);
}
});
});
}
}第二步:增量编译
// webpack编译器生成热更新文件
class HotModuleReplacementPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('HMR', (compilation, callback) => {
const changedModules = this.getChangedModules(compilation);
// 生成热更新清单文件
const hotUpdateMainContent = {
h: compilation.hash,
c: changedModules.reduce((acc, moduleId) => {
acc[this.getChunkId(moduleId)] = true;
return acc;
}, {})
};
// 输出 [hash].hot-update.json
compilation.assets[`${compilation.hash}.hot-update.json`] = {
source: () => JSON.stringify(hotUpdateMainContent),
size: () => JSON.stringify(hotUpdateMainContent).length
};
// 为每个变化的模块生成 [chunkId].[hash].hot-update.js
changedModules.forEach(moduleId => {
const chunkId = this.getChunkId(moduleId);
const moduleContent = this.generateModuleUpdate(compilation, moduleId);
compilation.assets[`${chunkId}.${compilation.hash}.hot-update.js`] = {
source: () => moduleContent,
size: () => moduleContent.length
};
});
callback();
});
}
}第三步:WebSocket通信
// 服务端推送更新消息
class HMRServer {
sendUpdateToClients(stats) {
const message = {
type: 'hash',
data: stats.hash
};
// 发送新的hash给所有客户端
this.sockets.forEach(socket => {
socket.send(JSON.stringify(message));
});
// 通知客户端可以开始热更新
this.sockets.forEach(socket => {
socket.send(JSON.stringify({ type: 'ok' }));
});
}
}
// 客户端接收消息
class HMRClient {
constructor() {
this.socket = new WebSocket('ws://localhost:3000');
this.currentHash = null;
this.socket.onmessage = (event) => {
const message = JSON.parse(event.data);
this.handleMessage(message);
};
}
handleMessage(message) {
switch (message.type) {
case 'hash':
this.currentHash = message.data;
break;
case 'ok':
this.performUpdate();
break;
case 'warnings':
console.warn('⚠️ 编译警告:', message.data);
break;
case 'errors':
console.error('❌ 编译错误:', message.data);
this.handleErrors(message.data);
break;
}
}
}第四步:模块更新和React状态保持
// React Fast Refresh的核心逻辑
class ReactRefreshRuntime {
performReactRefresh() {
const prevExports = this.getCurrentExports();
// 重新执行模块代码
this.executeUpdatedModule();
const nextExports = this.getCurrentExports();
// 检查是否可以进行快速刷新
if (this.canPerformFastRefresh(prevExports, nextExports)) {
console.log('🔥 React Fast Refresh');
// 保持组件状态,只更新渲染
this.performFastRefresh(prevExports, nextExports);
} else {
console.log('🔄 完整重新挂载组件');
// 需要完整重新挂载
this.performFullRefresh();
}
}
canPerformFastRefresh(prevExports, nextExports) {
// 检查导出是否为React组件
const prevType = this.getComponentType(prevExports);
const nextType = this.getComponentType(nextExports);
// 只有React函数组件或类组件才能快速刷新
return prevType && nextType &&
prevType.$$typeof === nextType.$$typeof;
}
performFastRefresh(prevExports, nextExports) {
// 更新React Fiber树中的组件类型
this.updateFiberTypes(prevExports, nextExports);
// 触发重新渲染,但保持状态
this.scheduleRefresh();
}
}4. 不同类型修改的行为
React组件修改(保持状态)
// 修改前
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>计数: {count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}
// 修改后 - 状态保持
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h2>计数器组件</h2> {/* 新增标题 */}
<p>当前计数: {count}</p> {/* 修改文本 */}
<button onClick={() => setCount(count + 1)}>点击增加</button>
</div>
);
}
// ✅ count状态会保持,不会重置为0CSS样式修改(即时更新)
/* 修改前 */
.counter {
background: blue;
padding: 20px;
}
/* 修改后 */
.counter {
background: red; /* 立即生效,无需刷新 */
padding: 30px;
border-radius: 10px; /* 新增样式 */
}工具函数修改(可能需要刷新)
// src/utils.js
export function formatNumber(num) {
return num.toLocaleString(); // 修改这里可能触发页面刷新
}
// 如果想要热更新,需要在使用的组件中处理
// src/components/Counter.js
import { formatNumber } from '../utils';
function Counter() {
// ... 组件代码
}
// 添加HMR处理
if (module.hot) {
module.hot.accept('../utils', () => {
// 重新渲染组件以使用新的工具函数
console.log('工具函数已更新');
});
}5. 调试和监控
开发者工具监控
// 添加HMR状态监控
if (module.hot) {
// 监听HMR状态变化
module.hot.addStatusHandler((status) => {
console.log(`🔥 HMR状态: ${status}`);
switch (status) {
case 'check':
console.log('检查更新中...');
break;
case 'prepare':
console.log('准备更新...');
break;
case 'ready':
console.log('更新就绪');
break;
case 'dispose':
console.log('清理旧模块...');
break;
case 'apply':
console.log('应用更新...');
break;
case 'idle':
console.log('更新完成');
break;
case 'fail':
console.log('更新失败');
break;
}
});
// 监听更新检查
module.hot.check(true).then((updatedModules) => {
if (updatedModules) {
console.log('📦 更新的模块:', updatedModules);
}
});
}错误处理
// 全局错误处理
if (module.hot) {
module.hot.accept((err) => {
if (err) {
console.error('❌ HMR更新失败:', err);
// 可以选择回退到页面刷新
if (process.env.NODE_ENV === 'development') {
window.location.reload();
}
}
});
}这就是React + webpack热更新的完整细节。关键点是React Fast Refresh能够智能地保持组件状态,而webpack HMR提供了底层的模块替换机制,两者配合实现了优秀的开发体验。