什么是 HMR?
HMR(Hot Module Replacement,热模块替换)是一种在应用运行时替换、添加或删除模块的技术,无需完整刷新页面,从而保留应用状态并极大提升开发体验。
Vite HMR 整体架构
arduino
┌─────────────────────────────────────────────────┐
│ Browser (Client) │
│ │
│ ┌──────────┐ WebSocket ┌──────────┐ │
│ │ Vue/React│ <─── HMR Update ──> │ HMR Client│ │
│ │ Module │ │ Runtime │ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
▲ ▲
│ ESM import │ WebSocket
│ │
┌─────────────────────────────────────────────────┐
│ Vite Dev Server │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │FS Watcher│ → │ Module │ → │ HMR Server │ │
│ │(chokidar)│ │ Graph │ │ (WebSocket) │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
└─────────────────────────────────────────────────┘
HMR 时序图
1. 完整 HMR 更新流程(JS 模块)

2. 初始化连接时序

3. CSS 热更新时序(无需模块边界)

4. Vue SFC 分块更新时序

核心原理拆解
1. 基于原生 ESM 的按需加载
Vite 在开发模式下不打包源代码,而是直接利用浏览器原生支持的 ES Modules,每个文件即一个模块。
javascript
// 浏览器直接请求原始模块
import { foo } from '/src/utils.ts' // → 向 Vite Dev Server 发起 HTTP 请求
这意味着 Vite 可以做到精确到单个模块的热更新,而不是像 Webpack 那样需要重新构建整个 bundle。
2. 模块依赖图(Module Graph)
Vite Dev Server 在处理每次模块请求时,会维护一张模块依赖图(ModuleGraph):
- 记录每个模块被哪些模块引用(
importers) - 记录每个模块引用了哪些模块(
importedModules) - 每个模块节点包含:文件路径、转换后的代码、时间戳、HMR 边界等信息
css
App.vue
└── Header.vue
└── utils.ts ← 文件发生变化
当 utils.ts 发生变化时,Vite 通过依赖图向上查找,确定需要更新的模块范围。
3. 文件监听(chokidar)
Vite 使用 chokidar 监听文件系统变化:
javascript
// 简化的监听逻辑
watcher.on('change', (file) => {
const mods = moduleGraph.getModulesByFile(file)
// 使模块缓存失效
mods.forEach(mod => moduleGraph.invalidateModule(mod))
// 触发 HMR 更新
handleHMRUpdate(file, server)
})
4. HMR 边界(HMR Boundary)传播
Vite 在依赖图中向上查找,直到找到能接受(accept)热更新的模块,该模块就是 HMR 边界。
传播规则:
css
utils.ts 变化
↓ 向上冒泡
Header.vue ← 如果 Header.vue 声明了 import.meta.hot.accept()
则 Header.vue 是 HMR 边界,停止向上冒泡
↓ 继续冒泡(如果 Header.vue 没有声明)
App.vue
↓ 继续冒泡
main.ts ← 找不到边界,触发整页刷新
- 找到边界 → 只替换该模块,保留页面状态
- 找不到边界 → 回退到整页刷新(
location.reload())
5. WebSocket 通信
Vite Dev Server 与客户端通过 WebSocket 保持长连接,服务端向客户端推送更新消息:
go
// 服务端推送的 HMR 消息格式(简化)
{
type: 'update',
updates: [
{
type: 'js-update', // 或 'css-update'
path: '/src/Header.vue',
acceptedPath: '/src/Header.vue',
timestamp: 1693000000000
}
]
}
消息类型包括:
| 类型 | 说明 |
|---|---|
update |
模块更新(JS / CSS) |
full-reload |
触发整页刷新 |
prune |
移除不再使用的模块 |
error |
构建错误,显示错误遮罩 |
connected |
连接建立 |
6. 客户端 HMR Runtime
Vite 在开发时会自动向页面注入客户端运行时代码(/@vite/client),负责:
- 建立 WebSocket 连接,监听服务端消息
- 收到
update消息后,动态import()拉取新模块 - 调用模块注册的
hot.accept回调,执行替换逻辑
javascript
// /@vite/client 简化逻辑
const socket = new WebSocket('ws://localhost:5173')
socket.on('message', ({ data }) => {
const payload = JSON.parse(data)
if (payload.type === 'update') {
payload.updates.forEach(update => {
// 动态 import 拉取最新模块(带时间戳防缓存)
import(`${update.path}?t=${update.timestamp}`)
})
}
if (payload.type === 'full-reload') {
location.reload()
}
})
7. HMR API(import.meta.hot)
模块可通过 import.meta.hot API 声明自己如何处理热更新:
javascript
// 接受自身更新
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
// 用新模块替换旧逻辑
newModule.setup()
})
}
// 接受依赖模块的更新
import.meta.hot.accept('./utils.ts', (newUtils) => {
console.log('utils 更新了', newUtils)
})
// 清理副作用(旧模块销毁前调用)
import.meta.hot.dispose((data) => {
clearInterval(timer)
data.count = currentCount // 向新模块传递状态
})
// 模块自身无法处理,让更新向上冒泡
import.meta.hot.decline()
// 拒绝更新,触发整页刷新
import.meta.hot.invalidate()
8. CSS 热更新(特殊路径)
CSS 的热更新比 JS 更简单,无需模块边界机制:
javascript
// Vite 对 CSS 的处理
// 直接找到对应的 <link> 标签,更新 href(加时间戳)
const linkEl = document.querySelector(`link[href="${path}"]`)
linkEl.href = `${path}?t=${timestamp}`
// 浏览器自动重新加载 CSS,页面不刷新,状态保留
Vue SFC 的 <style> 同理,通过注入的 <style> 标签直接替换内容。
Vue SFC / React 的 HMR 实现
框架级别的 HMR 由各自的 Vite 插件处理:
Vue SFC(@vitejs/plugin-vue):
- 将 SFC 拆分为
template、script、style三个虚拟模块 <template>变化 → 只更新渲染函数,组件实例保留<script>变化 → 重新创建组件,可能丢失状态<style>变化 → 走 CSS 热更新路径,完全无感
React(@vitejs/plugin-react / react-refresh):
- 集成
react-refresh,通过 Babel/SWC 在组件代码中注入热更新逻辑 - 组件更新时保留 hooks 状态(如
useState)
Vite HMR vs Webpack HMR 对比
| 对比维度 | Vite HMR | Webpack HMR |
|---|---|---|
| 更新粒度 | 单个 ESM 模块 | 整个 chunk |
| 更新速度 | 极快(O(1),与项目大小无关) | 较慢(需重新打包 chunk) |
| 底层传输 | 原生 ESM + 动态 import | bundle 替换 |
| 冷启动 | 无需打包,秒级启动 | 需全量打包,随项目增大变慢 |
| 状态保留 | 支持 | 支持 |
总结
Vite HMR 的核心优势在于利用原生 ESM 实现了模块级别的精准更新,整个流程可以概括为:
arduino
文件变化
→ chokidar 监听触发
→ 模块依赖图定位受影响模块
→ 向上查找 HMR 边界
→ WebSocket 推送更新消息到客户端
→ 客户端 Runtime 动态 import 拉取新模块
→ 执行 hot.accept 回调完成替换
由于不依赖打包,整个更新链路的耗时基本是固定的、毫秒级的,不受项目规模影响,这正是 Vite 开发体验远超传统打包工具的核心所在。