Webpack HMR(Hot Module Replacement)是 Webpack 提供的一种在运行时更新模块的能力,它允许开发者修改代码后,浏览器不重新加载整个页面,只替换发生变化的模块并保留应用当前状态。这种机制大幅缩短了前端开发中的反馈周期,尤其在处理复杂表单、动画或带有大量交互状态的页面时优势明显。

一、HMR 的整体运作流程
HMR 并不是浏览器原生支持的能力,而是 Webpack 生态通过一套约定和工具链共同实现的。在开发模式下,Webpack 会启动一个本地开发服务器(通常是 webpack-dev-server 或 webpack-dev-middleware 配合 express),这个服务器一方面提供打包后的资源,另一方面维护一个基于 WebSocket 或 SSE 的双向通道,用来向浏览器推送编译状态。
当开发者保存文件,Webpack 监听到文件变化并重新编译后,不会生成完整的打包文件丢给浏览器,而是只产出“变更模块”的补丁信息。服务器通过长连接通知浏览器:哪些模块 hash 变了、新代码是什么。浏览器端的 HMR runtime 接收到消息,就会按照依赖关系图,把旧模块卸载、新模块装入,并尝试调用模块或父模块里写的 accept 逻辑来完成局部刷新。
1.1 编译阶段的模块标记
在打包过程中,Webpack 会给每个模块分配唯一的 moduleId 和 chunkId,并在模块代码外层包裹一层 HMR 接口调用。例如一个普通 JS 模块会被注入 module.hot 对象,用来注册“当自身或依赖被替换时该怎么做”。如果模块没有写接受逻辑,热更新就会向上冒泡,直到某个父模块接受,或者最终回退成整页刷新。
这种标记方式意味着 HMR 是否生效,很大程度取决于模块是否在代码里声明了热替换边界。像 React 项目常用的 react-refresh 插件,就是在编译时自动给组件加上 accept 处理,让组件能无感替换;而你自己写的工具函数若没处理,就可能触发页面重载。
二、浏览器端如何不刷新替换模块
核心在于“模块热替换接口”与“状态保留”。传统整页刷新会销毁 window 下的所有 JS 上下文,而 HMR 只动特定的 module 实例。Webpack 的 runtime 维护了一个模块缓存表,热更新时先删除旧模块的缓存引用,再执行新模块代码写入缓存,最后通知依赖方用新引用来继续运行。
对于 React、Vue 这类框架,视图层状态本来由框架自己管理(比如 React 的 Fiber 树、Vue 的响应式系统),只要组件函数被替换且组件没被强制卸载,框架就能用之前的虚拟 DOM 或状态树重新渲染。这就是“不刷新页面却像刷新了一部分”的根本原因:页面 DOM 和 JS 全局环境都在,只是局部逻辑被换掉。
2.1 module.hot.accept 的作用
accept 是 HMR 里最关键的方法。当某个模块调用 module.hot.accept('./dep.js', callback),就表示它愿意在 dep.js 更新时执行 callback 而不是刷新页面。如果 dep.js 自己也能 accept 自身,那连上层都不用动。写 webpack 配置或 loader 时,常常要检查目标模块是否暴露了 hot 接口。
下面用一个简单表格说明不同接受策略的差异:
| 策略 | 写法示例 | 更新行为 |
|---|---|---|
| 自身接受 | module.hot.accept() | 模块自我替换,不通知父级 |
| 接受子依赖 | module.hot.accept('./a', fn) | a 变更时执行 fn,父模块不刷新 |
| 无接受逻辑 | 未调用 accept | 冒泡至顶层,通常整页刷新 |
三、为什么有时 HMR 会失效
实际开发中常遇到改了代码却全页刷新,或者界面没变化的情况。常见原因包括:模块依赖链上没有任何 accept 边界;CSS 没走 style-loader 的 HMR 分支;使用了不支持热替换的自定义 loader 且未注入 hot 代码;以及服务端推送的 hash 与客户端记录不一致导致拒绝更新。
另一个隐蔽问题是状态被模块级变量锁死。比如你在模块顶部写了 const cache = {},热替换后新模块会有自己的 cache,但旧模块实例若被其他地方闭包引用,就可能读到过期数据。因此写可热替换的模块,应尽量避免把可变状态挂在模块作用域,或是在 accept 回调里手动迁移状态。
3.1 如何主动支持 HMR
如果你在写组件库或工具模块,可以在入口判断 if (module.hot) 并注册 accept,同时在回调里把必要的运行时状态从旧模块搬到新模块。对于 Vue 组件,vue-loader 已经内置了处理;对于普通 TS 模块,可以配合 hmr 注释或自定义插件来生成接受代码。
此外,保持 webpack 的 output.hash 稳定、 devServer.hot 设为 true、以及不使用过多手动清缓存的逻辑,都能降低热更新异常的概率。当 HMR 报错时,浏览器控制台通常会打印是哪个模块 reject 了更新,顺着堆栈就能找到缺失的 accept 边界。
四、小结
Webpack HMR 通过编译期注入热接口、开发服务器长连接推送差异、浏览器端按模块替换这三步,实现了不刷新页面换模块。它依赖明确的接受边界和框架层的状态保留能力。弄清原理后,不仅能更快排查热更新失灵,也能在搭建脚手架时把本地开发体验做得更流畅。
Webpack_HMR热更新原理模块替换修改时间:2026-08-11 20:09:34