Webpack 的热更新(Hot Module Replacement, HMR)原理是其最亮眼的特性之一。它不仅仅是简单的 LiveReload(整页刷新),而是在应用程序运行时,无需完全刷新页面,就能替换、添加或删除模块的高级能力。
其核心原理可以概括为:建立 WebSocket 连接 + 增量更新机制。
核心角色
理解 HMR 前,需要先了解其中的几个关键角色:
- Webpack Dev Server (WDS):一个基于 Express 的本地开发服务器。它负责:
- 提供静态文件服务。
- 与浏览器建立 WebSocket 连接,实现双向通信。
- Webpack Compiler:标准的 Webpack 编译器。它负责:
- 将源代码编译成 Bundle。
- 在编译时向代码中注入 HMR 运行时代码。
- HMR Runtime:被注入到 Bundle 中的一小段代码。它运行在浏览器中,负责:
- 与 WDS 建立的 WebSocket 连接。
- 接收更新的模块代码。 *触发模块的热更新。
- HMR Server:集成在 WDS 内部的一个服务。它负责:
- 通过 WebSocket 连接向浏览器端的 HMR Runtime 发送更新消息。
热更新完整流程
整个过程是一个精巧的闭环,可以分为启动阶段和更新阶段。
阶段一:启动阶段(服务启动 -> 浏览器拉取代码)
-
启动 WDS 和 Compiler:
- 你运行
webpack serve命令。 - WDS 启动,Webpack Compiler 开始编译。
- Compiler 会将 HMR Runtime 代码注入到最终的 Bundle.js 中。这是实现 HMR 的基础。
- 你运行
-
建立 WebSocket 连接:
- 浏览器拉取最初版本的 Bundle.js 并执行。
- 其中的 HMR Runtime 开始工作,与 WDS 内的 HMR Server 建立 WebSocket 长连接。
- 此后,双方通过这个通道进行实时通信。
阶段二:更新阶段(文件修改 -> 页面无刷新更新)
这是最核心的循环,流程图如下所示:
sequenceDiagram
participant U as 用户(修改文件)
participant C as Webpack Compiler
participant S as WDS (HMR Server)
participant R as Browser (HMR Runtime)
participant A as 应用程序
U->>C: 修改并保存源文件
Note right of C: 重新编译<br>生成新的编译哈希值和<br>变更的模块文件
C->>S: 编译完成,推送消息<br>(包含hash和manifest)
Note right of S: 通知有更新可用
S->>R: 通过 Websocket 发送 hash 和 manifest
Note right of R: 检查更新,<br>用hash值请求增量更新文件
R->>S: 请求新的模块 chunks (jsonp)
S->>R: 返回增量更新代码
Note right of R: HMR Runtime 检查<br>模块是否能热更新
R->>A: 调用 module.hot.accept API
Note right of A: 替换旧模块,执行新逻辑
A->>R: 返回更新结果
下面我们来分解图中的关键步骤:
步骤 1 & 2:文件修改与重新编译
- 开发者修改并保存源文件。
- Webpack Compiler 监听到文件变化,立即增量编译(只编译变化的模块,速度很快)。
- 编译完成后,Compiler 生成两个核心东西:
- 本次编译的哈希值(hash):作为这次更新的唯一标识。
- 一份更新清单(manifest):一个 JSON 文件,描述了哪些模块(chunks)发生了变化。
步骤 3 & 4:服务器推送消息与浏览器检查
- WDS(HMR Server)通过 WebSocket 主动向浏览器推送一条消息,内容很简单,主要是刚才生成的 hash 值。
- 浏览器端的 HMR Runtime 收到消息,知道有了新的更新。但它还不知道具体要更新什么。于是它用这个 hash 值,主动向 WDS 发起一个 Ajax 请求(通常是 JSONP),获取步骤2中生成的更新清单(manifest)。
步骤 5 & 6:获取增量代码与模块热替换
- HMR Runtime 根据 manifest 中的信息,再次向 WDS 发起多个 JSONP 请求,只拉取那些发生变化的模块 chunk(增量更新)。
- 拉取到新的模块代码后,HMR Runtime 现在有了新代码和旧代码。
- 最关键的一步:HMR Runtime 会检查这个更新的模块是否定义了HMR处理逻辑(即代码中是否有
module.hot.accept回调)。- 如果定义了:HMR Runtime 就会安全地执行更新逻辑:用新模块替换掉老模块,并执行
module.hot.accept中的回调函数。此时页面不会刷新,但状态(如表格中的数据、输入框的内容)得以保留。 - 如果未定义:HMR Runtime 没有安全更新的办法,则会** fallback(回退)到整页刷新(liveReload)**。
- 如果定义了:HMR Runtime 就会安全地执行更新逻辑:用新模块替换掉老模块,并执行
关键:module.hot.accept API
这是开发者参与 HMR 的接口。框架(如 Vue CLI、Create React App)的 HMR 支持就是基于这个 API 实现的。
- 对于样式文件:
style-loader内部已经帮我们实现了。所以修改 CSS 能直接无刷新更新。 - 对于 Vue 组件:
vue-loader也自动实现了,修改.vue文件组件会自行更新。 - 对于 React 组件:通常需要使用
react-hot-loader或 React Fast Refresh(已集成在 Create React App 中)。 - 对于你自己写的模块:你可以手动添加处理逻辑。
// 假设这是 counter.js 模块
let count = 0; // 这个状态我们希望保留
function increment() {
count++;
console.log(`Count is now: ${count}`);
}
// 如果这个模块支持HMR
if (module.hot) {
// 当这个模块本身被更新时,执行以下回调
module.hot.accept((getNewModule) => {
// 新的模块代码已经执行,count 变量已经被重置
// 我们可以从全局状态恢复数据,或者执行其他逻辑
console.log(' counter module was updated!');
// 手动重新执行一下函数,让新代码生效
increment();
});
}
总结
Webpack HMR 的本质是一个由WebSocket驱动的增量更新协议。
- 通信渠道:WebSocket 用于高效通知浏览器有更新可用。
- 拉取机制:JSONP 用于按需拉取增量更新的代码块,充分利用浏览器缓存。
- 更新策略:HMR Runtime 根据模块是否提供接受更新的接口,决定是进行局部热替换还是整页刷新。
这种机制极大地提升了开发体验,让你在修改代码后几乎能即时看到变化,同时保持应用的使用状态。