Vite 模式 是做什么的?
适用于 Vite 8+ 项目的构建工具和开发服务器模式。涵盖配置、环境变量、代理设置、库模式、依赖预构建和常见生产陷阱。
使用时机
- 配置
vite.config.ts或vite.config.js - 设置环境变量或
.env文件 - 配置开发服务器代理以实现 API 后端
- 优化构建输出(代码分割、压缩、资源)
- 使用
build.lib发布库 - 调试依赖预构建或 CJS/ESM 互操作
- 调试 HMR、开发服务器或构建错误
- 选择或排序 Vite 插件
工作原理
- 开发模式 将源文件作为原生 ESM 提供 —— 不进行打包。转换按需在每个模块请求时进行,这就是为什么冷启动快且 HMR 精确。
- 构建模式 使用 Rolldown (v7+) 或 Rollup (v5–v6) 打包应用程序以用于生产,具有树摇、代码分割和基于 Oxc 的压缩功能。
- 依赖预构建 使用 esbuild 将 CJS/UMD 依赖转换为 ESM 一次,并将结果缓存在
node_modules/.vite下,以便后续启动跳过此工作。 - 插件 在开发和生产之间共享统一的接口 —— 相同的插件对象既适用于开发服务器的按需转换,也适用于生产管道。
- 环境变量 在构建时静态内联。
VITE_前缀的变量成为打包后的公共常量;没有前缀的变量对客户端代码不可见。
示例
配置结构
基本配置
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
resolve: {
alias: { '@': new URL('./src', import.meta.url).pathname },
},
})
条件配置
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd()) // 仅 VITE_ 前缀(安全)
return {
plugins: [react()],
server: command === 'serve' ? { port: 3000 } : undefined,
define: {
__API_URL__: JSON.stringify(env.VITE_API_URL),
},
}
})
关键配置选项
| 键 | 默认值 | 描述 |
|---|---|---|
root | '.' | 项目根目录(index.html 所在位置) |
base | '/' | 部署资源的公共基础路径 |
envPrefix | 'VITE_' | 客户端暴露环境变量的前缀 |
build.outDir | 'dist' | 输出目录 |
build.minify | 'oxc' | 压缩器('oxc'、'terser' 或 false) |
build.sourcemap | false | true、'inline' 或 'hidden' |
插件
必备插件
大部分插件需求由少数几个维护良好的包覆盖。在编写自己的插件之前,优先使用这些。
| 插件 | 用途 | 使用时机 |
|---|---|---|
@vitejs/plugin-react-swc | 通过 SWC 实现 React HMR + 快速刷新 | React 应用的默认选择(比 Babel 变体更快) |
@vitejs/plugin-react | 通过 Babel 实现 React HMR + 快速刷新 | 仅当你需要 Babel 插件时(emotion、MobX 装饰器) |
@vitejs/plugin-vue | Vue 3 SFC 支持 | Vue 应用 |
vite-plugin-checker | 在工作线程中运行 tsc + ESLint,并带有 HMR 覆盖层 | 任何 TypeScript 应用 —— Vite 在 vite build 期间不进行类型检查 |
vite-tsconfig-paths | 遵守 tsconfig.json 中的 paths 别名 | 任何已在 tsconfig.json 中有别名的情况 |
vite-plugin-dts | 在库模式下生成 .d.ts 文件 | 发布 TypeScript 库 |
vite-plugin-svgr | 将 SVG 导入为 React 组件 | 使用 SVG 作为组件的 React 应用 |
rollup-plugin-visualizer | 打包树图/旭日图报告 | 定期打包大小审计(使用 enforce: 'post') |
vite-plugin-pwa | 零配置 PWA + Workbox | 支持离线功能的应用 |
关键提示: vite build 会转译但不会进行类型检查。除非添加 vite-plugin-checker 或在 CI 中运行 tsc --noEmit,否则类型错误会静默地进入生产环境。
编写自定义插件
编写自定义插件的情况很少见 —— 大多数需求已被现有插件覆盖。当你确实需要一个时,从在 vite.config.ts 中内联开始,只有在重复使用时才提取出来。
// vite.config.ts —— 最小内联插件
function myPlugin(): Plugin {
return {
name: 'my-plugin', // 必需,必须唯一
enforce: 'pre', // 'pre' | 'post'(可选)
apply: 'build', // 'build' | 'serve'(可选)
transform(code, id) {
if (!id.endsWith('.custom')) return
return { code: transformCustom(code), map: null }
},
}
}
关键钩子: transform(修改源码)、resolveId + load(虚拟模块)、transformIndexHtml(注入 HTML)、configureServer(添加开发中间件)、hotUpdate(自定义 HMR —— 在 v7+ 中取代已弃用的 handleHotUpdate)。
虚拟模块 使用 \0 前缀约定 —— resolveId 返回 '\0virtual:my-id',这样其他插件会跳过它。用户代码导入 'virtual:my-id'。
有关完整插件 API,请参阅 vite.dev/guide/api-plugin。在开发过程中使用 vite-plugin-inspect 调试转换管道。
HMR API
框架插件(@vitejs/plugin-react、@vitejs/plugin-vue 等)会自动处理 HMR。仅在构建自定义状态存储、开发工具或需要跨更新持久化状态的与框架无关的工具时,才直接使用 import.meta.hot。
// src/store.ts —— 普通模块的手动 HMR
if (import.meta.hot) {
// 跨更新持久化状态(必须 修改,绝不能重新赋值 .data)
import.meta.hot.data.count = import.meta.hot.data.count ?? 0
// 在模块被替换之前清理副作用
import.meta.hot.dispose((data) => clearInterval(data.intervalId))
// 接受此模块自身的更新
import.meta.hot.accept()
}
所有 import.meta.hot 代码在生产构建中都会被树摇掉 —— 无需移除保护代码。
环境变量
Vite 按顺序加载 .env、.env.local、.env.[mode] 和 .env.[mode].local(后面的覆盖前面的);*.local 文件被 gitignore,用于本地密钥。
客户端访问
只有 VITE_ 前缀的变量会暴露给客户端代码:
import.meta.env.VITE_API_URL // 字符串
import.meta.env.MODE // 'development' | 'production' | 自定义
import.meta.env.BASE_URL // base 配置值
import.meta.env.DEV // 布尔值
import.meta.env.PROD // 布尔值
import.meta.env.SSR // 布尔值
在配置中使用环境变量
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd()) // 仅 VITE_ 前缀(安全)
return {
define: {
__API_URL__: JSON.stringify(env.VITE_API_URL),
},
}
})
安全
VITE_ 前缀不是安全边界
任何以 VITE_ 为前缀的变量在构建时都会被静态内联到客户端打包产物中。压缩、Base64 编码和禁用源映射不会隐藏它。一个坚定的攻击者可以从已发布的 JavaScript 中提取任何 VITE_ 变量。
规则: 只有公共值(API URL、特性标志、公钥)放入 VITE_ 变量。密钥(API 令牌、数据库 URL、私钥)必须位于服务器端,通过 API 或无服务器函数提供。
loadEnv('') 陷阱
// 错误:将 '' 作为第三个参数传递会加载所有环境变量 —— 包括服务器密钥 ——
// 并使它们可通过 `define` 内联到客户端代码中。
const env = loadEnv(mode, process.cwd(), '')
// 正确:显式前缀列表
const env = loadEnv(mode, process.cwd(), ['VITE_', 'APP_'])
生产环境中的源映射
生产环境中的源映射会泄露你的原始源代码。除非你上传到错误跟踪器(Sentry、Bugsnag)并在本地删除,否则请禁用它:
build: {
sourcemap: false, // 默认 —— 保持这种方式
}
.gitignore 清单
.env.local、.env.*.local—— 本地密钥覆盖dist/—— 构建输出node_modules/.vite—— 预构建缓存(过时的条目会导致幽灵错误)
服务器代理
// vite.config.ts —— server.proxy
server: {
proxy: {
'/foo': 'http://localhost:4567', // 字符串简写
'/api': {
target: 'http://localhost:8080',
changeOrigin: true, // 虚拟主机后端需要此项
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
}
要启用 WebSocket 代理,请将 ws: true 添加到路由配置中。
构建优化
手动代码分割
// vite.config.ts —— build.rolldownOptions
build: {
rolldownOptions: {
output: {
// 对象形式:分组特定包
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-popover'],
},
},
},
}
// 函数形式:按启发式规则分割
manualChunks(id) {
if (id.includes('node_modules/react')) return 'react-vendor'
if (id.includes('node_modules')) return 'vendor'
}
性能
避免桶文件
桶文件(从目录中重新导出所有内容的 index.ts)会强制 Vite 加载每个重新导出的文件,即使你只导入单个符号。这是官方文档指出的头号开发服务器减速原因。
// 错误 —— 导入一个工具函数会强制 Vite 加载整个桶文件
import { slash } from '@/utils'
// 正确 —— 直接导入,只加载一个文件
import { slash } from '@/utils/slash'
使用显式导入扩展名
每个隐式扩展名会通过 resolve.extensions 强制进行多达 6 个文件系统检查。在大型代码库中,这会累积起来。
// 错误
import Component from './Component'
// 正确
import Component from './Component.tsx'
将 tsconfig.json 中的 allowImportingTsExtensions 和 resolve.extensions 限制为你实际使用的扩展名。
预热热路径路由
server.warmup.clientFiles 在浏览器请求之前预先转换已知的热点入口 —— 消除了大型应用中的冷加载请求瀑布。
// vite.config.ts
server: {
warmup: {
clientFiles: ['./src/main.tsx', './src/routes/**/*.tsx'],
},
}
分析慢速开发服务器
当 vite dev 感觉缓慢时,从 vite --profile 开始,与应用程序交互,然后按 p+enter 保存 .cpuprofile。在 Speedscope 中加载它,找出哪些插件消耗了时间 —— 通常是社区插件中的 buildStart、config 或 configResolved 钩子。
库模式
发布 npm 包时,使用 build.lib。两个常见陷阱比配置细节更重要:
- 不会生成类型声明 —— 添加
vite-plugin-dts或单独运行tsc --emitDeclarationOnly。 - 对等依赖必须外部化 —— 未列出的对等依赖会被打包到你的库中,导致消费者出现重复运行时错误。
// vite.config.ts
build: {
lib: {
entry: 'src/index.ts',
formats: ['es', 'cjs'],
fileName: (format) => `my-lib.${format}.js`,
},
rolldownOptions: {
external: ['react', 'react-dom', 'react/jsx-runtime'], // 每个对等依赖
},
}
SSR 外部化
裸 createServer({ middlewareMode: true }) 设置属于框架作者的领域。大多数应用应使用 Nuxt、Remix、SvelteKit、Astro 或 TanStack Start。作为框架用户,你需要调整的是当依赖在 SSR 中损坏时的外部化配置:
// vite.config.ts —— ssr 选项
ssr: {
external: ['node-native-package'], // 在 SSR 打包中保持为 require()
noExternal: ['esm-only-package'], // 强制打包到 SSR 输出中(修复大多数 SSR 错误)
target: 'node', // 'node' 或 'webworker'
}
依赖预构建
Vite 预构建依赖以将 CJS/UMD 转换为 ESM 并减少请求数量。
// vite.config.ts —— optimizeDeps
optimizeDeps: {
include: [
'lodash-es', // 强制预构建已知的重量级依赖
'cjs-package', // 导致互操作问题的 CJS 依赖
'deep-lib/components/**', // 深度导入的通配符模式
],
exclude: ['local-esm-package'], // 如果排除,则必须是有效的 ESM
force: true, // 忽略缓存,重新优化(临时调试)
}
常见陷阱
开发与构建不一致
开发使用 esbuild/Rolldown 进行转换;构建使用 Rolldown 进行打包。CJS 库在这两者之间可能表现不同。在部署前始终验证 vite build && vite preview。
部署后过时的块
新构建产生新的块哈希。有活动会话的用户会请求不再存在的旧文件名。Vite 没有内置的解决方案。缓解措施:
- 在部署窗口期间保留旧的
dist/assets/文件 - 在路由器中捕获动态导入错误并强制页面重新加载
Docker 和容器
Vite 默认绑定到 localhost,这从容器外部无法访问:
// vite.config.ts —— Docker/容器设置
server: {
host: true, // 绑定 0.0.0.0
hmr: { clientPort: 3000 }, // 如果在反向代理后面
}
单体仓库文件访问
Vite 将文件服务限制在项目根目录。根目录外的包会被阻止:
// vite.config.ts —— 单体仓库文件访问
server: {
fs: {
allow: ['..'], // 允许父目录(工作区根目录)
},
}
反模式
// 错误:将 envPrefix 设置为 '' 会向客户端暴露所有环境变量(包括密钥)
envPrefix: ''
// 错误:假设 require() 在 Vite 构建中有效