| name | vite-patterns |
| description | Vite 构建工具模式,包括配置、插件、HMR、环境变量、代理设置、SSR、库模式、依赖预打包和构建优化。在处理 vite.config.ts、Vite 插件或基于 Vite 的项目时激活。 |
| origin | ECC |
Vite 模式
适用于 Vite 8+ 项目的构建工具和开发服务器模式。涵盖配置、环境变量、代理设置、库模式、依赖预打包和常见生产环境陷阱。
何时使用
- 配置
vite.config.ts 或 vite.config.js
- 设置环境变量或
.env 文件
- 为 API 后端配置开发服务器代理
- 优化构建输出(分块、压缩、资产)
- 使用
build.lib 发布库
- 排查依赖预打包或 CJS/ESM 互操作问题
- 调试 HMR、开发服务器或构建错误
- 选择或排序 Vite 插件
工作原理
- 开发模式以原生 ESM 方式提供源文件 — 无打包。转换按每个模块请求按需进行,这就是冷启动快和 HMR 精确的原因。
- 构建模式使用 Rolldown(v7+)或 Rollup(v5-v6)为生产环境打包应用,具有 tree-shaking、代码分割和基于 Oxc 的压缩功能。
- 依赖预打包通过 esbuild 将 CJS/UMD 依赖一次性转换为 ESM,并将结果缓存在
node_modules/.vite 下,后续启动跳过此工作。
- 插件在开发和构建之间共享统一接口 — 同一个插件对象同时适用于开发服务器的按需转换和生产管道。
- 环境变量在构建时静态内联。带
VITE_ 前缀的变量成为包中的公共常量;所有未加前缀的变量对客户端代码不可见。
示例
配置结构
基本配置
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
resolve: {
alias: { '@': new URL('./src', import.meta.url).pathname },
},
})
条件配置
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd())
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 + Fast Refresh | React 应用的默认选择(比 Babel 变体更快) |
@vitejs/plugin-react | 通过 Babel 的 React HMR + Fast Refresh | 仅在你需要 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 中内联编写,仅在需要复用时才提取。
function myPlugin(): Plugin {
return {
name: 'my-plugin',
enforce: 'pre',
apply: 'build',
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。
if (import.meta.hot) {
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 代码在生产构建中会被 tree-shaking 移除 — 无需手动移除守卫。
环境变量
Vite 按 .env、.env.local、.env.[mode]、.env.[mode].local 的顺序加载(后者覆盖前者);*.local 文件被 gitignore,用于本地密钥。
客户端访问
只有带 VITE_ 前缀的变量会暴露给客户端代码:
import.meta.env.VITE_API_URL
import.meta.env.MODE
import.meta.env.BASE_URL
import.meta.env.DEV
import.meta.env.PROD
import.meta.env.SSR
在配置中使用环境变量
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
return {
define: {
__API_URL__: JSON.stringify(env.VITE_API_URL),
},
}
})
安全
VITE_ 前缀不是安全边界
任何带 VITE_ 前缀的变量都会在构建时静态内联到客户端包中。压缩、base64 编码和禁用 source map 都不能隐藏它。有决心的攻击者可以从发布的 JavaScript 中提取任何 VITE_ 变量。
规则: 只有公共值(API URL、功能标志、公钥)才放在 VITE_ 变量中。密钥(API 令牌、数据库 URL、私钥)必须存在于服务端 API 或无服务器函数之后。
loadEnv('') 陷阱
const env = loadEnv(mode, process.cwd(), '')
const env = loadEnv(mode, process.cwd(), ['VITE_', 'APP_'])
生产环境 Source Map
生产环境的 source map 会泄露你的原始源代码。除非你上传到错误跟踪器(Sentry、Bugsnag)并在之后删除本地副本,否则禁用它们:
build: {
sourcemap: false,
}
.gitignore 检查清单
.env.local、.env.*.local — 本地密钥覆盖
dist/ — 构建输出
node_modules/.vite — 预打包缓存(过期条目导致幽灵错误)
服务器代理
server: {
proxy: {
'/foo': 'http://localhost:4567',
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
}
对于 WebSocket 代理,在路由配置中添加 ws: true。
构建优化
手动分块
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 加载每个重新导出的文件,即使你只导入一个符号。这是官方文档标记的头号开发服务器减速问题。
import { slash } from '@/utils'
import { slash } from '@/utils/slash'
明确指定导入扩展名
每个隐式扩展名会强制最多 6 次文件系统检查(通过 resolve.extensions)。在大型代码库中,这会累积。
import Component from './Component'
import Component from './Component.tsx'
缩小 tsconfig.json 的 allowImportingTsExtensions + resolve.extensions,仅包含你实际使用的扩展名。
预热热路径路由
server.warmup.clientFiles 在浏览器请求之前预转换已知的热入口 — 消除大型应用上的冷加载请求瀑布。
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。
- 对等依赖必须外部化 — 未列出的对等依赖会被打包到你的库中,导致消费者出现重复运行时错误。
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 中出错时调整外部化配置:
ssr: {
external: ['node-native-package'],
noExternal: ['esm-only-package'],
target: 'node',
}
依赖预打包
Vite 预打包依赖项以将 CJS/UMD 转换为 ESM 并减少请求数。
optimizeDeps: {
include: [
'lodash-es',
'cjs-package',
'deep-lib/components/**',
],
exclude: ['local-esm-package'],
force: true,
}
常见陷阱
开发与构建不匹配
开发使用 esbuild/Rolldown 进行转换;构建使用 Rolldown 进行打包。CJS 库在两者之间可能有不同表现。部署前始终用 vite build && vite preview 验证。
部署后的过期分块
新构建产生新的分块哈希。有活跃会话的用户请求不再存在的旧文件名。Vite 没有内置解决方案。缓解措施:
- 保持旧
dist/assets/ 文件在一个部署窗口内可用
- 在路由器中捕获动态导入错误并强制页面重新加载
Docker 和容器
Vite 默认绑定到 localhost,从容器外部无法访问:
server: {
host: true,
hmr: { clientPort: 3000 },
}
Monorepo 文件访问
Vite 限制文件服务到项目根目录。根目录之外的包被阻止:
server: {
fs: {
allow: ['..'],
},
}
反模式
envPrefix: ''
const lib = require('some-lib')
manualChunks(id) {
if (id.includes('node_modules')) {
return id.split('node_modules/')[1].split('/')[0]
}
}
build: { minify: 'esbuild' }
import.meta.hot.data = { count: 0 }
import.meta.hot.data.count = 0
流程反模式:
vite preview 不是生产服务器 — 它是构建包的冒烟测试。将 dist/ 部署到真正的静态主机(NGINX、Cloudflare Pages、Vercel static)或使用多阶段 Dockerfile。
- 期望
vite build 进行类型检查 — 它只转译。类型错误会静默发布到生产环境。添加 vite-plugin-checker 或在 CI 中运行 tsc --noEmit。
- 默认附带
@vitejs/plugin-legacy — 它会使包膨胀约 40%,破坏 source map 包分析器,对 95%+ 使用现代浏览器的用户来说是不必要的。基于真实分析数据而非假设来启用它。
- 手动编写 30+ 个与
tsconfig.json paths 重复的 resolve.alias 条目 — 改用 vite-tsconfig-paths。在 Excalidraw 和 PostHog 中观察到此问题;新项目中避免使用。
- 在依赖变更后留下过期的
node_modules/.vite — 预打包缓存导致幽灵错误。切换分支或修补依赖后清除它。
快速参考
| 模式 | 何时使用 |
|---|
defineConfig | 始终使用 — 提供类型推断 |
loadEnv(mode, root, ['VITE_']) | 在配置中访问环境变量(显式前缀) |
vite-plugin-checker | 任何 TypeScript 应用(填补类型检查空白) |
vite-tsconfig-paths | 替代手动编写的 resolve.alias |
optimizeDeps.include | 导致互操作问题的 CJS 依赖 |
server.proxy | 在开发中将 API 请求路由到后端 |
server.host: true | Docker、容器、远程访问 |
server.warmup.clientFiles | 预转换热路径路由 |
build.lib + external | 发布 npm 包 |
manualChunks(对象) | 厂商包分割 |
vite --profile | 调试慢开发服务器 |
vite build && vite preview | 本地冒烟测试生产包(不是生产服务器) |
相关技能
frontend-patterns — React 组件模式
docker-patterns — 使用 Vite 的容器化开发
nextjs-turbopack — Next.js 的替代打包器