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 开发体验远超传统打包工具的核心所在。

相关推荐
yuanyxh19 分钟前
Mac 软件推荐
前端·javascript·程序员
万少24 分钟前
AtomCode开发微信小程序《谁去呀》 全流程
前端·javascript·后端
某人辛木39 分钟前
Web自动化测试
前端·python·pycharm·pytest
Kagol1 小时前
Superpowers GSD gstack AgentSkills深度测评
前端·人工智能
excel2 小时前
JavaScript 字符串与模板字面量:从表象到本质理解
前端
京东云开发者3 小时前
当AI成为导演-如何用AI创作动漫短剧
前端
李白的天不白3 小时前
使用 SmartAdmin 进行前后端开发
java·前端
乘风gg3 小时前
🤡PUA AI Coding 工具 的 10 条终极语录
前端·ai编程·claude
学Linux的语莫3 小时前
Vue 3 入门教程
前端·javascript·vue.js
怕浪猫4 小时前
第一章、Chrome DevTools Protocol (CDP) 详解
前端·javascript·chrome