6.1 HMR 是什么
Hot Module Replacement(热模块替换)指"代码文件改动后,不重新加载整个页面,只替换变化的那个模块"——这样表单输入、滚动位置、应用状态都能保留,开发体验飞跃。
Vite 的 HMR 链路:
- 你保存
./Button.tsx - Vite dev server 用 chokidar 监听到文件变化
- 服务器通过 WebSocket 发
update消息:"/src/Button.tsx 变了" - 浏览器的 HMR 客户端接收,向服务器请求新版本的
Button.tsx - 新模块被注入当前运行时,调用
import.meta.hot.accept回调
6.2 import.meta.hot API
Vite 在每个模块上挂一个 import.meta.hot 对象(生产构建时为 undefined):
if (import.meta.hot) {
// 只在 dev 生效,生产自动 tree-shake
}
核心方法
- accept() / accept(cb) / accept(deps, cb)
- 声明"我能接受自己/依赖的更新"——不调这个,修改会退化为整页刷新。
- dispose(cb)
- 模块即将被替换前的清理回调,用来拆除副作用(定时器、事件监听、连接)。
- invalidate(msg)
- 放弃 HMR,触发整页刷新(当模块发现无法热更时调用)。
- data
- 跨热替换持久化的数据对象——旧模块往里写,新模块能读。
- decline()
- 声明"这个模块不参与 HMR",改它就整页刷。
6.3 最常见用法:框架自动 HMR
大多数时候你不用手写——框架插件已处理:
@vitejs/plugin-react:用 React Fast Refresh 实现组件热更,保留useState。@vitejs/plugin-vue:SFC(.vue)的 template/script/style 分别热更。- Svelte / Solid / Qwik 都有官方插件,自动注入 HMR 代码。
所以业务代码基本不需要碰 import.meta.hot——只有写"框架无关的状态模块"或"自定义文件格式插件"时才会用到。
6.4 手写一个简单 HMR 示例
// src/counter.ts
let count = 0;
export function setup(el: HTMLButtonElement) {
const render = () => (el.innerHTML = `count is ${count}`);
el.addEventListener('click', () => { count++; render(); });
render();
}
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
// newModule 是刚刚更新后的模块对象(若更新失败为 undefined)
if (newModule) newModule.setup(document.querySelector('#counter')!);
});
}
6.5 accept 的三种形态
① 自更新(self-accept)
import.meta.hot.accept((newModule) => {
// 自己变了时执行
});
不传参只写 accept() 表示"我接受更新但不做任何处理"——用于纯样式/纯数据模块。
② 接受依赖更新
import.meta.hot.accept('./dep.ts', (newDep) => {
// dep.ts 变了,newDep 是它的新导出
});
③ 接受多个依赖
import.meta.hot.accept(['./a.ts', './b.ts'], ([newA, newB]) => {
// a 或 b 变了都会触发
});
6.6 dispose:清理副作用
模块被替换前,旧版本创建的定时器、事件监听、WebSocket 要清理,否则会泄漏:
const timer = setInterval(() => console.log('tick'), 1000);
if (import.meta.hot) {
import.meta.hot.dispose(() => {
clearInterval(timer);
});
import.meta.hot.accept();
}
6.7 hot.data:跨热替换存状态
想让"变量在热替换后还保留"?用 hot.data:
const state = import.meta.hot?.data.state ?? { count: 0 };
render(state);
if (import.meta.hot) {
import.meta.hot.dispose((data) => {
data.state = state; // 把当前状态写进 data
});
import.meta.hot.accept();
}
6.8 invalidate:放弃 HMR
有时候你的模块更新"合理条件下做不了"(例如类层次变了),用 invalidate 告诉 Vite 退回整页刷新:
import.meta.hot.accept((newModule) => {
if (!newModule || !newModule.isCompatible()) {
import.meta.hot!.invalidate('API 不兼容');
}
});
6.9 HMR 失效的常见原因
- 模块没有 self-accept,也没有任何上级 accept 它
- Vite 会往上查找"谁接受了这个模块的更新"。如果一直找到顶都没有,只能整页刷新。React 组件默认有 Fast Refresh,Vue SFC 默认有 HMR block,一般不会发生。
- 使用了默认导出但没给组件命名(React)
- React Fast Refresh 靠组件名识别;
export default () => ...匿名组件不会热更——改成具名函数。 - 副作用放在了顶层
- 模块加载时执行的
setupStore()每次都会重跑,可能重置状态。把它们裹进if (!import.meta.hot || !import.meta.hot.data.inited)。 - 文件路径或扩展名大小写不一致
- macOS 大小写不敏感但 Vite 是敏感的,
./Button和./button会建两套模块图。
6.10 服务端触发 HMR:hot.send
插件可以向客户端发送自定义 HMR 事件:
// 插件里
server.ws.send({ type: 'custom', event: 'config-change', data: newConfig });
// 客户端监听
import.meta.hot?.on('config-change', (data) => {
applyConfig(data);
});
6.11 小结
- Vite HMR 基于 ESM + WebSocket,改哪个模块只重载哪个。
- 业务代码一般不用手写 HMR——框架插件已处理。
- 自定义场景:
accept接收更新、dispose清理、data保状态、invalidate退出。 - React 默认导出匿名组件、顶层副作用是 HMR 失效两大最常见原因。