- 发布于
Vite 构建工具指南:下一代前端构建工具的完整实践
- 作者

- 姓名
- 全能波
- GitHub
- @weicracker
Vite 构建工具指南:下一代前端构建工具的完整实践
Vite 是一个现代化的前端构建工具,以其极快的冷启动速度和高效的热模块替换而闻名。本文将深入探讨 Vite 的核心特性、配置方法和最佳实践。
Vite 核心特性
快速冷启动
// vite.config.js - 基础配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'
export default defineConfig({
// 插件配置
plugins: [vue(), react()],
// 开发服务器配置
server: {
port: 3000,
open: true,
cors: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
// 构建配置
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: true,
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
admin: resolve(__dirname, 'admin.html'),
},
output: {
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: '[ext]/[name]-[hash].[ext]',
},
},
},
// 路径别名
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@utils': resolve(__dirname, 'src/utils'),
'@assets': resolve(__dirname, 'src/assets'),
},
},
// CSS 配置
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`,
},
less: {
modifyVars: {
'primary-color': '#1890ff',
'link-color': '#1890ff',
},
javascriptEnabled: true,
},
},
modules: {
localsConvention: 'camelCase',
},
},
// 环境变量
define: {
__APP_VERSION__: JSON.stringify(process.env.npm_package_version),
__BUILD_TIME__: JSON.stringify(new Date().toISOString()),
},
})
热模块替换 (HMR)
// HMR API 使用示例
// main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
// HMR 支持
if (import.meta.hot) {
import.meta.hot.accept('./App.vue', (newModule) => {
// 热更新处理
console.log('App.vue 已更新')
})
// 自定义 HMR 事件
import.meta.hot.on('custom-event', (data) => {
console.log('收到自定义事件:', data)
})
// 模块失效时的清理
import.meta.hot.dispose(() => {
console.log('模块即将被替换')
// 执行清理操作
})
}
// React HMR 示例
// App.jsx
import React, { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
return (
<div>
<h1>计数器: {count}</h1>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
)
}
export default App
// HMR 边界
if (import.meta.hot) {
import.meta.hot.accept()
}
插件系统
官方插件
// vite.config.js - 官方插件配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import react from '@vitejs/plugin-react'
import legacy from '@vitejs/plugin-legacy'
export default defineConfig({
plugins: [
// Vue 支持
vue({
include: [/\.vue$/, /\.md$/],
reactivityTransform: true,
}),
// React 支持
react({
include: '**/*.{jsx,tsx}',
babel: {
plugins: [['@babel/plugin-proposal-decorators', { legacy: true }]],
},
}),
// 传统浏览器支持
legacy({
targets: ['defaults', 'not IE 11'],
}),
],
})
社区插件
// vite.config.js - 社区插件
import { defineConfig } from 'vite'
import { resolve } from 'path'
import eslint from 'vite-plugin-eslint'
import { createHtmlPlugin } from 'vite-plugin-html'
import { visualizer } from 'rollup-plugin-visualizer'
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd(), '')
return {
plugins: [
// ESLint 检查
eslint({
include: ['src/**/*.{js,jsx,ts,tsx,vue}'],
exclude: ['node_modules'],
}),
// HTML 模板处理
createHtmlPlugin({
minify: true,
inject: {
data: {
title: env.VITE_APP_TITLE || 'Vite App',
description: env.VITE_APP_DESCRIPTION || 'A Vite App',
},
},
}),
// 打包分析
visualizer({
filename: 'dist/stats.html',
open: true,
gzipSize: true,
}),
// PWA 支持
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
},
manifest: {
name: 'Vite PWA App',
short_name: 'ViteApp',
description: 'A Vite PWA Application',
theme_color: '#ffffff',
icons: [
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png',
},
],
},
}),
],
}
})
自定义插件
// plugins/custom-plugin.js
export function customPlugin(options = {}) {
return {
name: 'custom-plugin',
// 插件配置
configResolved(config) {
console.log('配置已解析:', config)
},
// 构建开始
buildStart(opts) {
console.log('构建开始')
},
// 解析模块
resolveId(id, importer) {
if (id === 'virtual:my-module') {
return id
}
},
// 加载模块
load(id) {
if (id === 'virtual:my-module') {
return 'export const msg = "Hello from virtual module"'
}
},
// 转换代码
transform(code, id) {
if (id.endsWith('.special')) {
return {
code: `export default ${JSON.stringify(code)}`,
map: null,
}
}
},
// 生成 bundle
generateBundle(options, bundle) {
// 添加额外的文件到 bundle
this.emitFile({
type: 'asset',
fileName: 'custom-file.txt',
source: 'Custom content',
})
},
// 开发服务器中间件
configureServer(server) {
server.middlewares.use('/api/custom', (req, res, next) => {
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ message: 'Custom API response' }))
})
},
}
}
// 使用自定义插件
// vite.config.js
import { customPlugin } from './plugins/custom-plugin.js'
export default defineConfig({
plugins: [
customPlugin({
option1: 'value1',
}),
],
})
环境变量和模式
环境变量配置
# .env - 所有环境
VITE_APP_TITLE=My Vite App
VITE_API_BASE_URL=https://api.example.com
# .env.local - 本地环境(被 git 忽略)
VITE_API_KEY=your-secret-api-key
# .env.development - 开发环境
VITE_APP_ENV=development
VITE_API_BASE_URL=http://localhost:3000/api
VITE_DEBUG=true
# .env.production - 生产环境
VITE_APP_ENV=production
VITE_API_BASE_URL=https://api.production.com
VITE_DEBUG=false
# .env.staging - 预发布环境
VITE_APP_ENV=staging
VITE_API_BASE_URL=https://api.staging.com
VITE_DEBUG=true
// 在代码中使用环境变量
// config.js
export const config = {
appTitle: import.meta.env.VITE_APP_TITLE,
apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
isDevelopment: import.meta.env.DEV,
isProduction: import.meta.env.PROD,
mode: import.meta.env.MODE
}
// 类型定义 (TypeScript)
// vite-env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
readonly VITE_API_BASE_URL: string
readonly VITE_API_KEY: string
readonly VITE_DEBUG: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
多环境配置
// vite.config.js - 多环境配置
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ command, mode }) => {
// 加载环境变量
const env = loadEnv(mode, process.cwd(), '')
const config = {
plugins: [],
define: {
__APP_VERSION__: JSON.stringify(process.env.npm_package_version),
},
}
// 开发环境配置
if (command === 'serve') {
config.server = {
port: 3000,
proxy: {
'/api': env.VITE_API_BASE_URL,
},
}
}
// 生产环境配置
if (command === 'build') {
config.build = {
minify: 'terser',
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router'],
utils: ['lodash', 'axios'],
},
},
},
}
}
// 特定模式配置
if (mode === 'staging') {
config.build.sourcemap = true
}
return config
})
性能优化
代码分割
// 动态导入实现代码分割
// router.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue'),
},
{
path: '/admin',
name: 'Admin',
component: () =>
import(
/* webpackChunkName: "admin" */
'@/views/Admin.vue'
),
},
]
// 预加载关键路由
const router = createRouter({
history: createWebHistory(),
routes,
})
router.beforeEach((to, from, next) => {
// 预加载下一个可能访问的路由
if (to.name === 'Home') {
import('@/views/About.vue')
}
next()
})
// 手动代码分割
// utils/lazy-load.js
export const lazyLoad = (importFunc) => {
return () => ({
component: importFunc(),
loading: () => import('@/components/Loading.vue'),
error: () => import('@/components/Error.vue'),
delay: 200,
timeout: 3000,
})
}
// 使用示例
const AsyncComponent = lazyLoad(() => import('@/components/HeavyComponent.vue'))
构建优化
// vite.config.js - 构建优化
export default defineConfig({
build: {
// 启用 CSS 代码分割
cssCodeSplit: true,
// 构建目标
target: 'es2015',
// 资源内联阈值
assetsInlineLimit: 4096,
// Rollup 选项
rollupOptions: {
output: {
// 手动分包
manualChunks: {
// 第三方库
vendor: ['vue', 'vue-router', 'vuex'],
ui: ['element-plus', '@element-plus/icons-vue'],
utils: ['lodash-es', 'dayjs', 'axios'],
// 按功能分包
admin: ['./src/views/admin/Dashboard.vue', './src/views/admin/Users.vue'],
},
// 自定义分包策略
manualChunks(id) {
// node_modules 中的包
if (id.includes('node_modules')) {
// 大型库单独分包
if (id.includes('echarts')) {
return 'echarts'
}
if (id.includes('monaco-editor')) {
return 'monaco'
}
// 其他第三方库
return 'vendor'
}
// 按目录分包
if (id.includes('/src/views/admin/')) {
return 'admin'
}
if (id.includes('/src/components/')) {
return 'components'
}
},
},
},
// 压缩选项
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log'],
},
},
},
// 依赖预构建优化
optimizeDeps: {
include: ['vue', 'vue-router', 'vuex', 'axios', 'lodash-es'],
exclude: ['your-local-package'],
},
})
缓存策略
// vite.config.js - 缓存配置
export default defineConfig({
build: {
rollupOptions: {
output: {
// 文件名包含 hash
entryFileNames: 'js/[name].[hash].js',
chunkFileNames: 'js/[name].[hash].js',
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.')
const ext = info[info.length - 1]
if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)$/.test(assetInfo.name)) {
return `media/[name].[hash].${ext}`
}
if (/\.(png|jpe?g|gif|svg)$/.test(assetInfo.name)) {
return `images/[name].[hash].${ext}`
}
if (/\.(woff2?|eot|ttf|otf)$/.test(assetInfo.name)) {
return `fonts/[name].[hash].${ext}`
}
return `assets/[name].[hash].${ext}`
},
},
},
},
// 开发服务器缓存
server: {
headers: {
'Cache-Control': 'no-cache',
},
},
})
多页面应用
MPA 配置
// vite.config.js - 多页面配置
import { defineConfig } from 'vite'
import { resolve } from 'path'
export default defineConfig({
build: {
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
admin: resolve(__dirname, 'admin/index.html'),
mobile: resolve(__dirname, 'mobile/index.html'),
},
},
},
// 为不同页面配置不同的代理
server: {
proxy: {
'^/api/admin/.*': {
target: 'http://localhost:8081',
changeOrigin: true,
},
'^/api/mobile/.*': {
target: 'http://localhost:8082',
changeOrigin: true,
},
'^/api/.*': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
})
<!-- index.html - 主页面 -->
<!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>
<script type="module" src="/src/main.js"></script>
</body>
</html>
<!-- admin/index.html - 管理后台 -->
<!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="admin-app"></div>
<script type="module" src="/src/admin/main.js"></script>
</body>
</html>
测试集成
单元测试配置
// vitest.config.js
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./tests/setup.js'],
include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
exclude: ['node_modules', 'dist', '.idea', '.git', '.cache'],
coverage: {
provider: 'c8',
reporter: ['text', 'json', 'html'],
exclude: [
'coverage/**',
'dist/**',
'packages/*/test{,s}/**',
'**/*.d.ts',
'cypress/**',
'test{,s}/**',
'test{,-*}.{js,cjs,mjs,ts,tsx,jsx}',
'**/*{.,-}test.{js,cjs,mjs,ts,tsx,jsx}',
'**/*{.,-}spec.{js,cjs,mjs,ts,tsx,jsx}',
'**/__tests__/**',
],
},
},
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
})
// tests/setup.js
import { vi } from 'vitest'
// Mock 全局对象
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})
// 测试示例
// src/utils/__tests__/format.test.js
import { describe, it, expect } from 'vitest'
import { formatDate, formatCurrency } from '../format'
describe('format utils', () => {
it('should format date correctly', () => {
const date = new Date('2024-03-30')
expect(formatDate(date)).toBe('2024-03-30')
})
it('should format currency correctly', () => {
expect(formatCurrency(1234.56)).toBe('¥1,234.56')
})
})
部署配置
静态部署
// vite.config.js - 部署配置
export default defineConfig({
base: process.env.NODE_ENV === 'production' ? '/my-app/' : '/',
build: {
outDir: 'dist',
assetsDir: 'static',
// 生成 manifest.json
manifest: true,
// 生成 service worker
rollupOptions: {
output: {
// 确保文件名稳定
entryFileNames: 'js/[name].[hash].js',
chunkFileNames: 'js/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]',
},
},
},
})
// 部署脚本
// scripts/deploy.js
import { execSync } from 'child_process'
import { readFileSync } from 'fs'
const packageJson = JSON.parse(readFileSync('./package.json', 'utf8'))
const version = packageJson.version
console.log(`部署版本: ${version}`)
// 构建
execSync('npm run build', { stdio: 'inherit' })
// 部署到 CDN
execSync(`aws s3 sync dist/ s3://my-bucket/releases/${version}/`, { stdio: 'inherit' })
// 更新 latest 指向
execSync(`aws s3 sync dist/ s3://my-bucket/latest/`, { stdio: 'inherit' })
console.log('部署完成!')
总结
Vite 为现代前端开发提供了卓越的开发体验:
- 极速启动:基于 ES 模块的开发服务器
- 热模块替换:快速的 HMR 支持
- 插件生态:丰富的插件系统
- 构建优化:基于 Rollup 的生产构建
- 开发体验:优秀的开发者体验
掌握 Vite,你就能享受到现代前端开发的最佳体验!
Vite 是现代前端开发的首选构建工具,值得深入学习和实践。