工程化专题 | 改完组件保存,浏览器毫无反应,重启 dev server 又好了——气不气?

21 阅读4分钟

问题场景

团队用 Vite 开发一个中大型后台项目(React + TypeScript,40+ 路由页面),某天开始部分组件修改后 HMR 完全失效。保存文件后终端无日志、浏览器不刷新,必须手动 r 重启整个 dev server 才能看到效果。一天重启十几回,开发效率直接腰斩。

原因分析

Vite HMR 的链路非常清晰:

文件保存 → 文件系统监听 → Vite 检测变更
  → 编译更新 → WebSocket 推送 → 客户端 apply

这条链路上任何一个环节断裂,HMR 就会静默退化回 full reload(甚至什么也不做)。常见根因集中在以下 6 个方向:


🔍 排查方向 & 解决方案

1️⃣ optimizeDeps.excludeinclude 打架(最高频)

症状:改了某个 npm 包内的文件(比如 node_modules/ui-lib 下的补丁),HMR 完全无反应。

原因:Vite 只对预构建缓存中的依赖做 HMR。如果你在 optimizeDeps.exclude 里排除了某个包,包内文件修改不会被热更新。

// vite.config.ts — 这是错的 ❌
export default defineConfig({
  optimizeDeps: {
    exclude: ['@company/ui-lib'], // 排除后这个包的变更就不会触发 HMR
  },
})

解决:改成 include + 重启 dev server:

// ✅ 正确做法
export default defineConfig({
  optimizeDeps: {
    include: ['@company/ui-lib'],
  },
})

然后移除 node_modules/.vite/deps 目录清除缓存。


2️⃣ 文件监听 backend 限制(macOS / Linux 高频)

症状:项目大了以后,改文件时终端偶尔报 EWATCHERENOSPC 错误,HMR 时灵时不灵。

原因:系统文件监听数(fs.inotify)有上限,大项目容易超出。

# Linux: 检查当前上限
cat /proc/sys/fs/inotify/max_user_watches
# 一般是 8192,中大型项目不够用

# 临时调高
sudo sysctl fs.inotify.max_user_watches=131072

# 永久修改
echo "fs.inotify.max_user_watches=131072" | sudo tee -a /etc/sysctl.conf

macOS 用户:升级到 Vite 6+,它内部已优化文件监听策略。旧版本配合 chokidar 配置:

export default defineConfig({
  server: {
    watch: {
      usePolling: true,       // 兜底方案:改用轮询(性能会下降)
      interval: 100,
    },
  },
})

3️⃣ 符号链接 / Monorepo 路径问题

症状:Monorepo 里 packages/shared 用 workspace 协议引用,改 shared 代码后引用方不热更新。

原因:Vite 默认不同路径的符号链接。Monorepo 下的 workspace 包本质就是 symlink。

// vite.config.ts
export default defineConfig({
  server: {
    watch: {
      ignored: ['!**/node_modules/@mycompany/**'], // 白名单 monorepo workspace 包
    },
  },
  // 或干脆关闭 resolved symlink 限制
  resolve: {
    preserveSymlinks: false, // 默认 false,确认不是 true
  },
})

💡 最佳实践:Monorepo 引用本地包时,用 resolve.alias 直接指向源码目录,跳过 workspace 协议:

// vite.config.ts
resolve: {
  alias: {
    '@mycompany/shared': path.resolve(__dirname, 'packages/shared/src'),
  },
},

4️⃣ 组件文件名大小写不一致(Windows/macOS 不敏感但 CI 报)

症状import Button from './Button',实际文件叫 button.tsx。macOS 上能跑,HMR 偶尔失效。

原因:文件系统大小写敏感度不一致。Vite 依赖文件路径做 HMR 模块映射,路径大小写不一致时 map 找不到 module。

排查:在项目根目录运行:

# 查找可能的文件名大小写问题
find . -name '*.tsx' -o -name '*.ts' | sort > /tmp/files.txt
# 检查 https://github.com/niexq/vite-hmr-case-check (或手动 grep 重复大小写)

解决:统一小写命名规范 + ESLint 规则:

// .eslintrc
{
  "rules": {
    "import/no-useless-path-segments": "error"
  }
}

5️⃣ React Fast Refresh 条件钩子违规(React 项目特有)

症状:修改 React 组件后控制台报 [@vite/plugin-react] 相关错误,HMR 退化为 full reload。

原因:React Fast Refresh 要求组件文件内只能导出组件,不能混用 export const 普通函数 + 组件。

// ❌ 混用导致 Fast Refresh 失效
export const formatDate = (d: Date) => d.toISOString() // 普通函数

export default function DatePicker() { ... } // 组件

// ✅ 拆开
// utils/date.ts
export const formatDate = (d: Date) => d.toISOString()

// components/DatePicker.tsx
import { formatDate } from '../utils/date'
export default function DatePicker() { ... }

同理,文件内不允许useState/useEffect 在组件顶层条件语句中。


6️⃣ Websocket 连接中断(反向代理 / HTTPS 场景)

症状:页面加载正常,保存后终端有 [vite] hmr update 日志,但浏览器端不更新。

原因:WebSocket (wss) 连接被中间代理或证书问题阻断。

排查

  1. 打开浏览器 DevTools → Network → WS,看 Vite HMR 的 WebSocket 是否有 closeerror
  2. 确认 server.hmr 配置:
export default defineConfig({
  server: {
    hmr: {
      protocol: 'wss',          // HTTPS 下必须
      host: 'yourdomain.com',
      port: 443,
      clientPort: 443,
    },
  },
})

如果 Nginx 反代,确认保留 Upgrade 头:

location / {
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";
}

🧪 通用诊断速查表

现象最可能的原因3 秒试一下
改任何文件都不热更系统文件监听上限sysctl fs.inotify.max_user_watches 检查
只某个 npm 包文件不热更exclude/include 配置清空 node_modules/.vite 重启
提示 React Refresh 错误钩子规则违规看控制台具体报错
WS 没断但浏览器不更新开发环境 HTTPS 配置检查 HMR protocol: 'wss'
时好时坏硬件/IDE 缓存rm -rf node_modules/.vite 然后重启

要点总结

  1. 先清缓存再重查rm -rf node_modules/.vite 解决 50% 的 HMR 问题
  2. 看终端日志:Vite 输出的 HMR 日志里藏着关键线索(hmr update vs silent)
  3. 逐步还原:把最近改的 vite.config.tstsconfig 一行行回退测试
  4. 终极大招server.watch.ignored 配置走一波,把不需要监听的目录排除掉(dist, node_modules 的子集等)