开发服务器与 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 选项,force 传 polling: 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 在开发服务器启动时就提前编译这些文件,而不是等到浏览器首次请求时再进行编译。