Vite 高级指南
1. 插件开发 API(#%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91-api) 2. SSR(服务端渲染)(#ssr%EF%BC%88%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%B8%B2%E6%9F%93%EF%BC%89) 3. 中间件模式(#%E4%B8%AD%E9%97%B4%E4%BB%B6%E6%A8%A1%E5%BC%8F) 4. JavaScript API(#javascript-api) 5. 性能优化(#%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96) 6.
官方文档:https://vite.dev/guide/
适用版本:Vite 6.x(2026-05-08 核实)
目录
- 插件开发 API
- SSR(服务端渲染)
- 中间件模式
- JavaScript API
- 性能优化
- 环境变量进阶与多环境部署
- Preview Options 完整配置
- Worker Options 完整配置
- 打包与部署
- Monorepo 配置
- 高级插件示例:自定义 Mock 插件
- 踩坑与最佳实践清单
插件开发 API
插件结构
Vite 插件使用工厂函数模式,函数接收用户选项,返回一个 Plugin 对象(或 Plugin 数组)。
命名规范:
- Vite 专用插件:
vite-plugin-xxx - Rollup 兼容插件:
rollup-plugin-xxx - 框架专用插件:
vite-plugin-vue-xxx
Plugin 对象顶层控制字段:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| name | string | 必填 | 插件名称,用于错误信息和日志 |
| enforce | 'pre' | 'post' | undefined |
undefined | 插件执行顺序。pre 在核心插件之前,post 在核心插件之后 |
| apply | 'build' | 'serve' | function |
两者都运行 | 控制插件在何时生效。传入函数可实现更精细的条件判断 |
执行顺序(从先到后):
- enforce: 'pre' 的插件
- 无 enforce 的插件(核心 Vite 插件穿插其中)
- enforce: 'post' 的插件
基本插件骨架:
// vite-plugin-example/index.js
export default function vitePluginExample(options = {}) {
const { prefix = 'example' } = options
return {
name: 'vite-plugin-example',
enforce: 'pre', // 可选
apply: 'build', // 可选,仅构建时启用
// ... 钩子函数
}
}
apply 使用函数进行条件判断:
export default function myPlugin() {
return {
name: 'vite-plugin-conditional',
apply(config, { command, mode }) {
// 仅在生产构建且 mode 为 staging 时启用
return command === 'build' && mode === 'staging'
},
}
}
通用钩子(与 Rollup 共享)
这些钩子与 Rollup 插件 API 完全兼容,在 Vite 中的行为与 Rollup 一致。
钩子总览:
| 钩子 | 触发时机 | 异步支持 | 说明 |
|---|---|---|---|
| options | 服务器/构建启动前 | 是 | 修改 Rollup 选项 |
| buildStart | 每次构建开始 | 是 | 初始化构建状态 |
| resolveId | 模块导入解析 | 是 | 自定义模块 ID 解析逻辑 |
| load | 模块加载 | 是 | 自定义模块内容读取 |
| transform | 模块内容转换 | 是 | 转换模块源代码 |
| buildEnd | 构建结束 | 是 | 构建完成或出错时触发 |
| closeBundle | bundle 关闭 | 是 | 清理资源 |
options(options)
参数:
| 参数名 | 类型 | 说明 |
|---|---|---|
| options | RollupOptions | 当前 Rollup 配置对象 |
返回值: RollupOptions | null。返回修改后的配置对象或 null(不修改)。
options(options) {
// 追加外部依赖
return {
...options,
external: [...(options.external || []), 'some-lib'],
}
}
buildStart(options)
参数:
| 参数名 | 类型 | 说明 |
|---|---|---|
| options | NormalizedInputOptions | 已规范化的 Rollup 输入选项 |
返回值: void
buildStart(options) {
console.log('构建开始,入口文件:', options.input)
// 初始化缓存、计时器等
this.cache = new Map()
}
resolveId(source, importer, options)
参数:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| source | string | 必填 | 被导入的模块标识符(原始字符串) |
| importer | string | undefined | 必填 | 发起导入的模块路径。入口模块时为 undefined |
| options.isEntry | boolean | — | 是否为入口模块 |
| options.assertions | Record<string, string> | — | 导入断言(import assertions) |
返回值: string | false | null | { id, external?, moduleSideEffects?, resolvedBy? }
- 返回字符串:作为解析后的模块 ID
- 返回 false:将该模块标记为外部模块(external)
- 返回 null:交给其他插件或默认解析逻辑处理
resolveId(source, importer) {
if (source === 'virtual:config') {
// 虚拟模块,返回特殊 ID
return '\0virtual:config'
}
return null // 其他模块交给默认处理
}
load(id)
参数:
| 参数名 | 类型 | 说明 |
|---|---|---|
| id | string | 模块 ID(由 resolveId 返回的值) |
返回值: string | null | { code, map?, moduleSideEffects?, syntheticNamedExports? }
load(id) {
if (id === '\0virtual:config') {
return `export const config = ${JSON.stringify({ version: '1.0.0' })}`
}
return null
}
transform(code, id)
参数:
| 参数名 | 类型 | 说明 |
|---|---|---|
| code | string | 模块源代码 |
| id | string | 模块 ID |
返回值: string | null | { code, map? }
transform(code, id) {
if (!id.endsWith('.vue')) return null
// 对 .vue 文件进行处理
const transformed = code.replace(/console\.log\(.*?\)/g, '')
return {
code: transformed,
map: null, // 若有 source map 请提供
}
}
buildEnd(error?)
参数:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| error | Error | undefined | undefined | 若构建出错,则为错误对象 |
buildEnd(error) {
if (error) {
console.error('构建失败:', error.message)
} else {
console.log('构建成功')
}
}
closeBundle()
无参数。用于清理资源、关闭数据库连接等。在 buildEnd 之后触发。
async closeBundle() {
await db.close()
console.log('资源清理完毕')
}
Vite 特有钩子
这些钩子仅在 Vite 中有效,Rollup 构建时会被忽略。
钩子总览:
| 钩子 | 异步 | 顺序 | 说明 |
|---|---|---|---|
| config | 是 | 串行 | 修改 Vite 配置(深度合并方式) |
| configResolved | 是 | 并行 | 读取最终配置,只读,不可修改 |
| configureServer | 否 | 串行 | 配置开发服务器,添加自定义中间件 |
| configurePreviewServer | 否 | 串行 | 配置预览服务器 |
| transformIndexHtml | 是 | 串行 | 转换 index.html |
| handleHotUpdate | 否 | 串行 | 自定义 HMR 更新处理逻辑 |
config(config, env)
参数:
| 参数名 | 类型 | 说明 |
|---|---|---|
| config | UserConfig | 用户配置(合并前) |
| env.command | 'build' | 'serve' |
当前运行的命令 |
| env.mode | string | 当前 mode(development/production/staging 等) |
返回值: Partial<UserConfig> | null。返回的对象将与现有配置深度合并。
config(config, { command, mode }) {
if (command === 'serve') {
return {
define: {
__DEV__: true,
},
}
}
return {
build: {
sourcemap: mode !== 'production',
},
}
}
configResolved(config)
参数:
| 参数名 | 类型 | 说明 |
|---|---|---|
| config | ResolvedConfig | 最终合并解析后的完整配置,只读 |
let resolvedConfig
configResolved(config) {
// 保存配置引用供其他钩子使用
resolvedConfig = config
console.log('最终 base:', config.base)
}
configureServer(server)
参数:
| 参数名 | 类型 | 说明 |
|---|---|---|
| server | ViteDevServer | 开发服务器实例 |
返回值: (() => void) | void。若返回函数,该函数将在内置中间件安装完毕后执行(可用于在内置中间件之后插入中间件)。
configureServer(server) {
// 在内置中间件之前添加
server.middlewares.use('/api/hello', (req, res) => {
res.end(JSON.stringify({ message: 'hello from plugin' }))
})
// 若要在内置中间件之后添加,返回一个函数
return () => {
server.middlewares.use((req, res, next) => {
// 后置中间件
next()
})
}
}
configurePreviewServer(server)
参数:
| 参数名 | 类型 | 说明 |
|---|---|---|
| server | PreviewServer | 预览服务器实例 |
与 configureServer 用法相同,但作用于 vite preview 命令启动的预览服务器。
transformIndexHtml(html, ctx)
参数:
| 参数名 | 类型 | 说明 |
|---|---|---|
| html | string | 原始 HTML 内容 |
| ctx.path | string | HTML 文件路径 |
| ctx.filename | string | HTML 文件绝对路径 |
| ctx.server | ViteDevServer | undefined | 开发模式下的服务器实例 |
| ctx.bundle | OutputBundle | undefined | 构建模式下的 bundle 信息 |
| ctx.chunk | OutputChunk | undefined | 与当前 HTML 对应的 chunk |
返回值: string | HtmlTagDescriptor[] | { html, tags }
transformIndexHtml(html, ctx) {
// 方式一:直接返回修改后的 HTML 字符串
return html.replace(
'<title>App</title>',
`<title>${process.env.npm_package_name}</title>`
)
}
// 方式二:返回标签描述符数组(推荐,更安全)
transformIndexHtml() {
return [
{
tag: 'meta',
attrs: { name: 'build-time', content: new Date().toISOString() },
injectTo: 'head',
},
{
tag: 'script',
attrs: { src: '/analytics.js', defer: true },
injectTo: 'body',
},
]
}
HtmlTagDescriptor 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| tag | string | HTML 标签名 |
| attrs | Record<string, string | boolean> | 标签属性 |
| children | string | HtmlTagDescriptor[] | 子内容 |
| injectTo | 'head' | 'body' | 'head-prepend' | 'body-prepend' |
注入位置 |
handleHotUpdate(ctx)
参数(ctx 对象):
| 参数名 | 类型 | 说明 |
|---|---|---|
| file | string | 发生变化的文件绝对路径 |
| timestamp | number | 变化发生的时间戳 |
| modules | ModuleNode[] | 受此变化影响的模块列表 |
| read | () => string | Promise |
读取文件内容的函数(带缓存) |
| server | ViteDevServer | 开发服务器实例 |
返回值: ModuleNode[] | void。返回需要热更新的模块列表;返回空数组则不触发 HMR;返回 undefined 则使用默认逻辑。
async handleHotUpdate({ file, modules, server }) {
if (file.endsWith('.json')) {
// 自定义:JSON 文件变化时,通知客户端刷新数据
server.ws.send({
type: 'custom',
event: 'json-update',
data: { file },
})
return [] // 阻止默认 HMR
}
// 过滤掉不需要热更新的模块
return modules.filter(m => !m.url.includes('vendor'))
}
虚拟模块
虚拟模块允许插件向构建系统提供不存在于磁盘上的模块内容。
约定:
- 模块 ID 以
virtual:前缀开头(面向用户的 ID) - 在
resolveId返回时,使用\0前缀(Rollup 约定,防止其他插件误处理)
// vite-plugin-virtual-config.js
export default function vitePluginVirtualConfig(appConfig = {}) {
const VIRTUAL_ID = 'virtual:app-config'
const RESOLVED_ID = '\0virtual:app-config'
return {
name: 'vite-plugin-virtual-config',
resolveId(id) {
if (id === VIRTUAL_ID) {
return RESOLVED_ID
}
},
load(id) {
if (id === RESOLVED_ID) {
return `
export const config = ${JSON.stringify(appConfig)};
export default config;
`
}
},
}
}
使用方式:
// vite.config.js
import vitePluginVirtualConfig from './vite-plugin-virtual-config'
export default {
plugins: [
vitePluginVirtualConfig({
apiBaseUrl: 'https://api.example.com',
version: '2.0.0',
}),
],
}
// src/app.js
import { config } from 'virtual:app-config'
console.log(config.apiBaseUrl) // 'https://api.example.com'
TypeScript 类型声明:
// src/env.d.ts
declare module 'virtual:app-config' {
export const config: {
apiBaseUrl: string
version: string
}
export default config
}
插件通信(Client-Server)
Vite 提供了开发服务器与浏览器客户端之间通过 WebSocket 通信的机制。
服务端发送事件
// 插件中(服务端)
configureServer(server) {
server.watcher.on('change', (file) => {
server.ws.send({
type: 'custom',
event: 'file-changed',
data: { file, timestamp: Date.now() },
})
})
}
server.ws.send 参数:
| 参数名 | 类型 | 说明 |
|---|---|---|
| type | 'custom' |
类型固定为 'custom'(内置类型由 Vite 管理) |
| event | string | 自定义事件名称 |
| data | any | 传递给客户端的数据(需可 JSON 序列化) |
客户端监听事件
// 客户端代码(会被 tree-shaking 掉,仅开发时有效)
if (import.meta.hot) {
import.meta.hot.on('file-changed', (data) => {
console.log('文件发生变化:', data.file)
// 执行自定义更新逻辑
})
}
客户端发送事件
// 客户端发送
if (import.meta.hot) {
import.meta.hot.send('client-event', { payload: 'hello server' })
}
服务端监听客户端事件
configureServer(server) {
server.ws.on('client-event', (data, client) => {
console.log('收到客户端消息:', data.payload)
// client 是发送消息的 WebSocket 连接
})
}
TypeScript 扩展 CustomEventMap
// src/env.d.ts
/// <reference types="vite/client" />
interface ImportMetaHot {
on(event: 'file-changed', cb: (data: { file: string; timestamp: number }) => void): void
on(event: 'build-complete', cb: (data: { duration: number }) => void): void
}
或使用推荐的 CustomEventMap 扩展方式:
declare module 'vite/types/customEvent' {
interface CustomEventMap {
'file-changed': { file: string; timestamp: number }
'build-complete': { duration: number }
'client-event': { payload: string }
}
}
完整插件示例:vite-plugin-auto-routes
自动扫描 pages/ 目录,生成 Vue Router 路由配置的完整插件:
// vite-plugin-auto-routes/index.js
import fs from 'node:fs'
import path from 'node:path'
const VIRTUAL_ID = 'virtual:auto-routes'
const RESOLVED_ID = '\0virtual:auto-routes'
/**
* 递归扫描目录,生成路由配置
* @param {string} dir - 扫描目录绝对路径
* @param {string} base - 路由 base path
* @returns {Array} 路由配置数组
*/
function scanRoutes(dir, base = '') {
const routes = []
if (!fs.existsSync(dir)) return routes
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
// 递归处理子目录
const children = scanRoutes(fullPath, `${base}/${entry.name}`)
if (children.length > 0) {
routes.push({
path: `${base}/${entry.name}`,
children,
})
}
} else if (entry.isFile() && /\.(vue|jsx|tsx)$/.test(entry.name)) {
const name = entry.name.replace(/\.(vue|jsx|tsx)$/, '')
const routePath = name === 'index'
? base || '/'
: `${base}/${name}`
// 处理动态路由:[id].vue -> :id
const normalizedPath = routePath.replace(/\[(\w+)\]/g, ':$1')
routes.push({
path: normalizedPath,
component: fullPath,
name: normalizedPath.replace(/\//g, '-').replace(/^-/, '') || 'home',
})
}
}
return routes
}
/**
* 将路由配置转换为可执行的模块代码
*/
function generateRouteCode(routes) {
const imports = []
const routeObjects = []
function processRoutes(routeList, level = 0) {
return routeList.map((route) => {
if (route.component) {
const importName = `Page_${imports.length}`
imports.push(
`const ${importName} = () => import(${JSON.stringify(route.component)})`
)
return `{
path: ${JSON.stringify(route.path)},
name: ${JSON.stringify(route.name)},
component: ${importName},
}`
} else {
const children = processRoutes(route.children || [])
return `{
path: ${JSON.stringify(route.path)},
children: [${children.join(',')}],
}`
}
})
}
const routeStrings = processRoutes(routes)
return `
${imports.join('\n')}
export const routes = [
${routeStrings.join(',\n ')}
]
export default routes
`.trim()
}
export default function vitePluginAutoRoutes(options = {}) {
const {
pagesDir = 'src/pages',
extensions = ['vue', 'jsx', 'tsx'],
} = options
let pagesAbsPath
let viteConfig
return {
name: 'vite-plugin-auto-routes',
enforce: 'pre',
configResolved(config) {
viteConfig = config
pagesAbsPath = path.resolve(config.root, pagesDir)
},
resolveId(id) {
if (id === VIRTUAL_ID) {
return RESOLVED_ID
}
},
load(id) {
if (id !== RESOLVED_ID) return null
const routes = scanRoutes(pagesAbsPath)
return generateRouteCode(routes)
},
configureServer(server) {
// 监听 pages 目录变化,使虚拟模块失效
server.watcher.add(pagesAbsPath)
server.watcher.on('add', invalidate)
server.watcher.on('unlink', invalidate)
server.watcher.on('addDir', invalidate)
server.watcher.on('unlinkDir', invalidate)
function invalidate(file) {
if (!file.startsWith(pagesAbsPath)) return
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
if (mod) {
server.moduleGraph.invalidateModule(mod)
server.ws.send({ type: 'full-reload' })
}
}
},
}
}
使用方式:
// vite.config.js
import autoRoutes from './vite-plugin-auto-routes'
export default {
plugins: [
autoRoutes({ pagesDir: 'src/pages' }),
],
}
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from 'virtual:auto-routes'
export const router = createRouter({
history: createWebHistory(),
routes,
})
SSR(服务端渲染)
SSR 项目结构
project/
├── src/
│ ├── main.js # 通用代码,导出 createApp 工厂函数
│ ├── entry-client.js # 客户端入口:挂载到 DOM
│ ├── entry-server.js # 服务端入口:返回渲染结果
│ └── App.vue
├── server.js # Express 服务器
├── index.html # HTML 模板
└── vite.config.js
src/main.js(通用代码):
import { createApp } from 'vue'
import App from './App.vue'
import { createRouter } from './router'
// 每次请求都需要创建全新的实例,避免状态污染
export function createVueApp() {
const app = createApp(App)
const router = createRouter()
app.use(router)
return { app, router }
}
src/entry-client.js:
import { createVueApp } from './main'
const { app, router } = createVueApp()
// 等待路由就绪后再挂载,确保 hydration 正确
router.isReady().then(() => {
app.mount('#app')
})
src/entry-server.js:
import { renderToString } from 'vue/server-renderer'
import { createVueApp } from './main'
export async function render(url, manifest) {
const { app, router } = createVueApp()
await router.push(url)
await router.isReady()
const ctx = {}
const html = await renderToString(app, ctx)
// 提取预加载链接(需要 manifest)
const preloadLinks = manifest
? renderPreloadLinks(ctx.modules, manifest)
: ''
return { html, preloadLinks }
}
function renderPreloadLinks(modules, manifest) {
let links = ''
const seen = new Set()
modules.forEach((id) => {
const files = manifest[id]
if (files) {
files.forEach((file) => {
if (!seen.has(file)) {
seen.add(file)
const ext = file.split('.').pop()
if (ext === 'js') {
links += `<link rel="modulepreload" crossorigin href="${file}">`
} else if (ext === 'css') {
links += `<link rel="stylesheet" href="${file}">`
}
}
})
}
})
return links
}
开发环境 SSR 配置
server.js(开发 + 生产合一):
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import express from 'express'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const isProd = process.env.NODE_ENV === 'production'
async function createExpressServer() {
const app = express()
let vite
let template
let render
if (!isProd) {
// 开发模式:使用 Vite 中间件
const { createServer } = await import('vite')
vite = await createServer({
root: __dirname,
appType: 'custom', // 不使用 Vite 的 HTML 处理逻辑
server: {
middlewareMode: true, // 中间件模式,不启动内置 HTTP 服务器
},
})
// 挂载 Vite 中间件(处理模块热更新等)
app.use(vite.middlewares)
} else {
// 生产模式:服务静态文件
app.use(express.static(path.resolve(__dirname, 'dist/client'), {
index: false, // 不自动提供 index.html,由 SSR 处理
}))
}
app.use('*', async (req, res) => {
const url = req.originalUrl
try {
if (!isProd) {
// 开发模式:每次请求都读取最新的 HTML 和模块
template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8')
template = await vite.transformIndexHtml(url, template)
// ssrLoadModule 自动处理 ESM 和热更新
const serverEntry = await vite.ssrLoadModule('/src/entry-server.js')
render = serverEntry.render
} else {
// 生产模式:缓存 template 和 render
if (!template) {
template = fs.readFileSync(
path.resolve(__dirname, 'dist/client/index.html'),
'utf-8'
)
}
if (!render) {
render = (await import('./dist/server/entry-server.js')).render
}
}
const manifest = isProd
? JSON.parse(fs.readFileSync(
path.resolve(__dirname, 'dist/client/.vite/ssr-manifest.json'),
'utf-8'
))
: {}
const { html: appHtml, preloadLinks } = await render(url, manifest)
const finalHtml = template
.replace('<!--preload-links-->', preloadLinks)
.replace('<!--app-html-->', appHtml)
res.status(200).set({ 'Content-Type': 'text/html' }).end(finalHtml)
} catch (e) {
// 让 Vite 修复错误堆栈(映射回源码)
if (vite) vite.ssrFixStacktrace(e)
console.error(e)
res.status(500).end(e.message)
}
})
return app
}
createExpressServer().then((app) => {
app.listen(3000, () => {
console.log('SSR 服务器启动:http://localhost:3000')
})
})
生产环境 SSR 构建
package.json scripts:
{
"scripts": {
"dev": "node server.js",
"build": "npm run build:client && npm run build:server",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --outDir dist/server --ssr src/entry-server.js",
"preview": "cross-env NODE_ENV=production node server.js"
}
}
vite.config.js(SSR 项目):
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
// SSR manifest 用于生成 preload links
ssrManifest: true,
},
ssr: {
// 强制将这些包打包进 SSR bundle(不外部化)
noExternal: ['some-lib-with-css', /^@my-org\//],
},
})
SSR Options 参数表
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| ssr.target | 'node' | 'webworker' |
'node' |
SSR 构建目标环境 |
| ssr.format | 'esm' | 'cjs' |
'esm' |
SSR 产物格式 |
| ssr.noExternal | string | RegExp | (string | RegExp)[] | true |
— | 强制将指定依赖打包进 SSR bundle。true 表示打包所有依赖 |
| ssr.external | string[] | true |
— | 强制将指定依赖外部化(不打包)。true 表示外部化所有依赖 |
| ssr.resolve.conditions | string[] | — | 解析依赖的 package.json exports 条件,叠加到默认条件上 |
| ssr.resolve.externalConditions | string[] | ['node'] |
解析外部化 SSR 依赖时使用的条件 |
| ssr.optimizeDeps.enabled | boolean | — | 是否为 SSR 启用依赖优化 |
中间件模式
中间件模式允许 Vite 作为现有 HTTP 服务器的中间件,而非独立服务器。
server.middlewareMode 配置
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| server.middlewareMode | boolean | false | 启用中间件模式,Vite 不创建 HTTP 服务器 |
| appType | 'spa' | 'mpa' | 'custom' |
'spa' |
指定应用类型。custom 表示完全自定义 HTML 处理 |
与 Express 集成
import express from 'express'
import { createServer as createViteServer } from 'vite'
const app = express()
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'custom',
})
// 挂载 Vite 中间件(必须在自定义路由之前)
app.use(vite.middlewares)
// 自定义 API 路由
app.get('/api/data', (req, res) => {
res.json({ data: 'from server' })
})
// 兜底:所有非 API 路由交给 Vite/SSR 处理
app.use('*', async (req, res) => {
// ... SSR 逻辑
})
app.listen(3000)
与 Koa 集成
import Koa from 'koa'
import { createServer as createViteServer } from 'vite'
import { createReadStream } from 'node:fs'
import { resolve } from 'node:path'
const app = new Koa()
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'custom',
})
// 将 Connect 风格的中间件转换为 Koa 中间件
app.use(async (ctx, next) => {
await new Promise((resolve, reject) => {
vite.middlewares(ctx.req, ctx.res, (err) => {
if (err) reject(err)
else resolve()
})
})
await next()
})
// Koa 自定义路由
app.use(async (ctx) => {
if (ctx.path.startsWith('/api')) {
ctx.body = { message: 'API response' }
return
}
// SSR 处理
const template = await vite.transformIndexHtml(
ctx.url,
'<html><body><!--app--></body></html>'
)
ctx.type = 'text/html'
ctx.body = template
})
app.listen(3000, () => {
console.log('Koa + Vite 服务器启动:http://localhost:3000')
})
JavaScript API
createServer(inlineConfig)
以编程方式创建 Vite 开发服务器。
参数:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| inlineConfig | InlineConfig | {} | Vite 配置(与配置文件深度合并) |
返回值: Promise<ViteDevServer>
ViteDevServer 完整 API
| 属性/方法 | 类型 | 说明 |
|---|---|---|
| config | ResolvedConfig | 最终解析后的 Vite 配置 |
| httpServer | http.Server | null | Node.js HTTP 服务器实例。中间件模式下为 null |
| watcher | FSWatcher | Chokidar 文件监听器实例 |
| ws | WebSocketServer | WebSocket 服务器,用于 HMR 通信 |
| moduleGraph | ModuleGraph | 模块依赖图,追踪模块间的导入关系 |
| pluginContainer | PluginContainer | 插件容器,可调用插件钩子 |
| transformRequest(url, options?) | Promise<TransformResult | null> | 对指定 URL 的模块执行 Vite 管道(解析、加载、转换) |
| ssrLoadModule(url, options?) | Promise<Record<string, any>> | 加载 SSR 模块(Node.js 环境中执行) |
| ssrFixStacktrace(e) | void | 修复 SSR 错误的堆栈信息,映射回源码位置 |
| listen(port?, isRestart?) | Promise |
启动 HTTP 服务器监听 |
| close() | Promise |
关闭服务器,停止文件监听和 WebSocket |
| restart(forceOptimize?) | Promise |
重启服务器。forceOptimize=true 强制重新优化依赖 |
| printUrls() | void | 打印服务器地址信息 |
| bindCLIShortcuts(options?) | void | 绑定 CLI 快捷键(q 退出,r 重启等) |
编程方式使用示例:
import { createServer } from 'vite'
const vite = await createServer({
root: process.cwd(),
server: {
port: 5173,
host: true,
},
})
await vite.listen()
vite.printUrls()
// 手动触发模块转换
const result = await vite.transformRequest('/src/main.js')
console.log(result?.code)
// 关闭服务器
process.on('SIGINT', async () => {
await vite.close()
process.exit(0)
})
build(inlineConfig)
以编程方式执行 Vite 构建。
参数:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| inlineConfig | InlineConfig | {} | Vite 配置(与配置文件深度合并) |
返回值: Promise<RollupOutput | RollupOutput[] | RollupWatcher>
import { build } from 'vite'
// 基础构建
const result = await build({
root: process.cwd(),
build: {
outDir: 'dist',
},
})
// result 是 RollupOutput 数组,包含所有输出文件信息
console.log('输出文件:', result.output.map(f => f.fileName))
// watch 模式(返回 RollupWatcher)
const watcher = await build({
build: {
watch: {},
},
})
watcher.on('event', (event) => {
if (event.code === 'BUNDLE_END') {
console.log('构建完成,耗时:', event.duration, 'ms')
}
})
preview(inlineConfig)
以编程方式启动预览服务器(服务 build 产物)。
参数:
| 参数名 | 类型 | 说明 |
|---|---|---|
| inlineConfig | InlineConfig | Vite 配置 |
返回值: Promise<PreviewServer>
import { preview } from 'vite'
const previewServer = await preview({
preview: {
port: 4173,
open: true,
},
})
previewServer.printUrls()
resolveConfig(inlineConfig, command, mode)
解析 Vite 配置(合并配置文件与 inlineConfig),不启动服务器。
参数:
| 参数名 | 类型 | 说明 |
|---|---|---|
| inlineConfig | InlineConfig | 内联配置 |
| command | 'build' | 'serve' |
模拟的命令 |
| mode | string | 模拟的 mode |
返回值: Promise<ResolvedConfig>
import { resolveConfig } from 'vite'
const config = await resolveConfig({}, 'build', 'production')
console.log('解析后的 base:', config.base)
console.log('所有插件:', config.plugins.map(p => p.name))
loadConfigFromFile(configEnv, configFile?)
加载 Vite 配置文件。
参数:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| configEnv.command | 'build' | 'serve' |
必填 | 当前命令 |
| configEnv.mode | string | 必填 | 当前 mode |
| configFile | string | 自动查找 | 配置文件路径,默认自动查找 vite.config.* |
返回值: Promise<{ path, config, dependencies } | null>
import { loadConfigFromFile } from 'vite'
const result = await loadConfigFromFile(
{ command: 'build', mode: 'production' },
'./vite.config.ts'
)
if (result) {
console.log('配置文件路径:', result.path)
console.log('配置内容:', result.config)
console.log('依赖的文件:', result.dependencies)
}
性能优化
开发服务器优化
server.warmup 预热关键文件
在服务器启动时提前转换关键模块,减少首次访问延迟:
export default {
server: {
warmup: {
// 预热客户端模块(路由入口、核心组件等)
clientFiles: [
'./src/main.js',
'./src/components/Layout.vue',
'./src/router/index.js',
],
// 预热 SSR 模块
ssrFiles: [
'./src/entry-server.js',
],
},
},
}
| 参数名 | 类型 | 说明 |
|---|---|---|
| server.warmup.clientFiles | string[] | 预热的客户端模块路径(支持 glob) |
| server.warmup.ssrFiles | string[] | 预热的 SSR 模块路径(支持 glob) |
optimizeDeps 减少首次启动扫描
export default {
optimizeDeps: {
// 明确指定需要预构建的依赖(避免 Vite 全量扫描)
include: [
'vue',
'vue-router',
'pinia',
'axios',
'lodash-es',
// 对于深层导入也要声明
'some-lib > nested-dep',
],
// 排除不需要预构建的依赖
exclude: ['@vite/client', '@vite/env'],
// 对需要转换的依赖进行 esbuild 配置
esbuildOptions: {
target: 'esnext',
},
// 强制重新构建(调试时使用)
force: false,
},
}
optimizeDeps 参数表:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| optimizeDeps.entries | string | string[] | — | 指定扫描入口,默认扫描 index.html |
| optimizeDeps.exclude | string[] | [] | 排除预构建的依赖列表 |
| optimizeDeps.include | string[] | [] | 强制预构建的依赖列表(即使被 exclude 排除) |
| optimizeDeps.esbuildOptions | EsbuildOptions | — | 传递给 esbuild 的选项 |
| optimizeDeps.force | boolean | false | 忽略缓存,强制重新预构建 |
| optimizeDeps.holdUntilCrawlEnd | boolean | true | 是否等待爬取结束再发送预构建模块 |
构建产物优化
代码分割策略(manualChunks)
export default {
build: {
rollupOptions: {
output: {
manualChunks(id) {
// Vue 相关库单独打包
if (id.includes('/node_modules/vue') ||
id.includes('/node_modules/@vue')) {
return 'vue-vendor'
}
// UI 组件库单独打包
if (id.includes('/node_modules/element-plus') ||
id.includes('/node_modules/@element-plus')) {
return 'element-plus'
}
// 工具库单独打包
if (id.includes('/node_modules/lodash-es') ||
id.includes('/node_modules/dayjs')) {
return 'utils'
}
// 其余第三方依赖合并为 vendor
if (id.includes('/node_modules/')) {
return 'vendor'
}
// 业务代码按模块分割
if (id.includes('/src/views/admin/')) {
return 'admin'
}
},
},
},
},
}
对象形式(静态声明):
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui': ['element-plus'],
'utils': ['lodash-es', 'dayjs', 'axios'],
},
},
},
},
}
build.rollupOptions.output 参数表
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| chunkFileNames | string | function | 'assets/[name]-[hash].js' |
非入口 chunk 的文件名模板 |
| entryFileNames | string | function | 'assets/[name]-[hash].js' |
入口 chunk 的文件名模板 |
| assetFileNames | string | function | 'assets/[name]-[hash][extname]' |
静态资源文件名模板 |
| manualChunks | Record<string, string[]> | function | — | 自定义 chunk 分割策略 |
| format | 'es' | 'cjs' | 'umd' | 'iife' | 'system' |
'es' |
输出格式 |
| globals | Record<string, string> | — | UMD/IIFE 格式中外部依赖的全局变量名 |
| inlineDynamicImports | boolean | false | 是否将动态导入内联(不分割 chunk) |
| sourcemap | boolean | 'inline' | 'hidden' |
false | 是否生成 sourcemap |
文件名模板可用占位符:
| 占位符 | 说明 |
|---|---|
| [name] | chunk 名称 |
| [hash] | 基于内容的 hash |
| [format] | 输出格式 |
| [extname] | 文件扩展名(含点号) |
build 核心参数表
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| build.target | string | string[] | 'modules' |
浏览器兼容目标,传 esbuild target 格式 |
| build.outDir | string | 'dist' |
构建输出目录 |
| build.assetsDir | string | 'assets' |
静态资源子目录名 |
| build.assetsInlineLimit | number | 4096 |
小于此字节的资源转为 base64 内联 |
| build.cssCodeSplit | boolean | true | 启用 CSS 代码分割 |
| build.sourcemap | boolean | 'inline' | 'hidden' |
false | 生成 sourcemap |
| build.rollupOptions | RollupOptions | {} | 传递给 Rollup 的配置 |
| build.lib | LibraryOptions | — | 构建库模式配置 |
| build.ssr | boolean | string | — | 启用 SSR 构建,传字符串时指定入口 |
| build.ssrManifest | boolean | string | false | 生成 SSR manifest 文件 |
| build.reportCompressedSize | boolean | true | 在终端输出 gzip 压缩后的大小 |
| build.chunkSizeWarningLimit | number | 500 | chunk 大小警告阈值(KB) |
| build.minify | boolean | 'terser' | 'esbuild' |
'esbuild' |
压缩工具选择 |
| build.terserOptions | TerserOptions | — | Terser 压缩选项(minify: 'terser' 时有效) |
| build.emptyOutDir | boolean | true | 构建前清空输出目录 |
| build.copyPublicDir | boolean | true | 是否将 public 目录文件复制到 outDir |
Tree Shaking 优化
在 package.json 中声明无副作用:
{
"name": "my-lib",
"sideEffects": false
}
或精确声明有副作用的文件:
{
"sideEffects": [
"*.css",
"./src/polyfills.js"
]
}
避免 import * 的写法:
// 不推荐:整个 lodash 都会被打包
import _ from 'lodash'
import * as _ from 'lodash'
// 推荐:只打包使用的函数
import { debounce, throttle } from 'lodash-es'
// 推荐:按需导入(lodash-es 支持 tree shaking)
import debounce from 'lodash-es/debounce'
在 vite.config.js 中配合 optimizeDeps:
export default {
optimizeDeps: {
include: ['lodash-es'],
},
}
大包分析
使用 rollup-plugin-visualizer:
npm install -D rollup-plugin-visualizer
import { defineConfig } from 'vite'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer({
open: true, // 构建后自动打开浏览器
filename: 'stats.html', // 分析报告文件名
gzipSize: true, // 显示 gzip 后的大小
brotliSize: true, // 显示 brotli 后的大小
template: 'treemap', // 可选 'sunburst', 'network', 'raw-data', 'list'
}),
],
build: {
// 调整 chunk 大小警告阈值
chunkSizeWarningLimit: 1000, // KB
// 关闭压缩大小报告(大型项目可加快构建速度)
reportCompressedSize: false,
},
})
环境变量进阶与多环境部署
loadEnv() 在配置文件中使用
loadEnv 允许在 vite.config.js 中读取 .env 文件中的变量:
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig(({ command, mode }) => {
// 第三个参数为前缀过滤,'' 表示加载所有变量(包括 VITE_ 前缀和无前缀)
const env = loadEnv(mode, process.cwd(), '')
return {
plugins: [vue()],
define: {
// 将 Node.js 环境变量注入到客户端代码
__APP_VERSION__: JSON.stringify(env.npm_package_version),
__BUILD_TIME__: JSON.stringify(new Date().toISOString()),
},
server: {
proxy: {
'/api': {
target: env.VITE_API_BASE_URL || 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
build: {
sourcemap: env.VITE_SOURCEMAP === 'true',
},
}
})
loadEnv 参数:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| mode | string | 必填 | 环境名(development/production/staging 等) |
| envDir | string | 必填 | .env 文件所在目录,通常为 process.cwd() |
| prefixes | string | string[] | 'VITE_' |
变量前缀过滤,传 '' 加载所有变量 |
多环境配置示例
目录结构:
.env # 所有环境共享的基础变量
.env.development # 本地开发
.env.staging # 测试环境
.env.production # 生产环境
.env(基础):
VITE_APP_NAME=My App
VITE_APP_VERSION=1.0.0
.env.development:
VITE_API_BASE_URL=http://localhost:8080
VITE_SOURCEMAP=true
VITE_MOCK_ENABLED=true
VITE_LOG_LEVEL=debug
.env.staging:
VITE_API_BASE_URL=https://staging-api.example.com
VITE_SOURCEMAP=true
VITE_MOCK_ENABLED=false
VITE_LOG_LEVEL=info
.env.production:
VITE_API_BASE_URL=https://api.example.com
VITE_SOURCEMAP=false
VITE_MOCK_ENABLED=false
VITE_LOG_LEVEL=error
package.json scripts:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"build:staging": "vite build --mode staging",
"build:production": "vite build --mode production",
"preview": "vite preview"
}
}
TypeScript ImportMetaEnv 扩展:
// src/env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
// 基础(Vite 内置)
readonly MODE: string
readonly BASE_URL: string
readonly PROD: boolean
readonly DEV: boolean
readonly SSR: boolean
// 自定义变量
readonly VITE_APP_NAME: string
readonly VITE_APP_VERSION: string
readonly VITE_API_BASE_URL: string
readonly VITE_SOURCEMAP: string
readonly VITE_MOCK_ENABLED: string
readonly VITE_LOG_LEVEL: 'debug' | 'info' | 'warn' | 'error'
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
在代码中使用:
// 有类型提示和自动补全
const apiUrl = import.meta.env.VITE_API_BASE_URL
const isMockEnabled = import.meta.env.VITE_MOCK_ENABLED === 'true'
const appName = import.meta.env.VITE_APP_NAME
Preview Options 完整配置
preview 选项用于配置 vite preview 命令启动的预览服务器(用于本地预览构建产物)。
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| preview.host | string | boolean | 'localhost' |
监听的主机名。true 或 '0.0.0.0' 监听所有地址 |
| preview.allowedHosts | string[] | true | — | 允许访问的主机名列表。true 表示允许所有主机 |
| preview.port | number | 4173 | 监听端口号 |
| preview.strictPort | boolean | false | 端口占用时是否直接退出(false 则自动递增) |
| preview.https | https.ServerOptions | — | 启用 HTTPS,传入证书配置 |
| preview.open | boolean | string | false | 启动后自动打开浏览器。字符串时指定打开的 URL 路径 |
| preview.proxy | Record<string, ProxyOptions> | — | 代理配置,格式与 server.proxy 相同 |
| preview.cors | boolean | CorsOptions | — | 配置 CORS |
| preview.headers | OutgoingHttpHeaders | — | 指定响应头 |
export default {
preview: {
host: '0.0.0.0',
port: 4173,
strictPort: true,
https: {
key: fs.readFileSync('./certs/key.pem'),
cert: fs.readFileSync('./certs/cert.pem'),
},
open: true,
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
},
},
cors: true,
headers: {
'X-Custom-Header': 'my-value',
'Cache-Control': 'no-cache',
},
},
}
Worker Options 完整配置
worker 选项用于配置 Web Worker 的打包方式。
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| worker.format | 'es' | 'iife' |
'iife' |
Worker bundle 的输出格式。iife 兼容性更好,es 支持 import |
| worker.plugins | () => Plugin[] |
— | 应用于 Worker bundle 的 Vite 插件(工厂函数形式) |
| worker.rollupOptions | RollupOptions | — | Worker bundle 的 Rollup 配置 |
export default {
worker: {
// 使用 ES 模块格式(支持在 Worker 中使用 import)
format: 'es',
// Worker 专用插件(使用工厂函数避免实例共享)
plugins: () => [myWorkerPlugin()],
rollupOptions: {
output: {
// Worker 产物命名规则
entryFileNames: 'workers/[name]-[hash].js',
},
},
},
}
在代码中使用 Worker:
// 使用 ?worker 后缀导入
import MyWorker from './my.worker.js?worker'
const worker = new MyWorker()
worker.postMessage({ type: 'start', data: [1, 2, 3] })
worker.onmessage = (e) => {
console.log('Worker 结果:', e.data)
}
// 内联 Worker(打包进主 bundle)
import InlineWorker from './my.worker.js?worker&inline'
// SharedWorker
import SharedWorkerClass from './shared.worker.js?sharedworker'
const sharedWorker = new SharedWorkerClass()
Worker 文件示例(my.worker.js):
// Worker 内不能访问 window、document 等 DOM API
self.onmessage = (e) => {
const { type, data } = e.data
if (type === 'start') {
// 执行计算密集型任务
const result = data.reduce((sum, n) => sum + n, 0)
self.postMessage({ result })
}
}
打包与部署
Docker 多阶段构建
# Stage 1:构建阶段
FROM node:22-alpine AS build
WORKDIR /app
# 先复制依赖文件,利用 Docker 层缓存
COPY package.json package-lock.json ./
RUN npm ci --prefer-offline
# 复制源代码并构建
COPY . .
RUN npm run build
# Stage 2:生产阶段(仅包含静态文件)
FROM nginx:1.25-alpine AS production
# 复制构建产物
COPY --from=build /app/dist /usr/share/nginx/html
# 复制自定义 Nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx.conf(SPA 路由 + 性能优化):
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# 开启 gzip 压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/json
application/xml+rss
image/svg+xml;
# 静态资源长期缓存(Vite 产物带 hash,可永久缓存)
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# index.html 不缓存(确保用户获取最新版本)
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# 安全响应头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
.dockerignore:
node_modules
dist
.git
.gitignore
*.md
.env*
构建和运行:
docker build -t my-vite-app .
docker run -p 80:80 my-vite-app
GitHub Actions CI/CD
部署到 GitHub Pages:
# .github/workflows/deploy.yml
name: Deploy to GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
# 设置 GITHUB_TOKEN 权限
permissions:
contents: read
pages: write
id-token: write
# 防止并发部署
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
VITE_API_BASE_URL: ${{ secrets.VITE_API_BASE_URL }}
NODE_ENV: production
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: dist
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
部署到 VPS(通过 SSH):
# .github/workflows/deploy-vps.yml
name: Deploy to VPS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install and Build
run: |
npm ci
npm run build
env:
VITE_API_BASE_URL: ${{ secrets.VITE_API_BASE_URL }}
- name: Deploy to VPS via SCP
uses: appleboy/[email protected]
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
source: 'dist/*'
target: '/var/www/my-app'
strip_components: 1
- name: Restart Nginx
uses: appleboy/[email protected]
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: nginx -s reload
CDN 部署(base 配置)
部署到子路径:
export default {
base: '/my-app/', // 所有资源路径前缀
}
部署到 CDN:
export default defineConfig(({ command }) => ({
base: command === 'build'
? 'https://cdn.example.com/assets/my-app/'
: '/',
}))
动态 base(运行时注入):
// vite.config.js
export default {
experimental: {
renderBuiltUrl(filename, { hostId, hostType, type }) {
if (type === 'asset') {
return `https://cdn.example.com/${filename}`
}
// 返回 undefined 使用默认处理
},
},
}
Monorepo 配置
目录结构
my-monorepo/
├── package.json # 根 package.json(workspaces 配置)
├── pnpm-workspace.yaml # pnpm workspaces 配置(若用 pnpm)
├── packages/
│ ├── ui/ # 组件库
│ │ ├── package.json
│ │ ├── vite.config.js
│ │ └── src/
│ ├── app/ # 主应用
│ │ ├── package.json
│ │ ├── vite.config.js
│ │ └── src/
│ └── shared/ # 共享工具代码
│ ├── package.json
│ └── src/
└── node_modules/
根 package.json(npm workspaces):
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/*"
]
}
pnpm-workspace.yaml:
packages:
- 'packages/*'
resolve.dedupe 解决 Monorepo 依赖问题
在 Monorepo 中,不同包可能各自安装了同一依赖的不同副本,导致问题(如 Vue 实例不唯一)。
// packages/app/vite.config.js
import { defineConfig } from 'vite'
import { resolve } from 'path'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: {
// 强制使用根 node_modules 中的单一副本
dedupe: ['vue', 'vue-router', 'pinia', 'react', 'react-dom'],
// 若使用 pnpm,symlink 指向真实路径,设为 false 避免路径解析问题
preserveSymlinks: false,
alias: {
// 将 workspace 包别名指向源码(避免引用编译后版本)
'@my-org/ui': resolve(__dirname, '../ui/src'),
'@my-org/shared': resolve(__dirname, '../shared/src'),
},
},
optimizeDeps: {
// workspace 包的源码中如果有未预构建的依赖,需要显式包含
include: [
'vue',
'pinia',
// 对于 workspace 包中的深层依赖
'@my-org/ui > some-dep',
],
// 排除 workspace 包本身(它们会被 Vite 直接处理)
exclude: ['@my-org/ui', '@my-org/shared'],
},
})
完整 Monorepo vite.config.js 示例
packages/ui/vite.config.js(库模式):
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
build: {
// 库模式构建
lib: {
entry: resolve(__dirname, 'src/index.js'),
name: 'MyUI',
formats: ['es', 'cjs'],
fileName: (format) => `my-ui.${format}.js`,
},
rollupOptions: {
// 外部化 peer dependencies
external: ['vue'],
output: {
globals: { vue: 'Vue' },
// ES 模块保留目录结构(方便 tree shaking)
preserveModules: true,
preserveModulesRoot: 'src',
},
},
// 生成类型声明
emptyOutDir: true,
},
})
packages/app/vite.config.js(应用模式):
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
dedupe: ['vue', 'vue-router', 'pinia'],
alias: {
'@': resolve(__dirname, 'src'),
'@my-org/ui': resolve(__dirname, '../ui/src'),
'@my-org/shared': resolve(__dirname, '../shared/src'),
},
},
optimizeDeps: {
include: [
'vue',
'vue-router',
'pinia',
// 处理 workspace 包内的 CommonJS 依赖
'@my-org/ui > some-cjs-lib',
],
},
server: {
fs: {
// 允许访问 packages/ 目录(跨 workspace 引用时需要)
allow: ['../..'],
},
},
})
高级插件示例:自定义 Mock 插件
完整可运行的 vite-plugin-mock 实现:
// vite-plugin-mock/index.js
import fs from 'node:fs'
import path from 'node:path'
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
/**
* 解析请求体
*/
async function parseBody(req) {
return new Promise((resolve) => {
let body = ''
req.on('data', (chunk) => { body += chunk.toString() })
req.on('end', () => {
try {
resolve(body ? JSON.parse(body) : {})
} catch {
resolve({})
}
})
})
}
/**
* 从 mock 目录加载所有 mock 文件
*/
function loadMockFiles(mockDir) {
const mocks = []
if (!fs.existsSync(mockDir)) return mocks
const files = fs.readdirSync(mockDir).filter(f => f.endsWith('.js'))
for (const file of files) {
const filePath = path.join(mockDir, file)
try {
// 清除 require 缓存,支持热重载
delete require.cache[filePath]
const mockModule = require(filePath)
const mockList = Array.isArray(mockModule) ? mockModule : [mockModule]
mocks.push(...mockList)
} catch (e) {
console.error(`[vite-plugin-mock] 加载 mock 文件失败:${file}`, e.message)
}
}
return mocks
}
/**
* 匹配 mock 路由
*/
function matchMock(mocks, url, method) {
return mocks.find((mock) => {
const methodMatch = !mock.method || mock.method.toUpperCase() === method.toUpperCase()
const urlMatch = typeof mock.url === 'string'
? mock.url === url.split('?')[0]
: mock.url instanceof RegExp
? mock.url.test(url)
: false
return methodMatch && urlMatch
})
}
export default function vitePluginMock(options = {}) {
const {
mockDir = 'mock', // mock 文件目录
enable = true, // 是否启用
logger = true, // 是否打印日志
} = options
return {
name: 'vite-plugin-mock',
apply: 'serve', // 仅在开发服务器中启用
configureServer(server) {
if (!enable) return
const mockAbsDir = path.resolve(process.cwd(), mockDir)
let mocks = loadMockFiles(mockAbsDir)
// 监听 mock 目录变化,自动重新加载
server.watcher.add(mockAbsDir)
server.watcher.on('change', (file) => {
if (file.startsWith(mockAbsDir)) {
mocks = loadMockFiles(mockAbsDir)
if (logger) {
console.log(`[vite-plugin-mock] mock 文件已更新:${path.basename(file)}`)
}
}
})
// 在内置中间件之前插入 mock 中间件
server.middlewares.use(async (req, res, next) => {
const matched = matchMock(mocks, req.url, req.method)
if (!matched) return next()
if (logger) {
console.log(`[vite-plugin-mock] ${req.method} ${req.url}`)
}
// 支持延迟响应
if (matched.delay && matched.delay > 0) {
await new Promise(resolve => setTimeout(resolve, matched.delay))
}
const body = await parseBody(req)
// 支持函数形式的响应(动态响应)
let responseData
if (typeof matched.response === 'function') {
responseData = await matched.response({
url: req.url,
method: req.method,
body,
headers: req.headers,
query: Object.fromEntries(new URL(req.url, 'http://localhost').searchParams),
})
} else {
responseData = matched.response
}
const statusCode = matched.statusCode || 200
res.writeHead(statusCode, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
...(matched.headers || {}),
})
res.end(JSON.stringify(responseData))
})
},
}
}
Mock 文件示例(mock/user.js):
// mock/user.js
module.exports = [
{
url: '/api/users',
method: 'GET',
delay: 200, // 模拟 200ms 延迟
response: ({ query }) => {
const page = parseInt(query.page) || 1
const pageSize = parseInt(query.pageSize) || 10
const users = Array.from({ length: pageSize }, (_, i) => ({
id: (page - 1) * pageSize + i + 1,
name: `用户 ${(page - 1) * pageSize + i + 1}`,
email: `user${i + 1}@example.com`,
role: i % 3 === 0 ? 'admin' : 'user',
}))
return {
code: 0,
message: 'success',
data: {
list: users,
total: 100,
page,
pageSize,
},
}
},
},
{
url: '/api/users/login',
method: 'POST',
delay: 500,
response: ({ body }) => {
const { username, password } = body
if (username === 'admin' && password === '123456') {
return {
code: 0,
message: '登录成功',
data: {
token: 'mock-jwt-token-' + Date.now(),
userInfo: { id: 1, name: 'Admin', role: 'admin' },
},
}
}
return {
code: 401,
message: '用户名或密码错误',
data: null,
}
},
},
{
url: /^\/api\/users\/\d+$/, // 正则匹配
method: 'GET',
response: ({ url }) => {
const id = url.split('/').pop()
return {
code: 0,
data: { id: parseInt(id), name: `用户 ${id}`, email: `user${id}@example.com` },
}
},
},
]
vite.config.js 使用:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import mockPlugin from './vite-plugin-mock'
export default defineConfig(({ command }) => ({
plugins: [
vue(),
mockPlugin({
mockDir: 'mock',
enable: command === 'serve', // 仅开发环境启用
logger: true,
}),
],
}))
踩坑与最佳实践清单
SSR Hydration 不匹配问题
问题描述: 服务端渲染的 HTML 与客户端 Vue 重新渲染的结果不一致,导致 hydration 失败,出现闪烁或警告。
常见原因及解决方案:
// 错误:在服务端和客户端使用不一致的随机值
const id = Math.random() // 每次调用结果不同
// 错误:直接使用 Date.now()
const timestamp = Date.now()
// 正确:使用可预测的、基于请求的数据
// 在 SSR 中通过 provide/inject 传递,确保客户端复水时一致
// 错误:服务端访问了 window 对象
if (window !== undefined) { ... }
// 正确:使用平台判断
if (typeof window !== 'undefined') { ... }
// 或在 onMounted 中处理(仅在客户端运行)
onMounted(() => {
// 这里的代码只在客户端执行
})
build.target 和 define 组合使用时的注意事项
问题: 当 build.target 低于某些语法支持版本时,define 替换后的代码可能无法正确压缩。
// 问题示例
export default {
build: {
target: 'es2015', // 目标 ES2015
},
define: {
// 这里替换的是字面量,esbuild 会进行常量折叠
__DEV__: false,
// 注意:不要 define 对象或数组,只 define 基本类型
__CONFIG__: JSON.stringify({ key: 'value' }), // 正确
},
}
// 陷阱:define 的值在替换时是直接字符串替换,不是 AST 转换
// 错误:这会替换成 undefined(字符串)
define: {
'process.env.NODE_ENV': undefined, // 错误
}
// 正确:始终用 JSON.stringify 包裹字符串值
define: {
'process.env.NODE_ENV': JSON.stringify('production'), // 正确
}
manualChunks 循环依赖导致 chunk 爆炸
问题: 错误的 manualChunks 配置可能导致模块被重复打包到多个 chunk 中,造成产物体积暴增。
// 危险:如果 A 依赖 B,B 依赖 A,强行拆分会导致循环
// 正确做法:使用函数形式,让 Rollup 帮助处理依赖关系
// 推荐:先分析依赖关系,再配置(用 visualizer 先可视化)
build: {
rollupOptions: {
output: {
manualChunks(id) {
// 不要把相互依赖的模块拆分到不同 chunk
if (id.includes('/node_modules/')) {
// 安全做法:按大的包名分组,不要过细拆分
const match = id.match(/node_modules\/([^/]+)/)
if (match) {
const pkg = match[1]
// 只对体积大的包单独分割
if (['lodash-es', 'echarts', 'monaco-editor'].includes(pkg)) {
return pkg
}
return 'vendor'
}
}
},
},
},
}
插件 enforce 顺序问题
执行顺序总览:
enforce: 'pre' 插件
→ Vite 核心插件(HTML、alias 等)
→ 无 enforce 插件
→ Vite 构建插件
→ enforce: 'post' 插件
常见误区:
// 错误:在无 enforce 插件中尝试处理原始 import alias
// 此时 alias 已被 Vite 核心插件处理,拿到的已经是解析后的路径
// 正确:若需要在 alias 解析前处理,使用 enforce: 'pre'
{
name: 'my-resolver',
enforce: 'pre',
resolveId(source) {
// 此时 source 是原始导入字符串,alias 尚未替换
}
}
// transform 插件的顺序同样受 enforce 影响
// 若需要在 Vue/React 编译前处理源码,使用 enforce: 'pre'
// 若需要在编译后处理,使用 enforce: 'post'
server.fs.deny 拦截 .env 文件
默认行为: Vite 默认会拦截对 .env 文件的直接 HTTP 请求,防止敏感信息泄露。
// 注意:Vite 默认拒绝访问 .env 文件
// 如果你的 API 代理路径包含 '.env' 字符串,会被误拦截
// 错误的代理配置(路径含 .env)
server: {
proxy: {
'/api/.env-config': 'http://backend.com', // 会被 Vite 拦截!
}
}
// 查看默认 fs.deny 规则:
server: {
fs: {
deny: ['.env', '.env.*', '*.{crt,pem}'] // Vite 默认值
}
}
// 若确实需要允许某类文件(谨慎操作):
server: {
fs: {
// 移除 .env 拦截(不推荐)
deny: ['*.{crt,pem}'],
}
}
Worker 中不能使用 window
// 错误:Worker 中没有 window 对象
self.onmessage = (e) => {
window.location.href // ReferenceError: window is not defined
document.querySelector('.app') // ReferenceError: document is not defined
localStorage.getItem('key') // Worker 中不可用
}
// 正确:Worker 中使用 self 代替 window,且只能访问受限 API
self.onmessage = (e) => {
// 可用:self, postMessage, fetch, XMLHttpRequest, WebSocket
// 可用:setTimeout, setInterval, Promise, crypto
// 可用:IndexedDB(通过 self.indexedDB)
// 不可用:window, document, localStorage, sessionStorage, alert
fetch('/api/data').then(r => r.json()).then(data => {
self.postMessage(data)
})
}
其他最佳实践
避免在 vite.config.js 中进行大量文件系统操作:
// 不推荐:在配置中使用大量 glob(每次启动都会执行)
import { glob } from 'glob'
const entries = glob.sync('src/pages/**/*.vue') // 每次启动都扫描
// 推荐:将扫描逻辑放到插件的 buildStart 钩子中(仅构建时执行一次)
// 或放到 configureServer 中(仅开发时执行)
正确使用 this.emitFile 生成额外文件:
// 在插件中生成额外的输出文件
{
name: 'my-plugin',
generateBundle(options, bundle) {
// 生成额外文件
this.emitFile({
type: 'asset',
fileName: 'manifest.json',
source: JSON.stringify({ version: '1.0.0' }),
})
}
}
CSS Modules 命名冲突:
// vite.config.js
export default {
css: {
modules: {
// 开发模式:保留原始类名方便调试
generateScopedName: process.env.NODE_ENV === 'development'
? '[name]__[local]'
: '[hash:base64:8]',
// 避免不同模块的类名哈希冲突
hashPrefix: 'my-app',
},
},
}
避免在生产构建中包含开发工具:
// 正确做法:使用 import.meta.env.DEV 进行条件导入
if (import.meta.env.DEV) {
// 这段代码和导入会在生产构建中被 tree-shaking 移除
const { setupDevtools } = await import('./devtools')
setupDevtools(app)
}
最佳实践
插件的 enforce 字段明确执行时机:插件不声明 enforce 时默认在 Vite 核心插件之后运行。需要在 resolve 之前介入(如路径别名插件)用 enforce: 'pre';需要在 build 输出之后处理(如 HTML 注入)用 enforce: 'post'。混淆时机是插件 Bug 最常见的根源。
SSR 插件用 apply 字段区分 serve/build:部分插件(如热更新辅助)只在 serve 阶段有意义;另一些(如 CDN 替换)只在 build 阶段需要。用 apply: 'serve' 或 apply: 'build' 缩小插件作用范围,避免 SSR 构建时加载无用插件。
resolveId 返回虚拟模块时加 \0 前缀:Vite/Rollup 约定以 \0 开头的 ID 为虚拟模块,不会被其他插件(如 node_modules 解析)拦截处理,避免路径冲突。
resolveId(id) {
if (id === 'virtual:my-module') return '\0virtual:my-module'
},
load(id) {
if (id === '\0virtual:my-module') return `export const msg = 'hello'`
},
createServer 的 middlewareMode 用于嵌入现有 HTTP 框架:将 Vite 作为中间件集成到 Express/Fastify 时,设置 server: { middlewareMode: true },不让 Vite 启动自己的 HTTP 服务器,避免端口冲突和双重请求处理。
环境变量文件按优先级顺序覆盖:.env.production.local > .env.production > .env.local > .env,本地覆盖文件(.local)加入 .gitignore,不提交到仓库,团队共享变量放 .env.production。
常见陷阱
陷阱:SSR 构建产物在 Node.js 运行时报 ReferenceError: window is not defined
现象: SSR 模式下启动服务端渲染,某个第三方库在导入时直接访问 window/document,构建通过但运行时崩溃。
原因: Vite SSR 构建输出的是 Node.js 可执行的 CommonJS/ESM 包,但部分前端库假设浏览器环境,在模块顶层读取 window。
解决: 在 vite.config.ts 中把问题包加入 ssr.noExternal(强制打包并让 Vite 注入 shim),或在动态 import 中延迟加载该库,或在服务端环境提供 global.window = {} 占位。
陷阱:自定义插件的 transform 在生产构建中不触发
现象: 开发服务器下插件生效,vite build 后功能缺失。
原因: 部分开发者在 transform 钩子中写了提前返回逻辑(如 if (!this.server) return),或插件依赖开发服务器特有的 API(如 server.moduleGraph),导致生产构建路径跳过了转换。
解决: 在 configResolved 钩子中保存 config,区分 command === 'serve' 和 'build' 两条路径,确保 transform 在两种模式下都执行必要逻辑。
陷阱:build.rollupOptions.output.manualChunks 导致循环依赖警告
现象: 配置 manualChunks 将某些模块合并后,Rollup 输出大量 circular dependency 警告,或运行时出现变量未定义错误。
原因: manualChunks 强制将模块分组,若分组破坏了 Rollup 自动确定的模块加载顺序,会引入循环引用。
解决: 优先使用 manualChunks 的函数形式而非对象形式,让 Rollup 仍能分析依赖关系;避免把有相互依赖的模块拆到不同 chunk;出现问题时先移除 manualChunks 验证是否是它引起的。