发布于

Vite 构建工具指南:下一代前端构建工具的完整实践

作者

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 为现代前端开发提供了卓越的开发体验:

  1. 极速启动:基于 ES 模块的开发服务器
  2. 热模块替换:快速的 HMR 支持
  3. 插件生态:丰富的插件系统
  4. 构建优化:基于 Rollup 的生产构建
  5. 开发体验:优秀的开发者体验

掌握 Vite,你就能享受到现代前端开发的最佳体验!


Vite 是现代前端开发的首选构建工具,值得深入学习和实践。