模块热替换 (HMR) 原理
什么是 HMR?
模块热替换(Hot Module Replacement,简称 HMR)是 Webpack 提供的最实用的功能之一。它允许在运行时更新所有类型的模块,而无需完全刷新整个页面。
好处:
- 保留在完全重新加载页面时丢失的应用程序状态(如表单输入内容、Vue/React 组件内部 state)。
- 只更新变更的内容,节省开发时的调试时间。
HMR 核心工作流程(面试高频)
HMR 的完整工作流可以概括为以下 7 个关键步骤:
- 注入 HMR Runtime 代码: 在开发模式下,Webpack 会将 HMR 的 Runtime 代码(用于客户端与服务器通信的代码)打包进最终的 bundle 中。
- 建立 WebSocket 通信:
客户端代码运行后,会与
webpack-dev-server建立一个 WebSocket (或 SockJS) 连接。 - 监听文件改动与增量构建:
Webpack 借助
webpack-dev-middleware监听本地文件的变化。一旦文件改变,Webpack 就会进行增量构建,并将构建后的模块暂存在内存中(memory-fs),而不是写入磁盘,从而保证了极高的读写性能。 - 发送 Hash 通知客户端:
构建完成后,DevServer 会通过 WebSocket 向客户端发送一个包含最新编译 Hash 值的消息(
hash事件)。 - 客户端请求变更 Manifest:
客户端接收到 Hash 后,会向 DevServer 发起一个 Ajax 请求(通常叫
hotCheck),获取一份包含所有更新模块列表的 JSON 文件(Manifest)。这个 JSON 告诉客户端哪些模块发生了变化以及最新的 Hash 是什么。 - 客户端请求增量代码: 客户端根据 Manifest 里的信息,通过 JSONP 的方式请求获取发生变更的具体的 JS 模块代码。 (注:使用 JSONP 是因为它能绕过跨域限制,并且能像执行脚本一样立刻将代码注入到当前运行环境中)。
- 执行热更新逻辑:
客户端获取到新代码后,Webpack 的 HMR Runtime 会调用模块内部的
module.hot.accept()钩子,执行代码的替换与状态更新。如果该模块没有配置相应的热更新逻辑,事件会向上冒泡,直到整个页面刷新(Fallback 机制)。
面试补充问题
Q: 为什么获取具体更新代码时要用 JSONP? A: JSONP 可以在加载完脚本后立刻执行,直接在全局上下文中注册新的模块代码。这对于 Webpack 的 HMR Runtime 来说非常方便,直接更新模块注册表即可。
Q: Vite 的热更新和 Webpack 的热更新有什么区别?
A: Webpack 的 HMR 依然是基于 Bundle 的,即使是增量构建,随着项目模块数量的增加,重新构建和模块注册的时间也会变长。而 Vite 的 HMR 是基于原生 ESM 的,无论项目多大,它只需要使被修改的模块及其边界失效即可,更新速度几乎是 O(1) 的,远快于 Webpack。