我来详细介绍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状态会保持,不会重置为0

CSS样式修改(即时更新)

/* 修改前 */
.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提供了底层的模块替换机制,两者配合实现了优秀的开发体验。