HMR热更新与常见问题
HMR 热更新与常见问题 🔥
HMR(Hot Module Replacement,热模块替换)是现代前端开发体验的核心特性。本章我们深入学习 Vite HMR 的原理、配置、API 以及常见问题的解决方案。
一、HMR 是什么 💎
HMR 允许在运行时更新各种模块,而无需进行完全刷新(Full Reload)。它有以下优势:
- 保留应用状态:修改组件样式 / 模板时,页面状态不会丢失
- 即时反馈:保存代码后立即看到效果
- 提升开发效率:避免每次修改都重新加载整个应用
传统开发:保存代码 → 重新打包 → 浏览器刷新 → 状态丢失
HMR: 保存代码 → 仅替换变更模块 → 应用状态保留二、HMR 工作原理 💎
Vite 的 HMR 基于原生 ES Module + WebSocket 实现:
┌─────────────────────────────────────────────────────────┐
│ 浏览器 │
│ │
│ 1. 加载 index.html │
│ 2. 通过 ES Module 按需请求模块 │
│ 3. 与 Vite Server 建立 WebSocket 连接 │
│ │
└─────────────────────────────────────────────────────────┘
↕ WebSocket
┌─────────────────────────────────────────────────────────┐
│ Vite Dev Server │
│ │
│ 1. 监听文件变化(chokidar) │
│ 2. 计算变更模块的边界 │
│ 3. 推送 HMR 更新事件到浏览器 │
│ 4. 浏览器收到后请求新模块,替换旧的模块 │
│ │
└─────────────────────────────────────────────────────────┘工作流程详解 💎
- 监听文件变化:Vite 使用
chokidar监听项目文件 - 分析依赖图:确定哪些模块依赖了变更的文件
- 推送 HMR 事件:通过 WebSocket 推送
update事件 - 浏览器接收更新:浏览器请求新模块,调用
module.hot.accept处理 - 替换模块:替换旧模块,触发对应的
accept回调 - 保留状态:除了被替换的模块,其它状态都保留
三、默认 HMR 行为 💎
Vite 已经为以下场景内置了 HMR 支持:
| 文件类型 | HMR 行为 |
|---|---|
.vue 文件 | 替换组件实例(保留状态) |
.tsx / .jsx | 替换组件 |
.css 文件 | 热替换(不刷新页面) |
.scss / .less | 编译后热替换 |
.ts / .js | 整页刷新(无 HMR API) |
| 静态资源 | 整页刷新 |
Vue / React 的智能 HMR
Vue 3 / React 框架配合 Vite 时,只更新组件的样式 / 模板 / 渲染逻辑,而不会丢失组件状态(如 ref、reactive 状态)。
四、自定义 HMR 💎
对于没有内置 HMR 的模块(如 .ts / .js),可以手动实现 HMR API。
HMR API 💎
// 模块顶部
if (import.meta.hot) {
// 接受自身模块的更新
import.meta.hot.accept((newModule) => {
// newModule 是新模块对象
if (newModule) {
// 执行更新逻辑
console.log('模块已更新:', newModule)
}
})
// 接受依赖模块的更新
import.meta.hot.accept('./dependency.ts', (newDep) => {
// 当 dependency.ts 更新时触发
})
// 自身模块更新时,向上传播
import.meta.hot.accept()
// 清理副作用
import.meta.hot.dispose(() => {
// 清理定时器、事件监听等
})
// 标记为不接受 HMR
import.meta.hot.decline()
}实战:手写 Store 的 HMR 💎
// src/stores/counter.ts
import { ref } from 'vue'
const count = ref(0)
export function useCounter() {
const increment = () => count.value++
return { count, increment }
}
// HMR 支持
if (import.meta.hot) {
// 接受自身更新
import.meta.hot.accept((newModule) => {
if (newModule) {
// 用新模块的 count 替换
Object.assign(count, newModule.count)
}
})
}实战:CSS Modules HMR 💎
// 接受 CSS 模块更新
if (import.meta.hot) {
import.meta.hot.accept('./styles.module.css', () => {
// CSS 已经在浏览器端自动更新
console.log('CSS updated')
})
}五、HMR 配置 💎
1. 启用 / 禁用 HMR 💎
// vite.config.ts
export default defineConfig({
server: {
hmr: {
// 启用 / 禁用 HMR
overlay: true, // 错误遮罩
// host / port / protocol
},
},
})2. HMR 错误处理 💎
export default defineConfig({
server: {
hmr: {
overlay: true, // 显示错误遮罩
},
},
})错误遮罩可以点击关闭。
3. 跨设备 HMR(局域网) 💎
export default defineConfig({
server: {
host: '0.0.0.0', // 监听所有网络接口
hmr: {
host: '192.168.1.100', // HMR 连接的 host
port: 5173,
protocol: 'ws',
},
},
})移动端调试
手机扫码访问开发服务器时,HMR 默认会失败,因为 WebSocket 无法连接到 localhost。需要:
- 设置
host: '0.0.0.0' - 设置
hmr.host: '你的局域网 IP'
六、强制刷新 💎
某些场景下,HMR 不适合使用(如修改了入口文件),需要强制刷新页面:
if (import.meta.hot) {
import.meta.hot.accept() // 接受更新,并向上传播(最终整页刷新)
}或者通过配置:
export default defineConfig({
server: {
hmr: {
// 设置不需要 HMR 的文件
},
},
})七、CSS HMR 💎
Vite 默认对 CSS 文件完美支持 HMR。修改 .css / .scss 文件后,浏览器会立即更新样式,但不会刷新页面。
// vite.config.ts
export default defineConfig({
css: {
devSourcemap: true, // 开启 sourcemap,便于调试
},
})如果 CSS HMR 失效,常见原因:
- CSS 被 JS 动态注入:通过 JS 创建
<style>标签 - CSS 跨域:从其它域名加载的 CSS
解决方案:把 CSS 写入 .css 文件,通过 import 引入。
八、常见问题与解决方案 💎
1. 修改代码后页面不更新 💎
# 方案一:手动刷新
Ctrl + R
# 方案二:清除缓存
Ctrl + Shift + R
# 方案三:重启 dev server
# Ctrl + C 停止
# pnpm dev 重新启动
# 方案四:删除缓存
rm -rf node_modules/.vite
pnpm dev2. HMR 报错:WebSocket connection failed 💎
// vite.config.ts
export default defineConfig({
server: {
hmr: {
host: 'localhost',
port: 5173,
protocol: 'ws',
},
},
})如果仍然失败,检查:
- 防火墙是否阻止了 WebSocket
- 是否使用了 HTTPS(需要
protocol: 'wss') - 代理服务器(Nginx)是否正确配置了 WebSocket 转发
location / {
proxy_pass http://localhost:5173;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}3. 局域网内手机无法接收 HMR 💎
// vite.config.ts
export default defineConfig({
server: {
host: '0.0.0.0',
hmr: {
// 用你电脑的局域网 IP
host: '192.168.1.100',
port: 5173,
},
},
})4. 修改 Vue 组件后状态丢失 💎
通常是因为组件被完全卸载重建。检查:
- 是否修改了组件的
key或v-if - 是否修改了组件的 props 接收方式
- 是否修改了 setup 函数结构
<!-- 错误:每次都创建新 key,组件会重建 -->
<MyComponent :key="Date.now()" />
<!-- 正确:使用稳定 key -->
<MyComponent :key="userId" />5. SCSS 变量修改不生效 💎
// vite.config.ts
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`,
},
},
},
})修改 variables.scss 后,HMR 会重新编译所有引用了该变量的文件。
6. 依赖预构建问题 💎
某些依赖第一次启动时会预构建,修改后不会自动重新构建:
// vite.config.ts
export default defineConfig({
optimizeDeps: {
include: ['some-package'],
force: true, // 强制重新构建
},
})或者手动清理:
rm -rf node_modules/.vite
pnpm dev7. 启动报错:Cannot find module 💎
# 方案一:重新安装依赖
rm -rf node_modules
pnpm install
# 方案二:检查 package.json8. 启动报错:esbuild 相关 💎
# Windows 平台常见
# 方案一:使用 legacy SSL
"scripts": {
"dev": "set NODE_OPTIONS=--openssl-legacy-provider && vite"
}
# 方案二:重新安装 esbuild
pnpm install esbuild --force9. 端口被占用 💎
// vite.config.ts
export default defineConfig({
server: {
port: 5173, // 默认
strictPort: false, // 端口被占用时自动切换
},
})或强制使用指定端口:
vite --port 808010. 修改环境变量后不生效 💎
环境变量修改后必须重启 dev server:
# Ctrl + C 停止
# pnpm dev 重启11. 路径别名在 VSCode 中报错 💎
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}12. CSS @import 不生效 💎
// 错误
@import "variables.scss";
// 正确(使用相对路径或别名)
@import "./variables.scss";
@import "@/styles/variables.scss";13. 打包时 SVG 无法显示 💎
// vite.config.ts
import svgLoader from 'vite-svg-loader'
export default defineConfig({
plugins: [svgLoader()],
})14. 大文件构建慢 💎
// vite.config.ts
export default defineConfig({
build: {
// 关闭体积报告
reportCompressedSize: false,
},
})15. 部署后刷新 404 💎
需要配置服务器把所有未匹配的请求 fallback 到 index.html:
# Nginx
location / {
try_files $uri $uri/ /index.html;
}九、调试技巧 💎
1. 使用 Vue DevTools 💎
pnpm add -D vite-plugin-vue-devtools// vite.config.ts
import vueDevTools from 'vite-plugin-vue-devtools'
export default defineConfig({
plugins: [
vueDevTools(),
],
})启动后会自动打开 Vue DevTools 窗口。
2. 使用 vite-plugin-inspect 💎
pnpm add -D vite-plugin-inspect// vite.config.ts
import inspect from 'vite-plugin-inspect'
export default defineConfig({
plugins: [inspect()],
})访问 http://localhost:5173/__inspect/ 可视化查看插件执行流程。
3. 查看依赖图 💎
# 构建时分析
pnpm build --mode analyze
# 或者用 rollup-plugin-visualizer4. 性能分析 💎
// vite.config.ts
export default defineConfig({
build: {
sourcemap: true, // 生成 sourcemap
},
})# Chrome DevTools → Performance → 录制
# 可以看到每个 chunk 的加载时间十、Vue 3 HMR 实战 💎
<!-- Counter.vue -->
<template>
<div>
<h2>计数:{{ count }}</h2>
<button @click="increment">+1</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
// HMR 接受
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
console.log('Counter.vue 已更新')
})
}
</script>修改 increment 函数的实现,count 状态会保留。
十一、React HMR 实战 💎
// Counter.tsx
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<h2>计数:{count}</h2>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
)
}通过 vite-plugin-react / vite-plugin-react-swc 自动支持 HMR。
总结 💎
完整的学习路径
至此,Vite 章节的所有基础知识就告一段落了。回顾一下我们学过的内容:
- 初体验 Vite:了解 Vite 是什么、优势、原理
- 安装 Vite:多种创建项目的方式
- 配置文件详解:defineConfig 与所有配置项
- 环境变量与模式:多环境管理
- 静态资源处理:import、SVG、CDN
- CSS 预处理器与 PostCSS:SCSS、Less、自动前缀
- 路径别名与代理:dev server 优化
- 常用插件推荐:UnoCSS、自动导入、CDN
- 打包与优化:代码分割、压缩、部署
- HMR 与常见问题:热更新原理与排错
后续可根据需要继续深入:
- SSR:Vite 配合 Nuxt / Vite SSR
- 微前端:基于 Vite 的微前端方案
- 自定义插件开发:编写自己的 Vite 插件
- 大型项目架构:Monorepo + Vite
- 性能监控:Web Vitals 集成
至此,本章节的学习就到此结束了,如有疑惑,可对接技术客服进行相关咨询。