Vite 热更新(HMR)原理详解

什么是 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),负责:

  1. 建立 WebSocket 连接,监听服务端消息
  2. 收到 update 消息后,动态 import() 拉取新模块
  3. 调用模块注册的 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 拆分为 templatescriptstyle 三个虚拟模块
  • <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 开发体验远超传统打包工具的核心所在。

相关推荐
HelloReader2 小时前
Tauri 架构从“WebView + Rust”到完整工具链与生态
前端
Bigger2 小时前
告别版本焦虑:如何为 Hugo 项目定制专属构建环境
前端·架构·go
代码匠心4 小时前
AI 自动编程:一句话设计高颜值博客
前端·ai·ai编程·claude
_AaronWong5 小时前
Electron 实现仿豆包划词取词功能:从 AI 生成到落地踩坑记
前端·javascript·vue.js
cxxcode5 小时前
I/O 多路复用:从浏览器到 Linux 内核
前端
用户5433081441945 小时前
AI 时代,前端逆向的门槛已经低到离谱 — 以 Upwork 为例
前端
JarvanMo5 小时前
Flutter 版本的 material_ui 已经上架 pub.dev 啦!快来抢先体验吧。
前端
恋猫de小郭6 小时前
AI 可以让 WIFI 实现监控室内人体位置和姿态,无需摄像头?
前端·人工智能·ai编程
哀木6 小时前
给自己整一个 claude code,解锁编程新姿势
前端