Webpack 的热更新原理

151 阅读4分钟

Webpack 的热更新(Hot Module Replacement, HMR)原理是其最亮眼的特性之一。它不仅仅是简单的 LiveReload(整页刷新),而是在应用程序运行时,无需完全刷新页面,就能替换、添加或删除模块的高级能力。

其核心原理可以概括为:建立 WebSocket 连接 + 增量更新机制


核心角色

理解 HMR 前,需要先了解其中的几个关键角色:

  1. Webpack Dev Server (WDS):一个基于 Express 的本地开发服务器。它负责:
    • 提供静态文件服务。
    • 与浏览器建立 WebSocket 连接,实现双向通信。
  2. Webpack Compiler:标准的 Webpack 编译器。它负责:
    • 将源代码编译成 Bundle。
    • 在编译时向代码中注入 HMR 运行时代码。
  3. HMR Runtime:被注入到 Bundle 中的一小段代码。它运行在浏览器中,负责:
    • 与 WDS 建立的 WebSocket 连接。
    • 接收更新的模块代码。 *触发模块的热更新
  4. HMR Server:集成在 WDS 内部的一个服务。它负责:
    • 通过 WebSocket 连接向浏览器端的 HMR Runtime 发送更新消息。

热更新完整流程

整个过程是一个精巧的闭环,可以分为启动阶段更新阶段

阶段一:启动阶段(服务启动 -> 浏览器拉取代码)

  1. 启动 WDS 和 Compiler

    • 你运行 webpack serve 命令。
    • WDS 启动,Webpack Compiler 开始编译。
    • Compiler 会将 HMR Runtime 代码注入到最终的 Bundle.js 中。这是实现 HMR 的基础。
  2. 建立 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)**。

关键: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驱动的增量更新协议

  1. 通信渠道:WebSocket 用于高效通知浏览器有更新可用。
  2. 拉取机制:JSONP 用于按需拉取增量更新的代码块,充分利用浏览器缓存。
  3. 更新策略:HMR Runtime 根据模块是否提供接受更新的接口,决定是进行局部热替换还是整页刷新

这种机制极大地提升了开发体验,让你在修改代码后几乎能即时看到变化,同时保持应用的使用状态。