Vite 原理与配置

Vite 是一个快速的开发构建工具,基于 ESBuild(开发)和 Rollup(生产),提供极速的开发体验。


📋 学习目标

  • ✅ 理解 Vite 的核心原理和优势
  • ✅ 掌握 Vite 的基础配置
  • ✅ 能够搭建 React、Vue 等项目
  • ✅ 理解开发和生产环境的差异
  • ✅ 掌握 Vite 的插件系统

什么是 Vite

Vite(法语意为”快速”)是一个由 Vue.js 作者尤雨溪开发的构建工具,主要特点:

  • 极速开发服务器:基于 ESBuild,启动速度极快
  • 按需编译:只编译当前页面需要的模块
  • 原生 ESM:开发环境使用原生 ES 模块
  • 生产构建:使用 Rollup 进行优化打包

核心原理深度剖析

开发环境的革命性设计

1. 原生 ESM 的威力

传统构建工具的问题

  • Webpack 需要先打包整个应用才能启动
  • 随着项目增长,启动时间线性增长
  • 修改一个文件需要重新打包整个依赖图

Vite 的解决方案

// index.html
<script type="module" src="/src/main.js"></script>
 
// main.js
import { createApp } from 'vue'
import App from './App.vue'
// 浏览器直接请求这些模块,无需打包

工作原理

  1. 浏览器作为模块加载器:利用浏览器原生 ES 模块支持
  2. 按需请求:只请求当前页面需要的模块
  3. HTTP/2 多路复用:充分利用 HTTP/2 的并行请求能力

性能对比

项目规模:1000 个模块

Webpack 启动:~30s(需要打包所有模块)
Vite 启动:~300ms(只启动开发服务器,按需编译)

2. 依赖预构建机制

为什么需要预构建

  • 第三方库通常是 CommonJS 或 UMD 格式
  • 浏览器无法直接使用
  • 大量小文件导致 HTTP 请求过多

预构建过程

// Vite 自动检测 node_modules 中的依赖
// 使用 ESBuild 将 CommonJS 转换为 ESM
// 合并多个小文件为单个文件
 
// 例如:lodash 有 600+ 文件
// 预构建后:lodash-es.js (单个文件)

预构建配置深度解析

export default defineConfig({
  optimizeDeps: {
    // 强制包含的依赖(即使不在 node_modules)
    include: ['lodash', 'vue'],
    
    // 排除的依赖(不进行预构建)
    exclude: ['@some/package'],
    
    // ESBuild 选项
    esbuildOptions: {
      target: 'es2020',
      // 定义全局变量(用于替换)
      define: {
        'process.env.NODE_ENV': '"development"'
      },
      // 插件
      plugins: []
    },
    
    // 强制重新构建(清除缓存)
    force: false
  }
})

预构建缓存机制

  • 缓存位置:node_modules/.vite
  • 缓存键:基于 package.jsonlockfile
  • 失效条件:依赖版本变化或 force: true

3. 按需编译的智能策略

编译时机

// 用户访问 /src/App.vue
// 1. 浏览器请求:GET /src/App.vue
// 2. Vite 拦截请求
// 3. 实时编译 .vue 文件
// 4. 返回编译后的 JavaScript

编译流程

请求 → Vite 拦截 → 插件处理 → ESBuild 编译 → 返回结果

性能优化技巧

  • 文件系统缓存:编译结果缓存到内存
  • 增量编译:只编译变更的文件
  • 并行编译:利用多核 CPU

生产环境的 Rollup 集成

为什么生产环境使用 Rollup?

  1. 代码质量:Rollup 的 Tree-shaking 更彻底
  2. 输出格式:支持多种输出格式(ESM、CJS、UMD)
  3. 插件生态:Rollup 插件生态成熟
  4. 代码分割:Rollup 的代码分割策略更优

生产构建流程

// 1. 依赖预构建(使用 Rollup)
// 2. 应用代码打包(使用 Rollup)
// 3. 代码分割(基于动态 import)
// 4. 资源优化(压缩、优化)

构建配置深度解析

export default defineConfig({
  build: {
    // 输出目录
    outDir: 'dist',
    
    // 资源内联阈值(小于此大小的资源内联为 base64)
    assetsInlineLimit: 4096,
    
    // CSS 代码分割
    cssCodeSplit: true,
    
    // Source Map
    sourcemap: true, // 或 'inline' | 'hidden'
    
    // 最小化
    minify: 'esbuild', // 'esbuild' | 'terser' | false
    
    // Terser 选项(当 minify: 'terser' 时)
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    },
    
    // Rollup 选项
    rollupOptions: {
      // 入口
      input: {
        main: resolve(__dirname, 'index.html'),
        admin: resolve(__dirname, 'admin.html')
      },
      
      // 输出配置
      output: {
        // 手动代码分割
        manualChunks: (id) => {
          // node_modules 中的包
          if (id.includes('node_modules')) {
            // 将 React 相关包单独打包
            if (id.includes('react')) {
              return 'react-vendor'
            }
            // 其他第三方库
            return 'vendor'
          }
        },
        
        // 文件命名
        chunkFileNames: 'js/[name]-[hash].js',
        entryFileNames: 'js/[name]-[hash].js',
        assetFileNames: 'assets/[name]-[hash].[ext]'
      },
      
      // 外部依赖(不打包)
      external: ['react', 'react-dom']
    },
    
    // 构建大小警告阈值(KB)
    chunkSizeWarningLimit: 1000
  }
})

快速开始

安装

# 使用 npm
npm create vite@latest my-project
 
# 使用 yarn
yarn create vite my-project
 
# 使用 pnpm
pnpm create vite my-project

项目结构

my-project/
├── src/
│   ├── main.js
│   └── App.vue
├── index.html
├── package.json
└── vite.config.js

基础配置

vite.config.js

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import vue from '@vitejs/plugin-vue'
 
export default defineConfig({
  // 插件配置
  plugins: [
    react(), // React 项目
    // vue(), // Vue 项目
  ],
  
  // 开发服务器配置
  server: {
    port: 3000,
    open: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },
  
  // 构建配置
  build: {
    outDir: 'dist',
    assetsDir: 'assets',
    sourcemap: true,
    minify: 'esbuild', // 或 'terser'
  },
  
  // 路径别名
  resolve: {
    alias: {
      '@': '/src',
    },
  },
})

React 项目配置

安装依赖

npm install react react-dom
npm install -D @vitejs/plugin-react

vite.config.js

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
 
export default defineConfig({
  plugins: [react()],
})

Vue 项目配置

安装依赖

npm install vue
npm install -D @vitejs/plugin-vue

vite.config.js

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
 
export default defineConfig({
  plugins: [vue()],
})

环境变量

.env 文件

# .env
VITE_API_URL=http://localhost:8080
VITE_APP_TITLE=My App

使用环境变量

// 在代码中使用
const apiUrl = import.meta.env.VITE_API_URL
const appTitle = import.meta.env.VITE_APP_TITLE

插件系统深度解析

Vite 插件架构

Vite 插件基于 Rollup 插件系统,但扩展了开发服务器特定的钩子。

插件执行顺序

// 插件按数组顺序执行
plugins: [
  plugin1(), // 先执行
  plugin2(), // 后执行
]

插件钩子详解

构建钩子(Rollup 兼容)

export default function myPlugin() {
  return {
    name: 'my-plugin',
    
    // 选项解析(修改配置)
    config(config, { command }) {
      // 开发环境
      if (command === 'serve') {
        config.server.port = 3000
      }
      // 生产环境
      if (command === 'build') {
        config.build.minify = 'terser'
      }
    },
    
    // 配置解析后(读取最终配置)
    configResolved(resolvedConfig) {
      console.log('最终配置:', resolvedConfig)
    },
    
    // 转换代码(最重要的钩子)
    transform(code, id) {
      // id: 文件路径
      // code: 文件内容
      
      // 只处理特定文件
      if (id.endsWith('.vue')) {
        // 转换 Vue 文件
        return transformVue(code)
      }
      
      // 返回 null 表示不处理
      return null
    },
    
    // 加载文件(自定义加载逻辑)
    load(id) {
      if (id === 'virtual:my-module') {
        return 'export default "Hello"'
      }
      return null
    },
    
    // 解析 ID(路径解析)
    resolveId(id, importer) {
      if (id === 'virtual:my-module') {
        return id // 返回虚拟模块 ID
      }
      return null
    }
  }
}

开发服务器钩子(Vite 特有)

export default function myPlugin() {
  return {
    name: 'my-plugin',
    
    // 配置开发服务器
    configureServer(server) {
      server.middlewares.use((req, res, next) => {
        // 自定义中间件
        if (req.url === '/api/custom') {
          res.end('Custom Response')
        } else {
          next()
        }
      })
    },
    
    // 转换 HTML
    transformIndexHtml(html) {
      // 注入脚本、样式等
      return html.replace(
        '<head>',
        '<head><script src="/inject.js"></script>'
      )
    },
    
    // 热更新处理
    handleHotUpdate({ file, server }) {
      if (file.endsWith('.vue')) {
        // 自定义 HMR 逻辑
        server.ws.send({
          type: 'update',
          updates: [{
            type: 'js-update',
            path: file,
            acceptedPath: file,
            timestamp: Date.now()
          }]
        })
        return [] // 阻止默认更新
      }
    }
  }
}

常用插件深度解析

@vitejs/plugin-react

工作原理

import react from '@vitejs/plugin-react'
 
export default defineConfig({
  plugins: [
    react({
      // 使用新的 JSX 运行时(React 17+)
      jsxRuntime: 'automatic',
      
      // JSX 导入源
      jsxImportSource: 'react',
      
      // Babel 选项
      babel: {
        plugins: [
          // 自定义 Babel 插件
        ]
      },
      
      // 排除的文件
      exclude: /node_modules/
    })
  ]
})

内部实现

  1. 使用 ESBuild 转换 JSX
  2. 自动注入 React import(如果使用 classic runtime)
  3. Fast Refresh 支持(HMR)

@vitejs/plugin-vue

工作原理

import vue from '@vitejs/plugin-vue'
 
export default defineConfig({
  plugins: [
    vue({
      // 模板编译选项
      template: {
        compilerOptions: {
          // 自定义指令
          directives: {},
          // 是否是自定义元素
          isCustomElement: (tag) => tag.startsWith('my-')
        }
      },
      
      // 响应式转换(Vue 3.2+)
      reactivityTransform: true,
      
      // 排除的文件
      exclude: /node_modules/
    })
  ]
})

自定义插件实战

案例 1:自动注入环境变量

export default function envPlugin() {
  return {
    name: 'env-plugin',
    config(config) {
      // 读取 .env 文件
      const env = loadEnv(config.mode, process.cwd())
      
      // 注入到 define
      config.define = {
        ...config.define,
        'import.meta.env.CUSTOM_VAR': JSON.stringify(env.CUSTOM_VAR)
      }
    }
  }
}

案例 2:虚拟模块

export default function virtualModulePlugin() {
  const virtualModuleId = 'virtual:my-data'
  const resolvedVirtualModuleId = '\0' + virtualModuleId
  
  return {
    name: 'virtual-module',
    
    resolveId(id) {
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId
      }
    },
    
    load(id) {
      if (id === resolvedVirtualModuleId) {
        // 动态生成模块内容
        return `export const data = ${JSON.stringify(getData())}`
      }
    }
  }
}
 
// 使用
import { data } from 'virtual:my-data'

案例 3:资源处理

export default function assetPlugin() {
  return {
    name: 'asset-plugin',
    
    transform(code, id) {
      // 处理 SVG 文件
      if (id.endsWith('.svg')) {
        const svg = fs.readFileSync(id, 'utf-8')
        // 转换为 React 组件
        return `
          import React from 'react'
          export default function SvgIcon() {
            return ${JSON.stringify(svg)}
          }
        `
      }
    }
  }
}

性能优化深度策略

依赖预构建优化

优化策略 1:精确控制预构建范围

export default defineConfig({
  optimizeDeps: {
    // 只预构建必要的依赖
    include: [
      // 大型库(如 lodash)
      'lodash',
      // 频繁使用的工具库
      'axios',
      'dayjs'
    ],
    
    // 排除不需要预构建的包
    exclude: [
      // 已经是 ESM 格式的包
      '@some/esm-package',
      // 动态导入的包
      '@dynamic/package'
    ],
    
    // 强制重新构建(开发时调试用)
    force: process.env.FORCE_OPTIMIZE === 'true'
  }
})

优化策略 2:预构建性能分析

# 查看预构建信息
DEBUG=vite:optimize-deps npm run dev
 
# 输出示例:
# [vite] Optimizing dependencies...
# [vite] Pre-bundling lodash (this will be run only when your dependencies or config have changed)

性能指标

  • 首次预构建:5-30s(取决于依赖数量)
  • 后续启动:< 1s(使用缓存)

代码分割深度优化

策略 1:基于路由的代码分割

// router.js
const routes = [
  {
    path: '/home',
    // 动态导入,自动代码分割
    component: () => import('./pages/Home.vue')
  },
  {
    path: '/about',
    component: () => import('./pages/About.vue')
  }
]

策略 2:手动代码分割

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: (id) => {
          // 1. 第三方库分离
          if (id.includes('node_modules')) {
            // React 生态
            if (id.includes('react') || id.includes('react-dom')) {
              return 'react-vendor'
            }
            // Vue 生态
            if (id.includes('vue')) {
              return 'vue-vendor'
            }
            // UI 库
            if (id.includes('ant-design') || id.includes('element-plus')) {
              return 'ui-vendor'
            }
            // 工具库
            if (id.includes('lodash') || id.includes('axios')) {
              return 'utils-vendor'
            }
            // 其他第三方库
            return 'vendor'
          }
          
          // 2. 业务代码分离
          if (id.includes('/src/pages/')) {
            // 按页面目录分割
            const match = id.match(/\/src\/pages\/([^/]+)/)
            if (match) {
              return `page-${match[1]}`
            }
          }
          
          // 3. 工具函数分离
          if (id.includes('/src/utils/')) {
            return 'utils'
          }
        }
      }
    }
  }
})

策略 3:预加载优化

// 使用 import() 的预加载提示
const Home = () => import(
  /* webpackPreload: true */
  /* webpackChunkName: "home" */
  './pages/Home.vue'
)

HMR 性能优化

优化策略

export default defineConfig({
  server: {
    hmr: {
      // 覆盖端口(默认与 server.port 相同)
      port: 24678,
      // 覆盖主机(默认与 server.host 相同)
      host: 'localhost',
      // 协议('ws' | 'wss')
      protocol: 'ws',
      // WebSocket 服务器路径
      path: '/hmr',
      // 客户端尝试重新连接的间隔
      clientPort: 24678
    }
  }
})

HMR 边界优化

// 避免 HMR 边界问题
// ❌ 错误:在模块顶层使用动态导入
const data = await import('./data.js')
 
// ✅ 正确:在函数中使用
async function loadData() {
  const data = await import('./data.js')
  return data
}

构建性能优化

1. 并行构建

export default defineConfig({
  build: {
    // 启用多线程压缩(Terser)
    minify: 'terser',
    terserOptions: {
      compress: {
        // 并行处理
        parallel: true
      }
    }
  }
})

2. 缓存策略

export default defineConfig({
  build: {
    // 启用构建缓存
    cacheDir: 'node_modules/.vite',
    
    // 输出文件包含 hash(利用浏览器缓存)
    rollupOptions: {
      output: {
        entryFileNames: 'js/[name]-[hash].js',
        chunkFileNames: 'js/[name]-[hash].js',
        assetFileNames: 'assets/[name]-[hash].[ext]'
      }
    }
  }
})

3. 构建分析

# 安装分析工具
npm install -D rollup-plugin-visualizer
 
# 配置
import { visualizer } from 'rollup-plugin-visualizer'
 
export default defineConfig({
  plugins: [
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true
    })
  ]
})

运行时性能优化

1. 资源预加载

<!-- index.html -->
<link rel="modulepreload" href="/src/main.js">
<link rel="preload" href="/assets/font.woff2" as="font" type="font/woff2" crossorigin>

2. 懒加载优化

// 使用 Intersection Observer 实现可视区域加载
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // 加载组件
      import('./HeavyComponent.vue').then(module => {
        // 渲染组件
      })
      observer.unobserve(entry.target)
    }
  })
})

3. 资源压缩

export default defineConfig({
  build: {
    // 启用 gzip 压缩(需要服务器支持)
    // 或使用 vite-plugin-compression
    assetsInlineLimit: 4096, // 小于 4KB 的资源内联
  }
})

与 Webpack 对比

特性ViteWebpack
开发启动极快(ESBuild)较慢
HMR极快较快
生产构建RollupWebpack
配置复杂度简单复杂
生态较新成熟

最佳实践

  1. 使用 TypeScript:Vite 原生支持 TypeScript
  2. 路径别名:使用 @ 等别名简化导入
  3. 环境变量:使用 .env 文件管理环境变量
  4. 插件选择:优先使用官方插件
  5. 性能优化:合理配置依赖预构建

相关链接


最后更新:2025


Vite 构建工具 工程化