module.hot 不能直接用于业务代码,因其仅在开发模式下由 webpack 注入且编译期被生产环境剔除;hmr 逻辑必须写在被监听模块内部,不可跨模块封装,且需配对使用 accept/dispose 清理副作用。

为什么 module.hot 不能直接用在业务代码里
因为 HMR 的触发时机和模块生命周期强耦合,module.hot 只在 Webpack 打包后的模块上下文中存在,且仅在开发模式下注入。你在源码里写 if (module.hot) 没问题,但一旦打包进生产环境(mode: 'production'),Webpack 会直接删掉整个 module.hot 分支——这不是运行时判断,是编译期剔除。
常见错误是把热更新逻辑写成独立函数然后到处 import,比如:import { handleHotUpdate } from './hmr-utils'。这会导致该模块本身无法被正确 accept,因为 HMR 的 accept 关系必须由被更新的模块自己声明。
- 所有
module.hot.accept()必须写在要监听更新的那个模块内部 - 不要跨模块封装
module.hot调用,否则 Webpack 无法建立正确的依赖图 -
module.hot.dispose()和module.hot.accept()是配对使用的,dispose 清理上一版副作用,accept 初始化新版状态
React 组件怎么写才能真正“热替换”而不是整页刷新
默认情况下,Webpack + React Refresh 插件会自动注入 HMR 逻辑,但前提是组件导出符合规范:必须是命名导出的函数组件或类组件,且不能包裹在 IIFE 或高阶函数里。比如 export default () => {...} 是匿名函数,React Refresh 拿不到组件名,降级为 reload;而 export default function MyButton() {...} 就可以保状态。
容易被忽略的是副作用清理。如果你在组件里用了 useEffect(() => { setInterval(...) }, []),热更新后旧定时器不会自动清除——React Refresh 不会卸载再挂载组件,而是直接替换函数体,老闭包还活着。
- 确保组件顶层导出是具名函数或 class,避免箭头函数直接 default 导出
- 所有副作用(定时器、事件监听、WebSocket 连接)必须在
useEffect的 cleanup 函数中显式清理 - 如果用了自定义 Hook,它内部的
useEffect同样要带 cleanup,否则热更新后会累积多个监听器
module.hot.accept('./utils', callback) 接受路径不生效?检查三件事
这个 API 看似简单,但实际失败率很高。最常踩的坑是路径没对齐 Webpack 解析结果:你写的 './utils' 是相对路径,但 Webpack 实际解析后可能是 ./utils/index.js 或 ./utils.mjs,而 accept() 匹配的是最终解析出的模块 ID,不是原始字符串。
另一个问题是依赖链断裂。比如 A.js accept('./utils'),但 B.js 也 import 了 utils 并做了修改,这时 Webpack 默认只通知 A.js,B.js 不会重新执行——除非你在 B.js 里也写了 accept() 或者 A.js 是唯一引用者。
- 用
console.log(module.id)和console.log(require.resolve('./utils'))对比两者输出,确认路径是否一致 - 如果 utils 是工具函数集合,建议改用
module.hot.accept('./utils', () => { location.reload() })强制刷新,比硬扛状态迁移更可靠 - Webpack 5 开始支持
module.hot.decline('./utils')显式拒绝更新某个依赖,防止意外 cascade update
样式文件热更新为什么总卡住?别碰 style-loader 的 injectType
Webpack 默认的 style-loader 在 HMR 下使用 'style-tag' 注入方式(即创建 <style></style> 标签),这是唯一能安全替换的模式。如果你手动改成 'singleton' 或 'lazy-style-tag',HMR 会失效,因为前者复用单个标签导致 diff 失败,后者延迟注入让 Webpack 找不到目标节点。
还有个隐藏雷区:CSS Modules 的 localIdentName 如果含时间戳或哈希,每次保存都会生成新 class 名,导致 React 组件里写的 className={styles.xxx} 指向已销毁的 style 对象——这不是 HMR 问题,是 CSS 生成策略和 JS 引用不同步。
- 保持
style-loader的injectType: 'style-tag'(默认值,别动) - CSS Modules 配置里禁用动态 hash,例如用
[name]_[local]而不是[hash:5]_[name]_[local] - 如果用了 Tailwind 的 JIT 模式,确保
content配置包含所有模板路径,否则新增 class 可能不被扫描到,HMR 后样式丢失
dispose 里漏掉一个 removeEventListener,或者以为 accept 能自动处理任意深度的依赖更新。










