Vite 简介

Vite 是下一代前端构建工具,由 Vue.js 的作者尤雨溪开发。它利用浏览器原生 ES 模块支持,提供极速的开发服务器启动和热模块替换(HMR)。

核心优势

  1. 极速启动:不需要打包,直接提供原生 ESM 模块
  2. 闪电般 HMR:模块更新速度快,不受应用规模影响
  3. 丰富的功能:开箱支持 TypeScript、JSX、CSS 等
  4. 优化的构建:基于 Rollup,高度可配置
  5. 插件系统:兼容 Rollup 插件生态

快速开始

创建项目

# 使用 npm
npm create vite@latest my-app

# 使用 yarn
yarn create vite my-app

# 使用 pnpm
pnpm create vite my-app

# 使用 bun
bun create vite my-app

# 指定模板
npm create vite@latest my-app -- --template vue-ts
npm create vite@latest my-app -- --template react-ts
npm create vite@latest my-app -- --template vanilla-ts
npm create vite@latest my-app -- --template preact-ts
npm create vite@latest my-app -- --template lit-ts
npm create vite@latest my-app -- --template svelte-ts

项目结构

my-app/
├── index.html          # 入口 HTML
├── package.json        # 项目配置
├── vite.config.ts      # Vite 配置
├── tsconfig.json       # TypeScript 配置
├── public/             # 静态资源(不经过构建)
│   └── favicon.ico
└── src/                # 源代码
    ├── main.ts         # 应用入口
    ├── App.vue         # 根组件(Vue)
    ├── components/     # 组件
    ├── assets/         # 资源文件(经过构建)
    └── styles/         # 样式文件

启动开发服务器

# 安装依赖
npm install

# 启动开发服务器
npm run dev
# 或
npx vite

# 指定端口
npx vite --port 3000

# 指定主机
npx vite --host 0.0.0.0

# 打开浏览器
npx vite --open

# HTTPS
npx vite --https

配置文件详解

Vite 配置文件为 vite.config.ts(或 .js.mjs)。

// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
import { visualizer } from 'rollup-plugin-visualizer';
import compression from 'vite-plugin-compression';

export default defineConfig(({ mode, command }) => {
  // mode: 'development' | 'production'
  // command: 'serve' | 'build'
  
  return {
    // 根目录(默认 process.cwd())
    root: process.cwd(),
    
    // 公共基础路径
    base: '/',
    
    // 模式
    mode: mode,
    
    // 插件
    plugins: [
      vue(),
      // react(),
    ],
    
    // 解析选项
    resolve: {
      // 路径别名
      alias: {
        '@': resolve(__dirname, 'src'),
        '@components': resolve(__dirname, 'src/components'),
        '@utils': resolve(__dirname, 'src/utils'),
        '@assets': resolve(__dirname, 'src/assets'),
      },
      
      // 自动补全扩展名
      extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
      
      // 条件导出
      conditions: ['import'],
    },
    
    // CSS 配置
    css: {
      // CSS 模块
      modules: {
        localsConvention: 'camelCaseOnly',
        generateScopedName: '[name]__[local]___[hash:base64:5]',
      },
      
      // PostCSS 配置
      postcss: {
        plugins: [
          require('autoprefixer'),
          require('tailwindcss'),
        ],
      },
      
      // 预处理器选项
      preprocessorOptions: {
        scss: {
          additionalData: `@import "@/styles/variables.scss";`,
          javascriptEnabled: true,
        },
        less: {
          modifyVars: {
            '@primary-color': '#1890ff',
          },
          javascriptEnabled: true,
        },
        stylus: {
          // stylus 选项
        },
      },
      
      // 是否启用 CSS 代码分割
      codeSplitting: true,
      
      // 开发服务器中启用 source map
      devSourcemap: true,
    },
    
    // JSON 配置
    json: {
      stringify: false, // 启用后 JSON 文件会被导入为字符串
    },
    
    // 静态资源处理
    assetsInclude: ['**/*.gltf', '**/*.hdr'],
    
    // 开发服务器配置
    server: {
      // 服务器主机
      host: 'localhost',
      
      // 服务器端口
      port: 3000,
      
      // 启动时自动打开浏览器
      open: true,
      
      // 启用 CORS
      cors: true,
      
      // 强制转换请求(用于开发)
      force: false,
      
      // 代理配置
      proxy: {
        // 字符串简写
        '/api': 'http://localhost:8080',
        
        // 对象配置
        '/api/v1': {
          target: 'http://jsonplaceholder.typicode.com',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api\/v1/, ''),
        },
        
        // WebSocket 代理
        '/socket.io': {
          target: 'ws://localhost:3000',
          ws: true,
          changeOrigin: true,
        },
        
        // 自定义代理
        '^/fallback/.*': {
          target: 'http://jsonplaceholder.typicode.com',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/fallback/, ''),
          configure: (proxy, options) => {
            proxy.on('proxyReq', (proxyReq, req, res) => {
              console.log('Proxying:', req.url);
            });
          },
        },
      },
      
      // 文件系统监听
      watch: {
        usePolling: false,
        interval: 100,
      },
      
      // HTTPS 配置
      https: {
        key: './server.key',
        cert: './server.cert',
      },
      
      // 预热文件(预转换)
      warmup: {
        clientFiles: ['./src/main.ts', './src/App.vue'],
      },
    },
    
    // 预览服务器配置(类似 server)
    preview: {
      port: 4173,
    },
    
    // 构建选项
    build: {
      // 输出目录
      outDir: 'dist',
      
      // 清空输出目录
      emptyOutDir: true,
      
      // 静态资源目录
      assetsDir: 'assets',
      
      // 生成 manifest 文件
      manifest: false,
      
      // 禁用 gzip 压缩大小报告
      brotliSize: true,
      
      // 代码分割策略
      chunkSizeWarningLimit: 500,
      
      // 静态资源内联阈值
      assetsInlineLimit: 4096, // 4kb
      
      // CSS 代码分割
      cssCodeSplit: true,
      
      // source map
      sourcemap: false,
      
      // 压缩选项
      minify: 'esbuild', // 'terser' | 'esbuild' | false
      
      // Terser 选项(当 minify 为 'terser' 时)
      terserOptions: {
        compress: {
          drop_console: true,
          drop_debugger: true,
        },
      },
      
      // Rollup 选项
      rollupOptions: {
        // 外部依赖(不打包)
        external: ['vue', 'react', 'react-dom'],
        
        // 输出选项
        output: {
          // 手动分块
          manualChunks: {
            'vendor': ['vue', 'vue-router', 'pinia'],
            'utils': ['lodash', 'dayjs'],
          },
          
          // 文件名格式
          entryFileNames: 'assets/[name]-[hash].js',
          chunkFileNames: 'assets/[name]-[hash].js',
          assetFileNames: 'assets/[name]-[hash].[ext]',
          
          // 全局变量(配合 external 使用)
          globals: {
            vue: 'Vue',
            react: 'React',
          },
        },
        
        // 插件
        plugins: [
          // rollup 插件
        ],
        
        //  treeshaking 选项
        treeshake: {
          moduleSideEffects: false,
        },
      },
      
      //  CommonJS 兼容
      commonjsOptions: {
        include: [/node_modules/],
      },
      
      // 动态导入预加载
      modulePreload: {
        polyfill: true,
      },
      
      // 报告压缩大小
      reportCompressedSize: true,
    },
    
    // 依赖优化
    optimizeDeps: {
      // 预转换依赖
      include: ['lodash-es', 'axios'],
      
      // 排除依赖
      exclude: ['@vue/compiler-sfc'],
      
      // 强制预转换
      force: false,
      
      // 自动处理
      entries: ['index.html'],
      
      // esbuild 选项
      esbuildOptions: {
        target: 'es2020',
      },
    },
    
    // SSR 特定选项
    ssr: {
      // SSR 外部化依赖
      external: [
        'lodash',
        'moment',
      ],
      
      // 不外部化的依赖
      noExternal: ['some-package'],
      
      // 优化选项
      optimizeDeps: {
        include: ['axios'],
      },
    },
    
    // 环境变量前缀
    envPrefix: 'VITE_',
    
    // 工作区根目录
    workspaceRoot: process.cwd(),
    
    // 清除屏幕
    clearScreen: true,
    
    // 日志级别
    logLevel: 'info', // 'info' | 'warn' | 'error' | 'silent'
    
    // 自定义 logger
    customLogger: undefined,
  };
});

环境变量

环境变量文件

Vite 使用 dotenv 加载环境变量。

# .env
VITE_APP_TITLE=我的应用
VITE_API_URL=http://localhost:8080/api
VITE_ENABLE_ANALYTICS=true

# .env.development
VITE_API_URL=http://localhost:8080/api
VITE_DEBUG=true

# .env.production
VITE_API_URL=https://api.example.com
VITE_ENABLE_ANALYTICS=false

# .env.staging
VITE_API_URL=https://staging-api.example.com

在代码中使用

// 访问环境变量
const appTitle = import.meta.env.VITE_APP_TITLE;
const apiUrl = import.meta.env.VITE_API_URL;

// 内置环境变量
console.log(import.meta.env.MODE); // 'development' | 'production'
console.log(import.meta.env.DEV); // 开发模式为 true
console.log(import.meta.env.PROD); // 生产模式为 true
console.log(import.meta.env.SSR); // 是否为 SSR

// TypeScript 类型定义
// env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string;
  readonly VITE_API_URL: string;
  readonly VITE_ENABLE_ANALYTICS: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

环境变量模式

# 开发模式
vite # 加载 .env 和 .env.development

# 生产构建
vite build # 加载 .env 和 .env.production

# 自定义模式
vite build --mode staging # 加载 .env 和 .env.staging
vite --mode development # 开发模式

HTML 入口

Vite 支持 HTML 文件作为入口。

<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    
    <!-- 环境变量替换 -->
    <title>%VITE_APP_TITLE%</title>
    
    <!-- 预加载关键资源 -->
    <link rel="modulepreload" href="/src/main.ts" />
  </head>
  <body>
    <div id="app"></div>
    
    <!-- 入口脚本(必须是 type="module") -->
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

静态资源处理

导入资源

// 直接导入(会被转换为 URL)
import logo from './assets/logo.png';
import video from './assets/video.mp4';

// 使用资源
<img :src="logo" />
<video :src="video"></video>

// URL 引用
const logoUrl = new URL('./assets/logo.png', import.meta.url).href;

Public 目录

public/
├── favicon.ico
├── robots.txt
└── images/
    └── background.jpg
<!-- 在 HTML 中直接引用 -->
<img src="/images/background.jpg" />
<link rel="icon" href="/favicon.ico" />

资源内联

// 小于 4kb 的资源会被内联为 base64
// 可以通过 ?url 强制使用 URL
import smallImage from './small.png'; // 可能是 base64
import largeImage from './large.png?url'; // 始终是 URL

// 通过 ?raw 导入原始内容
import textContent from './data.txt?raw';

插件系统

官方插件

// Vue 支持
import vue from '@vitejs/plugin-vue';

// React 支持
import react from '@vitejs/plugin-react';

// Preact 支持
import preact from '@vitejs/plugin-preact';

// Legacy 浏览器支持
import legacy from '@vitejs/plugin-legacy';

常用社区插件

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { visualizer } from 'rollup-plugin-visualizer';
import compression from 'vite-plugin-compression';
import vitePluginImp from 'vite-plugin-imp';
import viteSvgIcons from 'vite-plugin-svg-icons';
import viteCompression from 'vite-plugin-compression';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';

export default defineConfig({
  plugins: [
    vue(),
    
    // 包体积分析
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
    }),
    
    // Gzip 压缩
    compression({
      algorithm: 'gzip',
      ext: '.gz',
      threshold: 10240, // 10kb
    }),
    
    // 按需导入(Element Plus)
    vitePluginImp({
      libList: [
        {
          libName: 'element-plus',
          style: (name) => `element-plus/theme-chalk/${name}.css`,
        },
      ],
    }),
    
    // SVG 图标
    viteSvgIcons({
      iconDirs: [resolve(process.cwd(), 'src/assets/icons')],
      symbolId: 'icon-[dir]-[name]',
    }),
    
    // 自动导入 API
    AutoImport({
      imports: ['vue', 'vue-router', 'pinia'],
      resolvers: [ElementPlusResolver()],
      dts: 'src/auto-imports.d.ts',
    }),
    
    // 自动导入组件
    Components({
      resolvers: [ElementPlusResolver()],
      dts: 'src/components.d.ts',
    }),
  ],
});

编写自定义插件

// 简单的 Vite 插件
import type { Plugin } from 'vite';

function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    
    // 解析配置
    configResolved(config) {
      console.log('Config resolved:', config.mode);
    },
    
    // 转换模块
    transform(code, id) {
      if (id.endsWith('.vue')) {
        // 转换 Vue 文件
        return {
          code: code.replace(/console\.log\(.*?\);?/g, ''),
          map: null,
        };
      }
    },
    
    // 转换 HTML
    transformIndexHtml(html) {
      return html.replace(
        '</body>',
        '<script>console.log("Injected!")</script></body>'
      );
    },
    
    // 生成 bundle 后
    generateBundle(options, bundle) {
      // 处理生成的文件
    },
  };
}

// 带选项的插件
interface MyPluginOptions {
  include?: string[];
  exclude?: string[];
}

function myPluginWithOptions(options: MyPluginOptions = {}): Plugin {
  const { include = [], exclude = [] } = options;
  
  return {
    name: 'my-plugin-with-options',
    
    transform(code, id) {
      if (include.some(pattern => id.includes(pattern))) {
        // 转换
      }
      if (exclude.some(pattern => id.includes(pattern))) {
        return; // 跳过
      }
    },
  };
}

多页面应用

// vite.config.ts
import { resolve } from 'path';

export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        index: resolve(__dirname, 'src/main.ts'),
        admin: resolve(__dirname, 'src/admin/main.ts'),
        user: resolve(__dirname, 'src/user/main.ts'),
      },
      output: {
        entryFileNames: 'assets/[name]-[hash].js',
        chunkFileNames: 'assets/[name]-[hash].js',
        assetFileNames: 'assets/[name]-[hash].[ext]',
      },
    },
  },
});
<!-- index.html -->
<script type="module" src="/src/main.ts"></script>

<!-- admin.html -->
<script type="module" src="/src/admin/main.ts"></script>

<!-- user.html -->
<script type="module" src="/src/user/main.ts"></script>

库模式

将项目打包为库供他人使用。

// vite.config.ts
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.ts'),
      name: 'MyLibrary',
      fileName: (format) => `my-library.${format}.js`,
      formats: ['es', 'umd', 'iife'],
    },
    rollupOptions: {
      // 外部化依赖
      external: ['vue'],
      output: {
        globals: {
          vue: 'Vue',
        },
      },
    },
  },
});

性能优化

代码分割

// 路由级别的代码分割
const routes = [
  {
    path: '/',
    component: () => import('./views/Home.vue'),
  },
  {
    path: '/about',
    component: () => import('./views/About.vue'),
  },
  {
    path: '/admin',
    component: () => import('./views/Admin.vue'),
  },
];

// 组件级别的懒加载
const HeavyComponent = defineAsyncComponent(
  () => import('./components/HeavyComponent.vue')
);

预加载

// vite.config.ts
export default defineConfig({
  build: {
    modulePreload: {
      polyfill: true,
    },
  },
});
<!-- 手动预加载 -->
<link rel="modulepreload" href="/assets/vendor-hash.js" />
<link rel="modulepreload" href="/assets/main-hash.js" />

依赖预构建

// vite.config.ts
export default defineConfig({
  optimizeDeps: {
    include: [
      'lodash-es',
      'axios',
      'dayjs',
    ],
    exclude: [
      '@vue/compiler-sfc',
    ],
  },
});

常见问题

1. 开发服务器慢

// 预构建依赖
optimizeDeps: {
  include: ['large-package'],
  force: true, // 强制重新预构建
}

2. 热更新不生效

// 检查文件路径是否正确
server: {
  watch: {
    usePolling: true,
    interval: 100,
  },
}

3. 构建后路径错误

// 配置正确的 base
export default defineConfig({
  base: '/my-app/', // 如果部署在子目录
});

4. 环境变量不生效

# 确保使用 VITE_ 前缀
VITE_APP_TITLE=MyApp

# 重启开发服务器
# 清除缓存
rm -rf node_modules/.vite

5. TypeScript 路径别名不生效

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}
// vite.config.ts
resolve: {
  alias: {
    '@': resolve(__dirname, 'src'),
  },
}