模块热替换(HMR)是现代前端构建工具提供的核心开发特性,它能在不刷新整个页面的情况下,替换、添加或删除模块,同时保留应用的当前状态,大幅提升开发调试效率。Webpack作为主流的前端构建工具,其HMR实现有一套完整的运行时机制,下面我们就来拆解这套机制的工作原理。

HMR的基础概念
HMR的全称是Hot Module Replacement,即模块热替换,它的核心目标是实现局部更新,避免整页刷新带来的状态丢失。在Webpack的构建体系中,HMR需要构建阶段和运行阶段的共同配合:构建阶段会生成包含HMR运行时代码的bundle,运行阶段则负责监听更新、拉取新模块、执行替换逻辑。
Webpack HMR运行时的核心流程
当开发者修改了某个模块的代码并保存后,Webpack的HMR流程会按顺序执行以下几个步骤:
1. 更新检测
运行时的HMR client会定期向开发服务器发送请求,检查是否有新的编译产物。如果存在更新,服务器会返回本次更新的模块哈希值以及受影响的模块列表。
2. 下载更新模块
根据服务器返回的模块列表,HMR runtime会拉取对应的新模块代码,这些新模块会以JSON格式或者独立的chunk形式返回,加载完成后会存入运行时的模块缓存中。
3. 模块替换逻辑
新模块加载完成后,会执行模块的替换操作,这里的核心是hot.dispose和hot.accept两个接口的使用。
先来看一个简单的前端模块示例,展示HMR的接口使用方式:
// counter.js 模块
let count = 0;
export function add() {
count++;
return count;
}
export function getCount() {
return count;
}
// 模块热替换的处理逻辑
if (module.hot) {
// 当当前模块被替换时,执行清理逻辑
module.hot.dispose((data) => {
// 把当前状态存到data中,新模块可以读取
data.count = count;
});
// 接受自身模块的更新
module.hot.accept(() => {
// 新模块加载后,可以从data中恢复状态
const oldData = module.hot.data;
if (oldData) {
count = oldData.count;
}
console.log('counter模块已热更新,当前count值:', count);
});
}4. 状态恢复与生效
新模块执行时,会先读取之前存储的状态数据,再进行初始化,保证应用状态不丢失。如果模块没有定义hot.accept,HMR会向上冒泡查找父模块的accept逻辑,直到找到为止,如果最终没有找到,就会触发整页刷新。
运行时的核心数据结构
Webpack HMR运行时内部维护了两个核心的模块映射表:
- installedModules:存储已经加载过的模块,包含模块的导出、状态等信息
- hotUpdateChunks:存储每次热更新拉取到的新模块内容,更新完成后会合并到installedModules中
当执行模块替换时,运行时先标记旧模块为无效状态,然后执行旧模块的dispose回调清理副作用(比如清除定时器、解绑事件等),之后加载新模块,把新模块的导出挂载到模块系统中,最后触发accept回调通知业务代码更新完成。
常见问题说明
不是所有模块都支持HMR,像React、Vue这类框架都提供了对应的HMR插件,会在框架层处理组件的替换逻辑,开发者不需要手动写accept和dispose代码。
如果热更新失败,Webpack HMR运行时会自动降级为整页刷新,保证开发流程不受影响。另外,生产环境默认不会开启HMR,因为HMR会增加运行时代码的体积,不适合生产环境使用。