返回首页

说一下 Webpack 的热更新原理吧

问题解析

热模块替换(HMR, Hot Module Replacement)是 Webpack 开发环境的核心特性,实现代码修改后无需刷新页面即可更新。面试官通过此题考察候选人对 Webpack 底层通信机制的理解。

核心概念

HMR 的核心流程:

  1. WebSocket 连接:Webpack Dev Server(WDS)与浏览器建立长连接
  2. 变更推送:文件变化后,WDS 推送 hash 值到浏览器
  3. 差异获取:浏览器对比 hash,通过 Ajax 获取 manifest 文件
  4. 增量更新:通过 JSONP 请求更新的 chunk
  5. 模块替换:HotModuleReplacementPlugin 完成模块替换和状态保留

详细解答

HMR 完整流程图

┌─────────────────────────────────────────────────────────────────────┐
│                         热更新流程                                   │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  文件修改                                                           │
│     │                                                               │
│     ▼                                                               │
│  ┌─────────────┐    WebSocket    ┌─────────────┐                   │
│  │   Webpack   │ ───────────────▶ │   浏览器    │                   │
│  │  DevServer  │   推送 hash     │             │                   │
│  └─────────────┘                  └──────┬──────┘                   │
│       │                                  │                          │
│       │ 编译生成                         │ Ajax 请求                │
│       │ manifest + chunk                 ▼                          │
│       │                            ┌─────────────┐                 │
│       └───────────────────────────▶│  manifest   │                 │
│                                    │  (更新列表) │                 │
│                                    └──────┬──────┘                 │
│                                           │ JSONP 请求             │
│                                           ▼                        │
│                                    ┌─────────────┐                 │
│                                    │  chunk.js   │                 │
│                                    │ (更新代码)  │                 │
│                                    └──────┬──────┘                 │
│                                           │                        │
│                                           ▼                        │
│                                    ┌─────────────┐                 │
│                                    │  HMR Runtime │                 │
│                                    │  执行更新    │                 │
│                                    └─────────────┘                 │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

服务端实现(Webpack Dev Server)

1. 建立 WebSocket 连接

// webpack-dev-server 简化实现
class DevServer {
  constructor(compiler, options) {
    this.compiler = compiler;
    this.setupHooks();
    this.setupWebSocketServer();
  }

  setupHooks() {
    // 编译完成钩子
    this.compiler.hooks.done.tap('webpack-dev-server', (stats) => {
      this.sendStats(stats);
    });
  }

  setupWebSocketServer() {
    const WebSocket = require('ws');
    this.wss = new WebSocket.Server({ port: 8080 });

    this.wss.on('connection', (ws) => {
      this.clients.push(ws);

      // 发送初始 hash
      ws.send(JSON.stringify({
        type: 'hash',
        data: this.currentHash
      }));
    });
  }

  sendStats(stats) {
    const hash = stats.hash;
    this.currentHash = hash;

    // 广播给所有客户端
    this.clients.forEach(client => {
      if (client.readyState === WebSocket.OPEN) {
        // 发送最新 hash
        client.send(JSON.stringify({
          type: 'hash',
          data: hash
        }));

        // 发送 ok 信号
        client.send(JSON.stringify({
          type: 'ok'
        }));
      }
    });
  }
}

2. 生成更新文件

// HotModuleReplacementPlugin 生成更新文件
class HotModuleReplacementPlugin {
  apply(compiler) {
    compiler.hooks.emit.tap('HMR', (compilation) => {
      const hotUpdateMainContent = {
        h: compilation.hash,  // 当前 hash
        c: {}                  // 更新的 chunk
      };

      // 记录更新的 chunk
      for (const chunk of compilation.chunks) {
        if (chunk.hasRuntime()) {
          hotUpdateMainContent.c[chunk.id] = true;
        }
      }

      // 生成 [hash].hot-update.json (manifest)
      compilation.assets[`${compilation.hash}.hot-update.json`] = {
        source: () => JSON.stringify(hotUpdateMainContent),
        size: () => JSON.stringify(hotUpdateMainContent).length
      };

      // 生成 [chunkId].[hash].hot-update.js (更新代码)
      for (const chunk of compilation.chunks) {
        const hotUpdateChunkContent = this.renderChunk(chunk);
        compilation.assets[`${chunk.id}.${compilation.hash}.hot-update.js`] = {
          source: () => hotUpdateChunkContent,
          size: () => hotUpdateChunkContent.length
        };
      }
    });
  }
}

客户端实现(浏览器端 HMR Runtime)

1. WebSocket 消息处理

// 客户端 HMR Runtime 简化实现
const hmr = {
  currentHash: '',
  hot: module.hot,

  // 连接 WebSocket
  connect() {
    const ws = new WebSocket('ws://localhost:8080');

    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);

      switch (message.type) {
        case 'hash':
          this.currentHash = message.data;
          break;

        case 'ok':
          this.checkForUpdate();
          break;

        case 'reload':
          window.location.reload();
          break;

        case 'errors':
          console.error('编译错误:', message.data);
          break;
      }
    };
  },

  // 检查更新
  checkForUpdate() {
    if (!this.hot) {
      window.location.reload();
      return;
    }

    // 调用 module.hot.check 获取更新
    this.hot.check(true)
      .then((updatedModules) => {
        if (!updatedModules) {
          return;
        }
        console.log('[HMR] 更新模块:', updatedModules);
      })
      .catch((err) => {
        console.error('[HMR] 更新失败:', err);
        window.location.reload();
      });
  }
};

hmr.connect();

2. 获取 manifest 和更新 chunk

// module.hot.check 实现
module.hot.check = function(autoApply) {
  return new Promise((resolve, reject) => {
    // 1. 获取 manifest 文件
    const xhr = new XMLHttpRequest();
    xhr.open('GET', `${currentHash}.hot-update.json`);

    xhr.onload = function() {
      const update = JSON.parse(xhr.responseText);

      // 2. 加载更新的 chunk
      const promises = Object.keys(update.c).map(chunkId => {
        return loadUpdatedChunk(chunkId, update.h);
      });

      Promise.all(promises)
        .then(() => applyUpdate(update, autoApply))
        .then(resolve)
        .catch(reject);
    };

    xhr.send();
  });
};

// JSONP 加载更新 chunk
function loadUpdatedChunk(chunkId, hash) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = `${chunkId}.${hash}.hot-update.js`;
    script.charset = 'utf-8';

    // webpackHotUpdate 回调函数
    window.webpackHotUpdate = function(updatedChunkId, moreModules) {
      // 合并更新的模块
      for (const moduleId in moreModules) {
        hotUpdate[moduleId] = moreModules[moduleId];
      }
      resolve();
    };

    script.onerror = reject;
    document.head.appendChild(script);
  });
}

3. 应用更新

// 应用热更新
function applyUpdate(update, autoApply) {
  const outdatedModules = [];
  const outdatedDependencies = [];

  // 1. 找出过时的模块
  for (const moduleId in hotUpdate) {
    if (installedModules[moduleId]) {
      outdatedModules.push(moduleId);
    }
  }

  // 2. 执行模块的 dispose 钩子(清理旧状态)
  outdatedModules.forEach(moduleId => {
    const module = installedModules[moduleId];
    if (module.hot._disposeHandlers) {
      module.hot._disposeHandlers.forEach(handler => handler());
    }
  });

  // 3. 替换模块
  for (const moduleId in hotUpdate) {
    modules[moduleId] = hotUpdate[moduleId];
  }

  // 4. 执行 accept 回调
  outdatedModules.forEach(moduleId => {
    const module = installedModules[moduleId];
    if (module.hot._acceptedDependencies) {
      module.hot._acceptedDependencies.forEach((callback, dep) => {
        callback();
      });
    }
  });

  // 5. 重新执行模块
  outdatedModules.forEach(moduleId => {
    delete installedModules[moduleId];
    __webpack_require__(moduleId);
  });

  return outdatedModules;
}

模块热替换 API

// 在应用代码中使用 HMR API

// 1. 基本用法
if (module.hot) {
  module.hot.accept();
}

// 2. 接受特定依赖的更新
if (module.hot) {
  module.hot.accept('./utils.js', function() {
    console.log('utils.js 已更新');
    // 使用新模块
  });
}

// 3. 状态保留
if (module.hot) {
  // 保存状态
  module.hot.dispose(function(data) {
    data.count = count;
    // 清理副作用
    clearInterval(timer);
  });

  // 恢复状态
  if (module.hot.data) {
    count = module.hot.data.count;
  }
}

// 4. 拒绝更新(触发页面刷新)
if (module.hot) {
  module.hot.decline();
}

// 5. 添加状态检查
if (module.hot) {
  module.hot.accept(function(err) {
    if (err) {
      console.error('无法应用更新:', err);
    }
  });
}

深入理解

HMR 更新文件格式

manifest 文件 ([hash].hot-update.json)

{
  "h": "abc123def456",  // 当前编译 hash
  "c": {
    "0": true,           // chunk 0 有更新
    "1": true            // chunk 1 有更新
  },
  "m": [                 // 移除的模块(可选)
    5,
    6
  ]
}

chunk 更新文件 ([chunkId].[hash].hot-update.js)

// webpackHotUpdate 全局回调
webpackHotUpdate(0, {
  // 更新的模块
  5: function(module, exports, __webpack_require__) {
    // 新模块代码
    exports.add = function(a, b) {
      return a + b;
    };
  },

  6: function(module, exports, __webpack_require__) {
    // 另一个更新的模块
    console.log('模块 6 已更新');
  }
});

React 中的 HMR

// 使用 @pmmmwh/react-refresh-webpack-plugin
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

module.exports = {
  mode: 'development',
  devServer: {
    hot: true
  },
  plugins: [
    new ReactRefreshWebpackPlugin()
  ],
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              plugins: [require.resolve('react-refresh/babel')]
            }
          }
        ]
      }
    ]
  }
};

Vue 中的 HMR

Vue Loader 内置 HMR 支持:

// vue-loader 自动注入 HMR 代码
module.exports = {
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: 'vue-loader'
      }
    ]
  },
  devServer: {
    hot: true
  }
};

最佳实践

1. 配置开发环境 HMR

// webpack.dev.js
const webpack = require('webpack');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

module.exports = {
  mode: 'development',

  devServer: {
    hot: true,           // 启用 HMR
    port: 3000,
    open: true
  },

  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new ReactRefreshWebpackPlugin()  // React 项目
  ]
};

2. 在应用中处理 HMR

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

// HMR 处理
if (module.hot) {
  module.hot.accept('./App', () => {
    const NextApp = require('./App').default;
    root.render(<NextApp />);
  });
}

3. 状态管理库集成

// Redux 状态保留
import store from './store';

if (module.hot) {
  module.hot.accept('./reducers', () => {
    const nextReducer = require('./reducers').default;
    store.replaceReducer(nextReducer);
  });
}

// Vuex 状态保留
if (module.hot) {
  module.hot.accept(['./actions', './mutations'], () => {
    store.hotUpdate({
      actions: require('./actions').default,
      mutations: require('./mutations').default
    });
  });
}

4. 自定义 HMR 处理

// 处理 CSS 模块热更新
if (module.hot) {
  module.hot.accept('./styles.module.css', function() {
    // CSS 模块会自动更新
    console.log('样式已更新');
  });
}

// 处理 Web Worker
if (module.hot) {
  module.hot.accept(new Worker('./worker.js'), function() {
    console.log('Worker 已更新');
  });
}

面试要点

  1. 五个核心步骤

    • WebSocket 建立连接
    • 推送 hash 值
    • Ajax 请求 manifest
    • JSONP 请求 chunk
    • HotModulePlugin 完成更新
  2. 关键技术

    • WebSocket 实时通信
    • JSONP 跨域加载更新代码
    • 模块替换时保留状态
  3. manifest 作用:描述哪些 chunk 需要更新

  4. chunk 更新文件:包含实际的新模块代码

  5. HMR API

    • module.hot.accept() 接受更新
    • module.hot.dispose() 清理旧状态
    • module.hot.decline() 拒绝更新
  6. 与 Live Reload 区别

    • Live Reload:刷新整个页面
    • HMR:只更新变化的模块,保留应用状态
  7. 局限性

    • 入口文件修改通常需要刷新
    • 某些副作用难以自动处理
    • 需要模块支持 HMR API