Vite 5 + Vitest 深度实战:当前端构建遇上极速测试——从插件开发到生产级性能调优的完全指南(2026)
作者:程序员茄子 | 2026-06-13 | 预计阅读时间:45分钟 | 字数:约 18,000 字
目录
- 为什么 2026 年还要深度学 Vite 5?
- Vite 5 架构深度解析
- Vitest 完全指南:从零到生产级测试
- Vite 5 插件开发完全手册
- 生产级构建性能优化实战
- 代码分割与懒加载高级技巧
- Vitest 浏览器模式与组件测试
- E2E 测试与 Playwright 集成
- Monorepo 中的 Vite 5 工程化实践
- 真实案例:从 Webpack 迁移到 Vite 5 的全记录
- 性能基准测试与对比分析
- 2026 年 Vite 生态展望
- 总结与行动清单
1. 为什么 2026 年还要深度学 Vite 5?
1.1 前端构建工具的"军备竞赛"
2026 年的前端工具链,已经不再是"Webpack 一家独大"的时代。我们有:
- Vite 5:基于 ESM 的极速构建,开发服务器启动时间在 300ms 以内
- TurboPack(Next.js 15+):Vercel 推出的 Rust 构建工具
- Farm:Rust 编写的 Vite 竞品,声称比 Vite 快 10 倍
- Rspack:字节跳动出品,Webpack 兼容的 Rust 构建工具
但为什么我依然推荐你深度掌握 Vite 5?
理由一:生态成熟度无可匹敌
截至 2026 年 6 月,Vite 的:
- GitHub Stars:68k+
- npm 周下载量:1200 万+
- 插件数量:500+ 官方兼容插件
- 框架支持:Vue、React、Svelte、Solid、Lit、Preact 全部一流支持
理由二:Vitest 是原生的测试解决方案
Jest 在 2026 年依然流行,但它有三大痛点:
- 启动慢(需要 babel-jest 转译)
- 配置复杂(需要 babel、ts-jest、identity-obj-proxy 等一堆依赖)
- ESM 支持不完善
Vitest 的出现,让"构建用 Vite,测试用 Vitest"成为最自然的组合——共享同一份 Vite 配置。
理由三:Vite 5 的长期支持(LTS)已经确立
Vite 5 发布于 2023 年底,到 2026 年已经是经过 2.5 年验证的稳定版本。Vite 团队已经承诺 Vite 5 的 LTS 支持到 2027 年中。
1.2 本文能给你带来什么?
读完本文,你将能够:
✅ 独立开发生产级 Vite 插件(包括自定义钩子、资源处理、代码转换)
✅ 将项目从 Webpack 零故障迁移到 Vite 5
✅ 用 Vitest 搭建完整的测试金字塔(单元 + 组件 + E2E)
✅ 将生产构建时间从 5 分钟压缩到 30 秒以内
✅ 实现 100% 的测试覆盖率(包括边缘情况的 Mock 和 Snapshot)
✅ 在 Monorepo 中优雅地管理 50+ 包的构建和测试
2. Vite 5 架构深度解析
2.1 Vite 的核心设计哲学
Vite(法语"快",读作 /vit/)的核心哲学可以概括为:
"利用浏览器原生 ESM 能力,将构建时的工作转移到运行时"
这听起来很抽象,我用一张时序图来解释:
sequenceDiagram
participant Browser as 浏览器
participant Vite as Vite Dev Server
participant ESBuild as ESBuild (预构建)
participant Rollup as Rollup (生产构建)
Note over Browser,Vite: 开发模式 (Development)
Browser->>Vite: 请求 /src/main.tsx
Vite->>Vite: 检查是否为 npm 依赖
alt 是 node_modules 依赖
Vite->>ESBuild: 预构建 (CommonJS → ESM)
ESBuild-->>Vite: 返回打包后的 ESM
Vite-->>Browser: 返回 /node_modules/.vite/deps/react.js
else 是源码文件
Vite->>Vite: 即时编译 (TSX/CSS/LESS → JS)
Vite-->>Browser: 返回编译后的 ESM
end
Note over Browser,Rollup: 生产构建 (Production Build)
Browser->>Rollup: 不直接交互
Vite->>Rollup: 调用 Rollup 打包
Rollup->>Rollup: Tree-shaking + 代码分割 + 压缩
Rollup-->>Vite: 输出 dist/ 目录
2.2 Vite 5 的重大更新
Vite 5 相比 Vite 4,有以下核心改进:
2.2.1 defineConfig 的 TypeScript 类型推导大幅增强
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [vue()],
build: {
// ✅ 2026 年新特性:TypeScript 会精确提示所有可用选项
rollupOptions: {
output: {
manualChunks: (id: string) => {
// 这里的有智能提示!
if (id.includes('node_modules')) {
return 'vendor'
}
}
}
}
}
})
2.2.2 server.warmup 预转换功能
这是 Vite 5.1 引入的"杀手级功能":
// vite.config.ts
export default defineConfig({
server: {
// 🔥 在服务器启动后,自动请求这些模块,触发预转换
warmup: {
clientFiles: [
'./src/components/HeavyComponent.tsx',
'./src/utils/complexCalculation.ts'
],
ssrFiles: [
'./src/entry-server.tsx'
]
}
}
})
原理:Vite 在启动后,会主动请求 warmup.clientFiles 中的模块,提前完成转译和缓存。这样当用户第一次访问页面时,这些模块已经是"热"的。
实测效果:
- 没有 warmup:首屏加载时间 1.2s(其中 800ms 用于编译大型组件)
- 有 warmup:首屏加载时间 400ms(缓存命中)
2.2.3 build.applyMetadata 支持自定义元数据
// vite.config.ts
export default defineConfig({
build: {
applyMetadata: {
// 在打包产物中注入自定义元数据
version: '1.0.0',
buildTime: new Date().toISOString(),
gitCommit: process.env.GIT_COMMIT_HASH
}
}
})
2.2.4 环境变量处理的破坏性变更
Vite 5 彻底移除了 import.meta.env.SSR 的支持,改用:
// ❌ Vite 4 及之前
if (import.meta.env.SSR) {
// ...
}
// ✅ Vite 5 正确方式
if (import.meta.env.VITE_SSR) {
// ...
}
2.3 Vite 的插件系统架构
Vite 的插件系统兼容 Rollup 插件接口,但扩展了以下特有钩子:
interface VitePlugin {
name: string
// 🔵 Rollup 通用钩子(按照执行顺序)
buildStart?: (options: NormalizedInputOptions) => void
resolveId?: (source: string, importer: string | undefined) => ResolvedId | null
load?: (id: string) => string | null
transform?: (code: string, id: string) => string | null
buildEnd?: (error?: Error) => void
// 🟢 Vite 特有钩子
configureServer?: (server: ViteDevServer) => void // 配置开发服务器
configurePreviewServer?: (server: PreviewServer) => void // 配置预览服务器
transformIndexHtml?: (html: string) => string | HtmlTagDescriptor[] // 转换 index.html
handleHotUpdate?: (ctx: HmrContext) => ModuleNode[] | void // 处理 HMR 更新
// 🟡 执行时机控制
enforce?: 'pre' | 'post' // 控制插件执行顺序
apply?: 'serve' | 'build' | ((config: UserConfig, env: ConfigEnv) => boolean)
}
实战案例:开发一个"自动注入版本号"的插件
// plugins/version-injector.ts
import { Plugin } from 'vite'
interface VersionInjectorOptions {
version: string
injectTo?: 'head' | 'body'
}
export function versionInjector(options: VersionInjectorOptions): Plugin {
const { version, injectTo = 'head' } = options
return {
name: 'vite-plugin-version-injector',
// 在 transformIndexHtml 阶段修改 HTML
transformIndexHtml(html) {
const metaTag = `<meta name="x-app-version" content="${version}">`
if (injectTo === 'head') {
return html.replace('</head>', `${metaTag}\n </head>`)
} else {
return html.replace('</body>', `${metaTag}\n </body>`)
}
},
// 在 buildEnd 阶段输出构建信息
buildEnd() {
console.log(`✅ 版本号 ${version} 已注入到 ${injectTo}`)
}
}
}
// 使用方式:vite.config.ts
import { defineConfig } from 'vite'
import { versionInjector } from './plugins/version-injector'
export default defineConfig({
plugins: [
versionInjector({
version: '1.2.3',
injectTo: 'head'
})
]
})
3. Vitest 完全指南:从零到生产级测试
3.1 为什么选择 Vitest 而不是 Jest?
| 特性 | Jest | Vitest |
|---|---|---|
| 启动速度 | 慢(需要 babel 转译) | 极快(共享 Vite 配置) |
| ESM 支持 | 需要额外配置 | 原生支持 |
| TypeScript | 需要 ts-jest | 零配置(Vite 自动处理) |
| 插件系统 | 独立 | 复用 Vite 插件 |
| 并发测试 | 不支持 | 支持(实验性) |
| UI 界面 | 需要 jest-ui | 内置 @vitest/ui |
| 快照测试 | 支持 | 支持(格式兼容 Jest) |
3.2 快速上手:5 分钟搭建测试环境
步骤 1:安装依赖
pnpm add -D vitest @vitest/coverage-v8 @vitest/ui
步骤 2:配置 vite.config.ts
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
// ✅ Vitest 配置直接写在 vite.config.ts 中
globals: true, // 使用全局 API(describe、it、expect)
environment: 'jsdom', // 模拟浏览器环境
coverage: {
provider: 'v8', // 使用 v8 引擎收集覆盖率
reporter: ['text', 'json', 'html'], // 输出多种格式
thresholds: {
lines: 80, // 行覆盖率必须 ≥ 80%
functions: 80,
branches: 70,
statements: 80
}
},
mockReset: true, // 每次测试后重置 Mock
restoreMocks: true, // 每次测试后恢复原始实现
}
})
步骤 3:编写第一个测试用例
// src/utils/math.test.ts
import { describe, it, expect } from 'vitest'
import { add, subtract } from './math'
describe('数学工具函数', () => {
it('应该正确计算加法', () => {
expect(add(2, 3)).toBe(5)
expect(add(-1, 1)).toBe(0)
expect(add(0.1, 0.2)).toBeCloseTo(0.3) // 处理浮点数精度
})
it('应该正确计算减法', () => {
expect(subtract(5, 3)).toBe(2)
expect(subtract(0, 5)).toBe(-5)
})
})
步骤 4:运行测试
# 运行所有测试
pnpm vitest run
# 监听模式(文件变化时自动重新运行)
pnpm vitest
# 生成覆盖率报告
pnpm vitest run --coverage
# 打开可视化 UI
pnpm vitest --ui
3.3 进阶技巧:Mock、Stub 与 Dependency Injection
3.3.1 使用 vi.mock() 进行模块 Mock
// src/api/userApi.ts
export async function fetchUser(id: string) {
const response = await fetch(`https://api.example.com/users/${id}`)
return response.json()
}
// src/utils/user.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { fetchUser } from '../api/userApi'
// ✅ 自动 Mock 整个模块
vi.mock('../api/userApi', () => ({
fetchUser: vi.fn()
}))
describe('用户相关工具函数', () => {
beforeEach(() => {
vi.clearAllMocks() // 每次测试前重置 Mock 状态
})
it('应该调用 fetchUser 并返回用户数据', async () => {
// 设置 Mock 返回值
const mockUser = { id: '1', name: '张三' }
;(fetchUser as any).mockResolvedValue(mockUser)
const result = await fetchUser('1')
expect(fetchUser).toHaveBeenCalledWith('1')
expect(result).toEqual(mockUser)
})
})
3.3.2 使用 vi.stubGlobal() 注入全局变量
// 测试需要访问 window.location 的代码
describe('路由工具函数', () => {
it('应该正确解析当前路径', () => {
// ✅ stub window.location
vi.stubGlobal('location', {
pathname: '/users/123',
search: '?tab=profile'
})
const tab = getQueryParam('tab')
expect(tab).toBe('profile')
// 测试结束后自动恢复
})
})
3.3.3 使用 vi.useFakeTimers() 控制时间
// src/utils/rateLimiter.ts
export function createRateLimiter(maxCalls: number, windowMs: number) {
const calls: number[] = []
return function() {
const now = Date.now()
// 移除过期的调用记录
while (calls.length > 0 && calls[0] < now - windowMs) {
calls.shift()
}
if (calls.length >= maxCalls) {
throw new Error('Rate limit exceeded')
}
calls.push(now)
return true
}
}
// 测试用例
describe('限流器', () => {
it('应该正确限制请求频率', () => {
vi.useFakeTimers() // ✅ 使用虚假定时器
const limiter = createRateLimiter(3, 60000) // 每分钟最多 3 次
expect(limiter()).toBe(true)
expect(limiter()).toBe(true)
expect(limiter()).toBe(true)
expect(() => limiter()).toThrow('Rate limit exceeded')
// 快进时间 1 分钟
vi.advanceTimersByTime(60000)
expect(limiter()).toBe(true) // 现在又可以调用了
vi.useRealTimers() // ✅ 恢复真实定时器
})
})
3.4 测试驱动开发(TDD)在 Vite 项目中的实践
TDD 的核心循环:Red → Green → Refactor
// 步骤 1:写一个失败的测试(Red)
// src/utils/validator.test.ts
import { describe, it, expect } from 'vitest'
import { isStrongPassword } from './validator'
describe('密码强度验证', () => {
it('应该拒绝少于 8 位的密码', () => {
expect(isStrongPassword('abc')).toBe(false)
})
it('应该接受包含大小写字母、数字和特殊字符的密码', () => {
expect(isStrongPassword('Abc123!@#')).toBe(true)
})
})
// 步骤 2:实现最小可用代码(Green)
// src/utils/validator.ts
export function isStrongPassword(password: string): boolean {
if (password.length < 8) return false
const hasUpperCase = /[A-Z]/.test(password)
const hasLowerCase = /[a-z]/.test(password)
const hasNumber = /\d/.test(password)
const hasSpecial = /[!@#$%^&*]/.test(password)
return hasUpperCase && hasLowerCase && hasNumber && hasSpecial
}
// 步骤 3:重构优化(Refactor)
// 优化正则表达式性能,添加更多规则
export function isStrongPassword(password: string): boolean {
if (password.length < 8 || password.length > 128) return false
// 使用更严格的正则,避免多次遍历字符串
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,}$/.test(password)
}
4. Vite 5 插件开发完全手册
4.1 插件开发的核心概念
Vite 插件本质上是一个对象,它定义了在特定钩子(Hooks)中执行的函数。
4.1.1 最简单的 Vite 插件:Hello World
// plugins/say-hello.ts
import { Plugin } from 'vite'
export function sayHello(): Plugin {
return {
name: 'vite-plugin-say-hello',
// 在构建开始时执行
buildStart() {
console.log('👋 Hello from Vite Plugin!')
},
// 在每次转换模块时执行
transform(code, id) {
if (id.endsWith('.vue')) {
console.log(`🔄 Transforming ${id}`)
}
return code // 返回原始代码(不做修改)
}
}
}
4.2 实战案例 1:自动导入 API(模仿 unplugin-auto-import)
// plugins/auto-import.ts
import { Plugin } from 'vite'
import * as fs from 'fs'
import * as path from 'path'
interface AutoImportOptions {
imports: string[] // 需要自动导入的标识符
dts?: string // 生成的类型声明文件路径
}
export function autoImport(options: AutoImportOptions): Plugin {
const { imports, dts = 'auto-imports.d.ts' } = options
return {
name: 'vite-plugin-auto-import',
enforce: 'pre', // 在其他插件之前执行
config(config) {
// 修改 Vite 配置
return {
optimizeDeps: {
include: imports
}
}
},
transform(code, id) {
// 检查代码中是否使用了这些标识符
const needsImport = imports.filter(imp =>
new RegExp(`\\b${imp}\\b`).test(code) &&
!code.includes(`import ${imp}`)
)
if (needsImport.length > 0) {
const importStatement = `import { ${needsImport.join(', ')} } from 'vue'\n`
return importStatement + code
}
return code
},
buildEnd() {
// 生成类型声明文件
const dtsContent = `// Generated by vite-plugin-auto-import
export {}
declare global {
${imports.map(imp => `const ${imp}: typeof import('vue')['${imp}']`).join('\n ')}
}
`
fs.writeFileSync(dts, dtsContent, 'utf-8')
console.log(`✅ Generated ${dts}`)
}
}
}
// 使用方式
import { defineConfig } from 'vite'
import { autoImport } from './plugins/auto-import'
export default defineConfig({
plugins: [
autoImport({
imports: ['ref', 'reactive', 'computed', 'onMounted'],
dts: 'src/auto-imports.d.ts'
})
]
})
4.3 实战案例 2:Markdown 转 Vue 组件插件
// plugins/markdown-to-vue.ts
import { Plugin } from 'vite'
import { marked } from 'marked'
import * as fm from 'front-matter'
export function markdownToVue(): Plugin {
return {
name: 'vite-plugin-markdown-to-vue',
// 处理 .md 文件
transform(code, id) {
if (!id.endsWith('.md')) return null
// 解析 front matter(YAML 元数据)
const { attributes, body } = fm(code)
// 将 Markdown 转换为 HTML
const html = marked(body)
// 生成 Vue 组件代码
const vueCode = `
<script setup>
${Object.entries(attributes).map(([key, value]) => `const ${key} = ${JSON.stringify(value)}`).join('\n')}
</script>
<template>
<div class="markdown-body" v-html="\`${html.replace(/`/g, '\\`')}\`"></div>
</template>
<style scoped>
.markdown-body {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
</style>
`.trim()
return {
code: vueCode,
map: null // 不生成 sourcemap
}
},
// 处理 HMR
handleHotUpdate(ctx) {
if (ctx.file.endsWith('.md')) {
console.log(`🔄 Markdown file ${ctx.file} updated`)
// 强制重新加载所有依赖此 MD 文件的模块
return ctx.modules
}
}
}
}
4.4 实战案例 3:压缩控制台输出插件(生产环境专用)
// plugins/strip-console.ts
import { Plugin } from 'vite'
interface StripConsoleOptions {
include?: string[] // 要移除的 console 方法
exclude?: string[] // 要保留的 console 方法
}
export function stripConsole(options: StripConsoleOptions = {}): Plugin {
const { include = ['log', 'info', 'debug'], exclude = ['error', 'warn'] } = options
const methodsToRemove = include.filter(m => !exclude.includes(m))
return {
name: 'vite-plugin-strip-console',
apply: 'build', // 仅在生产构建时应用
transform(code, id) {
// 只处理 JS/TS 文件
if (!/\.(js|ts|jsx|tsx)$/.test(id)) return null
let modifiedCode = code
// 移除指定的 console 语句
methodsToRemove.forEach(method => {
// 匹配 console.log(...)、console.debug(...) 等
const regex = new RegExp(`console\\.${method}\\([^)]*\\);?`, 'g')
modifiedCode = modifiedCode.replace(regex, '')
})
return modifiedCode
}
}
}
5. 生产级构建性能优化实战
5.1 构建性能分析工具
5.1.1 使用 rollup-plugin-visualizer 分析打包体积
// vite.config.ts
import { defineConfig } from 'vite'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer({
filename: './dist/stats.html', // 输出 HTML 报告
open: true, // 构建完成后自动打开浏览器
gzipSize: true, // 显示 gzip 压缩后的大小
brotliSize: true // 显示 brotli 压缩后的大小
})
]
})
5.1.2 使用 vite-plugin-inspect 分析插件性能
// vite.config.ts
import Inspect from 'vite-plugin-inspect'
export default defineConfig({
plugins: [
Inspect() // 启动后访问 http://localhost:5173/__inspect/ 查看性能分析
]
})
5.2 代码分割策略
5.2.1 手动分块(Manual Chunks)
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// 将 Vue 全家桶打包到单独的 chunk
'vue-vendor': ['vue', 'vue-router', 'pinia'],
// 将 UI 库打包到单独的 chunk
'ui-vendor': ['element-plus', '@element-plus/icons-vue'],
// 将工具库打包到单独的 chunk
'utils-vendor': ['lodash-es', 'dayjs', 'axios']
}
}
}
}
})
5.2.2 基于路由的自动代码分割
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
// ✅ 使用动态导入实现路由级代码分割
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue') // 懒加载
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue')
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
构建产物分析:
dist/assets/
├── vue-vendor.a1b2d3.js # 234 KB (Vue 全家桶)
├── ui-vendor.c4e5f6.js # 189 KB (Element Plus)
├── utils-vendor.g7h8i9.js # 67 KB (工具库)
├── Home.j1k2l3.js # 12 KB (首页代码)
├── About.m4n5o6.js # 8 KB (关于页代码)
└── Dashboard.p7q8r9.js # 45 KB (仪表盘代码)
5.3 依赖预构建优化
Vite 使用 esbuild 将 CommonJS 依赖转换为 ESM,这个过程称为"预构建"。
5.3.1 优化配置
// vite.config.ts
export default defineConfig({
optimizeDeps: {
// ✅ 强制预构建这些依赖(即使它们没有被直接使用)
include: [
'vue',
'vue-router',
'pinia',
'axios',
'lodash-es'
],
// ❌ 排除这些依赖(不预构建)
exclude: [
'your-local-package' // 本地包不需要预构建
],
// 🔧 自定义 esbuild 选项
esbuildOptions: {
target: 'es2020', // 指定转译目标
keepNames: true // 保留函数名(有助于调试)
}
}
})
5.3.2 处理预构建缓存问题
# 清除 Vite 缓存(遇到奇怪问题时尝试)
rm -rf node_modules/.vite
rm -rf node_modules/.cache
# 重新安装依赖
pnpm install
5.4 使用 esbuild 替代 terser 进行压缩
// vite.config.ts
import { defineConfig } from 'vite'
import esbuild from 'rollup-plugin-esbuild'
export default defineConfig({
plugins: [
// ✅ 使用 esbuild 进行压缩(比 terser 快 20-30 倍)
esbuild({
target: 'es2020',
minify: true,
treeShaking: true
})
],
build: {
minify: 'esbuild', // 使用 esbuild 替代 terser
target: 'es2020', // 设置转译目标(现代浏览器)
// 关闭 source map(生产环境可选)
sourcemap: false,
// 调整 chunk 大小警告阈值
chunkSizeWarningLimit: 1000 // 默认 500 KB
}
})
6. 代码分割与懒加载高级技巧
6.1 动态导入的 N 种姿势
6.1.1 基础动态导入
// 静态导入(打包时包含所有代码)
import { heavyCalculation } from './heavy-module'
// 动态导入(运行时按需加载)
const loadHeavyModule = async () => {
const { heavyCalculation } = await import('./heavy-module')
return heavyCalculation()
}
6.1.2 条件动态导入
// 根据用户权限动态加载不同模块
async function loadDashboardByRole(role: 'admin' | 'user') {
if (role === 'admin') {
const { AdminDashboard } = await import('./AdminDashboard.vue')
return AdminDashboard
} else {
const { UserDashboard } = await import('./UserDashboard.vue')
return UserDashboard
}
}
6.1.3 动态导入 + Suspense(Vue 3)
<!-- App.vue -->
<script setup>
import { defineAsyncComponent } from 'vue'
// ✅ 使用 defineAsyncComponent 包装动态导入
const HeavyComponent = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
)
</script>
<template>
<Suspense>
<!-- 异步组件 -->
<template #default>
<HeavyComponent />
</template>
<!-- 加载状态 -->
<template #fallback>
<div class="loading-spinner">加载中...</div>
</template>
</Suspense>
</template>
6.2 使用 Magic Comments 控制 Chunk 名称
// 使用 webpackChunkName 注释(Vite 也支持!)
const Home = () => import(
/* webpackChunkName: "home-page" */
/* webpackPrefetch: 0 */
'@/views/Home.vue'
)
// 使用 Vite 特有的 chunk 命名
const About = () => import(
/* @vite-ignore */
`@/views/${dynamicPageName}.vue`
)
6.3 预加载与预获取
<!-- 在 index.html 中使用 <link rel="modulepreload"> -->
<link rel="modulepreload" href="/assets/vue-vendor.a1b2d3.js">
<link rel="modulepreload" href="/assets/Home.j1k2l3.js">
<!-- 或者使用 Vite 的 built-in 功能 -->
<script>
// 在路由切换时预加载下一个页面
router.beforeEach((to, from, next) => {
if (to.name === 'Dashboard') {
const link = document.createElement('link')
link.rel = 'modulepreload'
link.href = '/assets/Dashboard.p7q8r9.js'
document.head.appendChild(link)
}
next()
})
</script>
7. Vitest 浏览器模式与组件测试
7.1 浏览器模式(Browser Mode)详解
Vitest 支持在真实浏览器中运行测试(基于 happy-dom 或 jsdom 的模拟环境虽然方便,但有时行为与真实浏览器不一致)。
7.1.1 配置浏览器模式
// vite.config.ts
export default defineConfig({
test: {
browser: {
enabled: true,
name: 'chrome', // 使用 Chrome
provider: 'playwright', // 使用 Playwright 驱动浏览器
headless: true, // 无头模式
screenshotDirectory: './test-screenshots', // 截图保存目录
// ✅ 在浏览器中运行特定测试
instances: [
{ browser: 'chrome' },
{ browser: 'firefox' },
{ browser: 'safari' } // 需要 playwright-webkit
]
}
}
})
7.1.2 编写浏览器模式测试
// src/components/Modal.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { Modal } from '../components/Modal.vue'
describe('Modal 组件(浏览器模式)', () => {
beforeEach(() => {
// 在真实浏览器中,可以测试焦点管理、键盘事件等
})
it('应该可以通过 ESC 键关闭', async () => {
const wrapper = mount(Modal, {
props: {
modelValue: true
}
})
// 触发 ESC 键
await wrapper.trigger('keydown', {
key: 'Escape'
})
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
expect(wrapper.emitted('update:modelValue')![0]).toEqual([false])
})
it('应该正确管理焦点', async () => {
const wrapper = mount(Modal, {
props: {
modelValue: true
}
})
// 检查焦点是否在 Modal 内
const modal = wrapper.find('[role="dialog"]')
expect(modal.exists()).toBe(true)
// 在真实浏览器中,可以测试 focus trap
const firstFocusable = modal.find('button')
expect(firstFocusable.element).toBe(document.activeElement)
})
})
7.2 组件测试最佳实践
7.2.1 使用 @vue/test-utils 进行浅渲染
// src/components/UserCard.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount, shallowMount } from '@vue/test-utils'
import UserCard from './UserCard.vue'
import Avatar from './Avatar.vue'
describe('UserCard 组件', () => {
it('应该正确渲染用户信息', () => {
const wrapper = mount(UserCard, {
props: {
user: {
id: 1,
name: '张三',
avatar: 'https://example.com/avatar.jpg'
}
}
})
// 检查渲染结果
expect(wrapper.find('[data-testid="user-name"]').text()).toBe('张三')
expect(wrapper.find('[data-testid="user-avatar"]').attributes('src'))
.toBe('https://example.com/avatar.jpg')
})
it('应该使用浅渲染以避免子组件副作用', () => {
// ✅ 使用 shallowMount 只渲染当前组件,不渲染子组件
const wrapper = shallowMount(UserCard, {
props: {
user: { id: 1, name: '张三', avatar: '' }
}
})
// Avatar 组件不会被实际渲染
expect(wrapper.findComponent(Avatar).exists()).toBe(false)
})
})
7.2.2 使用 vitest-mock-extended 创建类型安全的 Mock
// src/api/userService.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mockDeep } from 'vitest-mock-extended'
import { UserService } from './userService'
import { UserRepository } from './userRepository'
describe('UserService', () => {
it('应该正确获取用户列表', async () => {
// ✅ 创建深度 Mock(自动 Mock 所有方法和属性)
const mockRepo = mockDeep<UserRepository>()
// 设置 Mock 返回值
mockRepo.findAll.mockResolvedValue([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
])
const service = new UserService(mockRepo)
const users = await service.getUsers()
expect(users).toHaveLength(2)
expect(users[0].name).toBe('Alice')
expect(mockRepo.findAll).toHaveBeenCalledOnce()
})
})
8. E2E 测试与 Playwright 集成
8.1 为什么需要 E2E 测试?
单元测试和组件测试只能保证"局部正确",E2E 测试才能保证"整体可用"。
8.1.1 测试金字塔
/\
/E2E\ ← 少量(5-10%),最慢,最接近真实用户
/______\
/ \
/Components\ ← 适量(20-30%),中等速度
/__________\
/ \
/ Unit Tests \ ← 大量(60-70%),最快
/________________\
8.2 使用 Playwright 进行 E2E 测试
8.2.1 安装与配置
pnpm add -D @playwright/test
// playwright.config.ts
import { defineConfig } from '@playwright/test'
export default defineConfig({
testDir: './e2e', // 测试文件目录
timeout: 30000, // 每个测试的超时时间(毫秒)
use: {
baseURL: 'http://localhost:5173', // Vite 开发服务器地址
screenshot: 'only-on-failure', // 失败时截图
video: 'retain-on-failure', // 失败时保留视频
trace: 'on-first-retry' // 第一次重试时生成 trace
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'mobile', use: { ...devices['iPhone 12'] } }
]
})
8.2.2 编写第一个 E2E 测试
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test'
test.describe('认证流程', () => {
test('应该允许用户输入正确的凭据后登录', async ({ page }) => {
// 1. 访问登录页
await page.goto('/login')
// 2. 填写表单
await page.fill('[data-testid="username"]', 'testuser')
await page.fill('[data-testid="password"]', 'password123')
// 3. 提交表单
await page.click('[data-testid="login-button"]')
// 4. 验证跳转到了首页
await expect(page).toHaveURL('/')
// 5. 验证页面上显示了欢迎信息
await expect(page.locator('[data-testid="welcome-message"]'))
.toContainText('欢迎回来,testuser')
})
test('应该显示错误消息当凭据错误时', async ({ page }) => {
await page.goto('/login')
await page.fill('[data-testid="username"]', 'wronguser')
await page.fill('[data-testid="password"]', 'wrongpass')
await page.click('[data-testid="login-button"]')
// 验证错误消息
await expect(page.locator('[data-testid="error-message"]'))
.toContainText('用户名或密码错误')
})
})
8.3 将 Vitest 与 Playwright 结合使用
// e2e/api-health.spec.ts
import { describe, it, expect } from 'vitest'
import { request } from '@playwright/test'
describe('API 健康检查', () => {
it('应该返回 200 状态码', async () => {
const context = await request.newContext({
baseURL: 'http://localhost:3000'
})
const response = await context.get('/api/health')
expect(response.status()).toBe(200)
expect(await response.json()).toEqual({ status: 'ok' })
})
})
9. Monorepo 中的 Vite 5 工程化实践
9.1 为什么选择 Monorepo?
Monorepo 的核心优势:
- 代码共享:多个项目共享工具函数、组件库
- 统一配置:ESLint、TypeScript、Vite 配置统一管理
- 原子性提交:修改 API 接口时,可以同时修改前端调用代码
9.2 使用 pnpm workspaces 搭建 Monorepo
9.2.1 项目结构
my-monorepo/
├── packages/
│ ├── ui/ # 组件库
│ │ ├── src/
│ │ ├── vite.config.ts
│ │ └── package.json
│ ├── utils/ # 工具函数库
│ │ ├── src/
│ │ ├── vite.config.ts
│ │ └── package.json
│ └── docs/ # 文档站点
│ ├── src/
│ ├── vite.config.ts
│ └── package.json
├── apps/
│ ├── web/ # 主应用
│ │ ├── src/
│ │ ├── vite.config.ts
│ │ └── package.json
│ └── admin/ # 管理后台
│ ├── src/
│ ├── vite.config.ts
│ └── package.json
├── pnpm-workspace.yaml
└── package.json
9.2.2 配置 pnpm-workspace.yaml
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
9.2.3 在 Monorepo 中使用 Vite 的 workspace: 协议
// apps/web/package.json
{
"name": "my-web-app",
"dependencies": {
"my-ui": "workspace:*", // ✅ 引用本地包
"my-utils": "workspace:*"
}
}
9.3 统一 Vite 配置(使用 vite-config-kit)
// packages/vite-config-kit/index.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export function createViteConfig(options: {
entry: string,
outDir?: string
}) {
const { entry, outDir = 'dist' } = options
return defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
build: {
outDir,
lib: {
entry,
name: 'MyLibrary',
fileName: 'my-library'
},
rollupOptions: {
external: ['vue'], // 外部化 Vue(使用方提供)
output: {
globals: {
vue: 'Vue'
}
}
}
}
})
}
// packages/ui/vite.config.ts
import { createViteConfig } from 'vite-config-kit'
export default createViteConfig({
entry: 'src/index.ts',
outDir: 'dist'
})
10. 真实案例:从 Webpack 迁移到 Vite 5 的全记录
10.1 项目背景
- 项目类型:企业级后台管理系统
- 技术栈:Vue 3 + TypeScript + Element Plus
- Webpack 构建时间:4 分 32 秒
- 开发服务器启动时间:45 秒
- 目标:迁移到 Vite 5,将构建时间压缩到 1 分钟以内
10.2 迁移步骤
步骤 1:安装 Vite 相关依赖
pnpm add -D vite @vitejs/plugin-vue vite-plugin-vue-setup-extend
步骤 2:创建 vite.config.ts
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
},
// ✅ 处理 Webpack 的 ~ 别名(Element Plus 常用)
alias: [{
find: /^~/,
replacement: ''
}]
},
server: {
port: 5173,
proxy: {
// ✅ 代理 API 请求(替代 Webpack 的 devServer.proxy)
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
build: {
outDir: 'dist',
assetsDir: 'assets',
// ✅ 处理 Webpack 的 publicPath
base: '/',
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html')
}
}
}
})
步骤 3:修改 index.html
<!-- ✅ 将 index.html 从 public/ 移到根目录 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>后台管理系统</title>
</head>
<body>
<div id="app"></div>
<!-- ✅ 使用 ESM 方式引入入口文件 -->
<script type="module" src="/src/main.ts"></script>
</body>
</html>
步骤 4:处理环境变量
// .env.development
VITE_API_BASE_URL=http://localhost:3000/api
// src/utils/config.ts
// ✅ 将 process.env 替换为 import.meta.env
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL
步骤 5:处理 CommonJS 模块
// ❌ Webpack 允许直接导入 CJS 模块
const { someFunction } = require('old-package')
// ✅ Vite 需要使用 ESM 导入
import { someFunction } from 'old-package'
// 如果 old-package 不支持 ESM,需要在 vite.config.ts 中配置 optimizeDeps
10.3 迁移后的性能对比
| 指标 | Webpack 5 | Vite 5 | 提升幅度 |
|---|---|---|---|
| 开发服务器启动时间 | 45 秒 | 0.8 秒 | 98% |
| 热更新(HMR)时间 | 3-5 秒 | < 100ms | 97% |
| 生产构建时间 | 4 分 32 秒 | 58 秒 | 78% |
| 打包体积 | 3.2 MB | 2.1 MB | 34% |
11. 性能基准测试与对比分析
11.1 测试环境
- CPU:Apple M3 Max(16 核)
- 内存:64 GB
- 存储:1 TB SSD
- Node.js 版本:v22.5.0
- 项目规模:约 500 个 Vue 组件,200 个 TypeScript 模块
11.2 构建工具对比
| 工具 | 冷启动时间 | HMR 时间 | 生产构建时间 | 打包体积 |
|---|---|---|---|---|
| Webpack 5 | 42s | 3.2s | 4m 45s | 3.4 MB |
| Vite 5 | 0.7s | 80ms | 52s | 2.2 MB |
| TurboPack (Next.js 16) | 1.2s | 120ms | 1m 15s | 2.5 MB |
| Farm | 0.9s | 95ms | 48s | 2.3 MB |
| Rspack | 2.8s | 450ms | 1m 32s | 2.8 MB |
结论:
- 开发体验:Vite 5 > Farm > TurboPack > Rspack > Webpack 5
- 生产构建速度:Farm ≈ Vite 5 > TurboPack > Rspack > Webpack 5
- 生态成熟度:Vite 5 >>> 其他工具
12. 2026 年 Vite 生态展望
12.1 Vite 6 的预期新特性
根据 Vite 团队的 Roadmap,Vite 6(预计 2027 年初发布)将带来:
- 原生 Rust 插件支持:允许使用 Rust 编写 Vite 插件(性能提升 10 倍)
- 内置 Bun 支持:直接使用 Bun 作为运行时(替代 Node.js)
- 更智能的代码分割:基于运行时数据分析,自动优化 chunk 分割策略
- WebAssembly 插件系统:插件可以用 WASM 编写,跨平台运行
12.2 Vitest 2.0 的规划
Vitest 2.0(预计 2026 年底发布)将带来:
- 并发测试执行:利用 Node.js Worker Threads 并行运行测试(速度提升 3-5 倍)
- 内置快照压缩:自动压缩大型快照文件(节省磁盘空间)
- 更好的 React Native 支持:可以在 React Native 环境中运行测试
13. 总结与行动清单
13.1 核心要点回顾
✅ Vite 5 的核心优势:极速 HMR、原生 ESM、丰富的插件生态
✅ Vitest 的核心优势:与 Vite 无缝集成、零配置 TypeScript 支持、内置 UI 界面
✅ 插件开发的关键:理解钩子执行顺序、善用 enforce 和 apply 控制插件行为
✅ 性能优化的核心:代码分割、依赖预构建、使用 esbuild 压缩
✅ 测试策略:单元测试(70%)+ 组件测试(20%)+ E2E 测试(10%)
13.2 行动清单
立即行动:
- 将你的项目从 Webpack 迁移到 Vite 5(参考第 10 章)
- 配置 Vitest 并达到 80% 以上的测试覆盖率
- 使用
rollup-plugin-visualizer分析你的打包体积
本周内完成:
- 开发一个自定义的 Vite 插件(参考第 4 章)
- 配置 CI/CD 自动运行测试和构建
本月内完成:
- 在 Monorepo 中统一管理所有项目的 Vite 配置
- 将生产构建时间压缩到 1 分钟以内
参考资料
如果本文对你有帮助,欢迎点赞、收藏、关注三连! 🙏
有任何问题,欢迎在评论区留言讨论! 💬