Webpack 热更新原理
HMR 的 WebSocket 通信与模块替换机制
问题
Webpack 热更新 (HMR) 是如何实现的?WebSocket 和 manifest 在其中扮演什么角色?
解答
HMR 整体流程
文件修改 → Webpack 编译 → 生成更新文件 → WebSocket 通知 → 浏览器拉取更新 → 替换模块
1. 建立 WebSocket 连接
webpack-dev-server 启动时,会在浏览器和服务器之间建立 WebSocket 连接:
// webpack-dev-server 简化实现
const WebSocket = require('ws');
class HMRServer {
constructor(compiler) {
this.sockets = [];
// 监听 webpack 编译完成
compiler.hooks.done.tap('HMRServer', (stats) => {
// 向所有客户端发送更新通知
this.sockets.forEach(socket => {
socket.send(JSON.stringify({
type: 'hash',
data: stats.hash // 本次编译的 hash 值
}));
socket.send(JSON.stringify({
type: 'ok' // 通知客户端可以更新
}));
});
});
}
// 创建 WebSocket 服务
createServer(server) {
const wss = new WebSocket.Server({ server });
wss.on('connection', (socket) => {
this.sockets.push(socket);
});
}
}
2. 客户端接收更新通知
浏览器端的 HMR runtime 监听 WebSocket 消息:
// HMR 客户端代码(简化版)
const socket = new WebSocket('ws://localhost:8080');
let currentHash = __webpack_hash__; // 当前模块的 hash
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'hash') {
// 保存最新的 hash
currentHash = message.data;
}
if (message.type === 'ok') {
// 检查是否需要更新
if (currentHash !== __webpack_hash__) {
hotCheck();
}
}
};
// 检查并应用更新
async function hotCheck() {
try {
// 拉取 manifest 文件
const update = await module.hot.check(true);
if (update) {
console.log('HMR 更新成功:', update);
}
} catch (err) {
// 更新失败,刷新页面
window.location.reload();
}
}
3. Manifest 和更新文件
Webpack 编译后生成两个关键文件:
// [hash].hot-update.json (manifest 文件)
// 描述哪些 chunk 需要更新
{
"c": { "main": true }, // 需要更新的 chunk
"r": [], // 需要移除的 chunk
"m": [] // 需要移除的模块
}
// [chunkId].[hash].hot-update.js (更新的模块代码)
self["webpackHotUpdate"]("main", {
"./src/app.js": (module, exports, __webpack_require__) => {
// 新的模块代码
module.exports = function() {
console.log('Updated!');
};
}
});
4. 模块替换过程
// HMR runtime 核心逻辑(简化版)
const installedModules = {}; // 已安装的模块缓存
function hotApply(options) {
// 1. 找出过期模块
const outdatedModules = getOutdatedModules();
// 2. 从缓存中移除过期模块
outdatedModules.forEach(moduleId => {
delete installedModules[moduleId];
});
// 3. 执行新模块代码
for (const moduleId in updatedModules) {
// 将新模块添加到 modules 对象
modules[moduleId] = updatedModules[moduleId];
}
// 4. 重新执行父模块,触发依赖更新
outdatedModules.forEach(moduleId => {
const module = installedModules[moduleId];
if (module && module.hot && module.hot._acceptedDependencies[moduleId]) {
// 执行 accept 回调
module.hot._acceptedDependencies[moduleId]();
}
});
}
5. 业务代码中使用 HMR
// src/app.js
import { render } from './render';
render();
// 声明该模块支持热更新
if (module.hot) {
module.hot.accept('./render', () => {
// 当 render.js 更新时,重新执行
render();
});
}
完整流程图
┌─────────────┐ 文件变化 ┌─────────────┐
│ 文件系统 │ ───────────→ │ Webpack │
└─────────────┘ └──────┬──────┘
│ 编译
↓
┌─────────────┐
│ 生成更新文件 │
│ - manifest │
│ - update.js │
└──────┬──────┘
│
WebSocket 推送 hash │
┌─────────────┐ ←────────────────────┘
│ 浏览器 │
│ HMR Runtime │
└──────┬──────┘
│ JSONP 请求 manifest
↓
┌─────────────┐
│ 拉取更新文件 │
└──────┬──────┘
│ 替换模块
↓
┌─────────────┐
│ 执行 accept │
│ 回调函数 │
└─────────────┘
关键点
- WebSocket 通信:用于服务器向浏览器推送更新通知,只传递 hash 值,不传输代码
- Manifest 文件:JSON 格式,描述哪些 chunk/模块需要更新
- JSONP 拉取:浏览器通过 JSONP 方式请求更新的模块代码
- 模块缓存替换:删除旧模块缓存,执行新模块代码,触发 accept 回调
- 降级处理:如果模块没有声明 accept,或更新失败,会冒泡到入口模块,最终刷新页面
目录