说一下 Webpack 的热更新原理吧
问题解析
热模块替换(HMR, Hot Module Replacement)是 Webpack 开发环境的核心特性,实现代码修改后无需刷新页面即可更新。面试官通过此题考察候选人对 Webpack 底层通信机制的理解。
核心概念
HMR 的核心流程:
- WebSocket 连接:Webpack Dev Server(WDS)与浏览器建立长连接
- 变更推送:文件变化后,WDS 推送 hash 值到浏览器
- 差异获取:浏览器对比 hash,通过 Ajax 获取 manifest 文件
- 增量更新:通过 JSONP 请求更新的 chunk
- 模块替换: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 已更新');
});
}
面试要点
-
五个核心步骤:
- WebSocket 建立连接
- 推送 hash 值
- Ajax 请求 manifest
- JSONP 请求 chunk
- HotModulePlugin 完成更新
-
关键技术:
- WebSocket 实时通信
- JSONP 跨域加载更新代码
- 模块替换时保留状态
-
manifest 作用:描述哪些 chunk 需要更新
-
chunk 更新文件:包含实际的新模块代码
-
HMR API:
module.hot.accept()接受更新module.hot.dispose()清理旧状态module.hot.decline()拒绝更新
-
与 Live Reload 区别:
- Live Reload:刷新整个页面
- HMR:只更新变化的模块,保留应用状态
-
局限性:
- 入口文件修改通常需要刷新
- 某些副作用难以自动处理
- 需要模块支持 HMR API