Skip to content
开发服务器与 HMR

开发服务器与 HMR

本章涵盖 Vite 开发服务器的完整配置和 HMR(热模块替换)的原理与实践。


开发服务器配置

// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  server: {
    // 基础
    host: '127.0.0.1',       // 监听地址,'0.0.0.0' 暴露到局域网
    port: 5173,               // 端口,默认 5173
    strictPort: false,        // true: 端口被占时直接报错而非尝试下一个
    https: false,             // true: 启用 https(自动生成自签名证书)
    open: true,               // true: 自动打开浏览器 / 指定路径 '/docs'

    // 代理
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },

    // CORS
    cors: true,               // 默认开启 CORS
    headers: {},              // 自定义响应头

    // 文件监听
    watch: {
      // 使用 chokidar 的 ignore 模式
      ignored: ['!**/node_modules/your-pkg/**'],
    },

    // 预热
    warmup: {
      // 启动时预先编译这些文件,减少首次请求的延迟
      clientFiles: [
        './src/App.vue',
        './src/router/index.js',
      ],
    },

    // 中间件模式(用于后端集成)
    middlewareMode: false,

    // 静态文件
    fs: {
      strict: true,           // 限制访问工作区以外的文件
      allow: [],              // 允许访问的额外目录
      deny: ['.env', '.env.*', '*.{pem,crt,key}'],  // 禁止访问的文件
    },
  },
})

核心配置项详解

配置项 默认值 说明
port 5173 开发服务器端口
host 'localhost' '0.0.0.0' 监听所有网卡,可在局域网访问
strictPort false true 时端口被占直接退出(CI 环境推荐)
open false 自动打开浏览器,可指定路径如 '/admin'
proxy undefined 代理配置,格式与 http-proxy 一致
cors true(允许任何来源) 可设为 false 禁用;或设为对象配置
watch {} chokidar 选项,forcepolling: true 给 WSL2/Docker

HMR(热模块替换)

HMR 原理

传统开发流程(Live Reload):

修改代码 → 全部重新构建 → 刷新整个页面 → 丢失所有状态

Vite HMR:

修改代码 → 只编译变更模块 → 推送更新边界 → 原位替换,保持状态

具体步骤:

1. 文件变更 → Vite 检测到变化(chokidar)
2. Vite 编译变更模块及其最近的 HMR 边界(HMR Boundary)
3. 编译产物通过 WebSocket 推送到浏览器
4. 浏览器端 HMR Runtime 接收更新
5. 执行更新:自我接受模块直接替换,否则向上冒泡找到边界
6. 边界刷新后,组件内部状态保持不变

🔬 深入原理:“HMR 边界”(HMR Boundary)是框架提供的 HMR 能力边界。Vue 的 .vue 文件、React 的 @vitejs/plugin-react 都实现了 HMR 边界——这意味着修改组件代码时,只有该组件被替换,其内部 useState/ref 等状态保持不变。

HMR 与 Live Reload 对比

维度 HMR Live Reload
更新范围 只更新变更模块 刷新整个页面
组件状态 保持 丢失(全部重新初始化)
速度 极快(< 50ms) 慢(需要完全重载)
表单输入 不会丢失 丢失
网络开销 极小(单模块 ESM 请求) 大(全量资源重载)
适用场景 组件/样式热更新 非模块化的简单项目

HMR API

Vite 通过 import.meta.hot 暴露 HMR API:

// 1. 接受自身更新(Accept)
if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    // 模块被更新时的回调
    // newModule 是新版本的模块导出
  })
}

// 2. 接受依赖的更新
if (import.meta.hot) {
  import.meta.hot.accept('./dep.js', (newDep) => {
    // 当 ./dep.js 变化时触发
    // 此时本模块不会重新加载
  })
}

// 3. 自定义清理逻辑
if (import.meta.hot) {
  import.meta.hot.dispose((data) => {
    // 旧模块被替换之前调用
    // 清理定时器、事件监听等
    clearInterval(timer)
  })
}

// 4. 状态保持
if (import.meta.hot) {
  import.meta.hot.dispose((data) => {
    data.state = currentState  // 保存当前状态
  })
  // 新模块加载后恢复
  if (import.meta.hot.data?.state) {
    restoreState(import.meta.hot.data.state)
  }
}

// 5. 拒绝更新(强制浏览器重载)
if (import.meta.hot) {
  import.meta.hot.decline()
}

// 6. 失效
if (import.meta.hot) {
  import.meta.hot.invalidate()  // 调用后暂停止 HMR 并向上一级冒泡
}

💡 最佳实践:通常不需要手动使用 HMR API,框架的 Vite 插件已内置处理。仅在编写自定义框架或特殊场景时需要。


Proxy 代理

开发阶段最常见的需求是将 API 请求代理到后端服务,避免跨域问题。

export default defineConfig({
  server: {
    proxy: {
      // 基础用法
      '/api': 'http://localhost:8080',

      // 高级用法
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,            // 修改请求头的 origin
        rewrite: (path) => path.replace(/^\/api/, ''),  // 去掉 /api 前缀
        ws: true,                      // 代理 WebSocket
        secure: false,                 // 不验证 HTTPS 证书
        configure: (proxy, options) => {
          // 自定义代理事件处理
          proxy.on('error', (err) => {
            console.log('proxy error', err)
          })
        },
      },

      // 多后端代理
      '/user': {
        target: 'http://user-service:8080',
        changeOrigin: true,
      },
      '/order': {
        target: 'http://order-service:8081',
        changeOrigin: true,
      },
    },
  },
})
选项 说明
target 目标服务器地址
changeOrigin 将请求头的 host/origin 改为 target 的地址
rewrite 重写请求路径
ws 是否代理 WebSocket(默认 true)
secure 是否验证 HTTPS 证书(默认 true)
bypass 函数,返回 falsy 值时跳过代理

bypass 用法

proxy: {
  '/api': {
    target: 'http://localhost:8080',
    bypass: (req, res, proxyOptions) => {
      // 直接返回 mock 数据,不经过代理
      if (req.headers.accept?.includes('text/html')) {
        return '/index.html'
      }
    },
  },
}

后端集成中间件模式

当你有自己的后端服务器(d),可以使用中间件模式,将 Vite 作为中间件接入:

// server.js(Node.js 后端)
import express from 'express'
import { createServer } from 'vite'

async function startServer() {
  const app = express()

  // 将 Vite 开发服务器作为中间件
  const vite = await createServer({
    server: { middlewareMode: true },
    appType: 'custom',
  })
  app.use(vite.middlewares)

  // 你的 API 路由
  app.get('/api/hello', (req, res) => {
    res.json({ msg: 'Hello' })
  })

  app.listen(3000)
}

startServer()

💡 最佳实践:中间件模式适用于已有 Node.js 后端并希望共用同一端口的场景。否则直接用 server.proxy 更简单。


文件监听(watch)

export default defineConfig({
  server: {
    watch: {
      // 传递给 chokidar 的选项
      ignored: ['**/node_modules/**', '**/.git/**'],
      // WSL2 / Docker 环境可能需要启用轮询
      usePolling: false,
      interval: 100,
      // 特别关注某些被忽略的包
      // 格式:以 ! 开头表示取反
      // ignored: ['!**/node_modules/my-pkg/**']
    },
  },
})

🚨 陷阱:在 WSL2 或 Docker 容器中,文件系统事件可能不生效。此时需要用 usePolling: true 或设置 CHOKIDAR_USEPOLLING=true 环境变量。


server.warmup(预热)

Vite 5.3+ 支持启动时预热常用文件,减少首次访问延迟:

export default defineConfig({
  server: {
    warmup: {
      clientFiles: [
        './src/App.vue',
        './src/views/Home.vue',
        './src/views/About.vue',
        './src/router/index.js',
      ],
    },
  },
})

预热会让 Vite 在开发服务器启动时就提前编译这些文件,而不是等到浏览器首次请求时再进行编译。