认识 Vite:第三代构建工具

Q: Vite 到底是脚手架还是构建工具?它的核心优势是什么?

A: Vite 既是脚手架也是构建工具,两者的界限在现代前端工具链中已经模糊。它可以搭建各种类型(Vue、React、Svelte、Solid.js)的项目,同时在内部包含两个构建工具——esbuild(用于开发环境)和rollup(用于生产环境)。

构建工具的演进

代际 代表工具 核心能力
第一代 npm scripts、grunt、gulp 编译、合并、压缩等基础任务
第二代 webpack、rollup、parcel 模块依赖处理、合并打包
第三代 Vite、Turbopack、Rspack 使用 Rust 重写工具链,极致性能

Vite 如何解决 webpack 的痛点

Webpack 在构建大型项目时非常慢,因为启动时必须先对项目进行全量打包,然后运行打包后的文件。

Webpack打包流程

Vite 的做法则完全不同——完全跳过打包步骤,利用浏览器原生的 ES Module import 机制按需获取内容:

Vite按需获取

对于 .vue 等浏览器无法直接识别的模块,Vite 在开发服务器端实时编译为 JS 再返回给浏览器:

Vite编译Vue文件

此外,Vite 的热更新(HMR)底层使用 WebSocket 实现,确保了模块替换的实时性和精准性。


Vite 配置文件全览

Q: Vite 配置文件如何组织?有哪些常用配置项?

A: Vite 配置文件位于项目根目录下的 vite.config.js,推荐使用 defineConfig 工具函数来获得更好的类型提示:

import { defineConfig } from 'vite';
export default defineConfig({
  // 配置选项
});

配置分为几大类别:普通配置开发服务器配置构建配置预览配置依赖优化配置SSR 配置Worker 配置

普通配置

配置项 类型 说明
root string index.html 所在位置,默认项目根目录
define Record 定义全局常量替换,构建时静态替换
resolve.alias object 路径别名,简化模块路径引用
resolve.extensions string[] 导入时可省略的扩展名列表

define 示例——定义全局常量,无需 import 即可直接使用:

export default defineConfig({
  define: {
    __APP_VERSION__: JSON.stringify('v1.0.0'),
    __API_URL__: 'window.__backend_api_url',
  },
});

注意:Vite 使用 esbuild 的 define 进行替换,值必须是包含 JSON 可序列化值的字符串或单个标识符。

resolve.alias 配置路径别名:

import path from 'path';
export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components'),
      '@utils': path.resolve(__dirname, 'src/utils'),
    },
  },
});

CSS 配置

CSS 相关的核心配置项:

export default defineConfig({
  css: {
    postcss: {                    // 内联 PostCSS 配置
      plugins: [autoprefixer(), cssnano()],
    },
    preprocessorOptions: {        // CSS 预处理器配置
      scss: {
        additionalData: `@import "@/styles/mixins.scss";`, // 全局注入
      },
    },
    devSourcemap: true,           // 开发环境 source maps
    transformer: 'postcss',       // CSS 处理引擎:'postcss' 或 'lightningcss'
  },
});

additionalData 非常实用——它会在每个 SCSS 文件的头部自动注入指定内容,常用于注入全局变量或 mixins。

开发服务器配置

export default defineConfig({
  server: {
    host: '0.0.0.0',        // 监听所有网络接口,方便多设备测试
    port: 5173,              // 端口号,默认 5173
    strictPort: true,        // 端口被占用时直接失败
    open: true,              // 启动时自动打开浏览器
    proxy: {                 // 代理配置
      '/api': {
        target: 'http://jsonplaceholder.typicode.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
    https: {                 // HTTPS 配置
      key: fs.readFileSync('/path/to/server.key'),
      cert: fs.readFileSync('/path/to/server.crt'),
    },
    watch: {                 // 文件监听(底层使用 chokidar)
      ignored: ['**/ignored-directory/**'],
      usePolling: true,
      interval: 100,
    },
  },
});

其中 proxy 是最常用的配置之一,用于解决开发环境下的跨域问题。


依赖预构建:启动性能的秘诀

Q: 前面说 Vite 不打包,为什么又出现了「依赖预构建」?

A: 这里存在两个痛点:

  1. 如果完全不打包,某些依赖文件过多会导致浏览器请求数量爆炸
  2. 部分依赖仍以 CommonJS 格式发布,不兼容浏览器原生 ESM 环境。

因此 Vite 在首次启动项目时,会针对 node_modules 中的依赖进行预构建。这个阶段的构建工具是 esbuild——一个用 Go 语言编写的高效构建工具,大部分工作并行处理,速度极快。

esbuild 在预构建阶段做的事情:

步骤 说明
格式转换 将 CommonJS、UMD 格式转换为 ESM 格式
打包 将多个内部模块合并为一个文件,减少请求数
最小化和压缩 在生产构建阶段执行

缓存机制

预构建产物缓存在 node_modules/.vite 目录下。以下情况会触发重新预构建:

  • 包管理器的锁文件内容发生变化(package-lock.jsonyarn.lock 等)
  • 补丁文件夹修改时间变化
  • vite.config.js 中依赖预构建相关配置变化
  • NODE_ENV 的值变动

浏览器端也会对预构建依赖进行强缓存max-age=31536000, immutable),版本变化会通过 URL 附加版本查询使缓存自动失效:

http://localhost:3000/node_modules/.vite/lodash.js?v=4.17.19
http://localhost:3000/node_modules/.vite/lodash.js?v=4.17.20  // 版本升级后 URL 变化

自定义预构建行为

通过 optimizeDeps 配置对预构建进行精细控制:

export default defineConfig({
  optimizeDeps: {
    entries: ['src/main.js', 'src/admin.js'],  // 指定入口文件
    include: ['my-lib/components/**/*.vue'],    // 强制包含某些包
    exclude: ['some-large-lib'],                // 排除某些包
    esbuildOptions: {                           // esbuild 选项
      target: 'es2015',
    },
    force: true,                                // 强制重新预构建
  },
});

构建生产版本:从开发到上线

Q: 为什么生产构建使用 rollup 而不是 esbuild?

A: 答案在于不同环境下构建目标不同

  • 开发环境:追求构建速度,esbuild 极快
  • 生产环境:追求代码质量和优化程度,rollup 更强

rollup 的生产优势:

能力 说明
代码分割和动态导入 处理循环依赖、动态模块路径等复杂场景
Tree Shaking 更精准地剔除未使用代码,减少包体积
插件生态 成熟广泛的插件生态,支持各种构建需求
输出控制 更细粒度的输出格式和结构控制
生产优化 更复杂的代码分割和加载策略

常用构建配置

export default defineConfig({
  build: {
    target: 'es2015',            // 浏览器兼容目标(默认 'modules')
    outDir: 'dist',              // 输出目录
    assetsDir: 'assets',         // 静态资源目录
    cssMinify: 'esbuild',        // CSS 压缩方式:'esbuild' | 'lightningcss' | false
    minify: 'esbuild',           // JS 压缩方式:'esbuild' | 'terser'
    sourcemap: true,             // 生成 source map
    rollupOptions: {             // 透传 rollup 配置
      plugins: [],
      external: ['some-external-lib'],
      output: {
        globals: { 'some-external-lib': 'SomeExternalLib' }
      }
    },
  },
});

关于压缩工具的选择:

工具 速度 压缩率 适用场景
esbuild 极快(比 terser 快 20-40 倍) 略低 1%-2% 默认选择,大多数场景足够
terser 较慢 稍好 需要更精细控制时

公共基础路径 base

base 是一个独立于 build 的重要配置项。当应用部署在子路径而非根目录时,需要通过 base 确保静态资源正确加载:

export default defineConfig({
  base: '/my-app/', // 所有静态资源以 /my-app/ 为前缀
});

库模式

如果要把应用打包成供其他项目引用的库,需要使用库模式:

import { resolve } from 'path';
export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'lib/main.js'),     // 入口文件
      name: 'MyLib',                                 // UMD/IIFE 全局变量名
      fileName: (format) => `my-lib.${format}.js`,   // 输出文件名
    },
    rollupOptions: {
      external: ['vue'],                              // 排除外部依赖
      output: {
        globals: { vue: 'Vue' },
      },
    },
  },
});

库模式和普通应用的核心区别:

维度 普通应用 库模式
入口文件 index.html JS 文件
输出格式 针对浏览器 UMD、ESM、CommonJS 多种格式
外部依赖 全部打包 需要排除(如 vue、react)

打包完成后,还需在 package.json 中配置不同环境的入口:

{
  "name": "my-lib",
  "type": "module",
  "main": "./dist/my-lib.umd.js",
  "module": "./dist/my-lib.es.js",
  "exports": {
    ".": {
      "import": "./dist/my-lib.es.js",
      "require": "./dist/my-lib.umd.js"
    }
  }
}

环境变量与模式

Q: Vite 的模式(mode)和 NODE_ENV 有什么不同?

A: NODE_ENV 是 Node.js 生态中广泛使用的环境变量,用于指定代码运行环境(development、production 或 test)。而Vite 中的模式是一个可以在启动时设置的属性,用来指导 Vite 如何加载 .env 文件

两者的关系:使用 mode 可以在不影响 NODE_ENV 的前提下,实现更细粒度的场景控制。

Vite 内置环境变量

变量 说明
import.meta.env.MODE 当前应用运行的模式
import.meta.env.PROD 是否运行在生产环境
import.meta.env.DEV 是否运行在开发环境
import.meta.env.SSR 是否运行在 server 上
import.meta.env.BASE_URL 公共基础路径

安全性:只有以 VITE_ 为前缀的变量才会暴露给客户端代码,防止敏感信息泄漏:

# .env
VITE_SOME_KEY=123     # ✅ 客户端可访问
DB_PASSWORD=foobar    # ❌ 客户端无法访问(没有 VITE_ 前缀)

env 文件类型

.env                # 所有情况下都会加载
.env.local          # 所有情况下都会加载,但会被 git 忽略
.env.[mode]         # 只在指定模式下加载
.env.[mode].local   # 只在指定模式下加载,但会被 git 忽略

典型项目结构:

my-vite-project/
├── .env                  # 默认环境变量
├── .env.development      # 开发环境(VITE_API_URL=https://dev.api.example.com)
├── .env.production       # 生产环境(VITE_API_URL=https://api.example.com)
└── vite.config.js

启动时指定模式:

vite --mode development     # 开发环境
vite build --mode production # 生产构建

Vite CLI 命令全解

Q: Vite CLI 提供了哪些命令?各自有什么用?

A: Vite CLI 分为四大类:

1. 开发服务器

vite                  # 启动开发服务器
vite dev              # 同 vite(别名)
vite serve            # 同 vite(别名)
./node_modules/.bin/vite  # 等价于上面

实际上 vite 命令指向的是 node_modules/.bin 目录下的可执行文件。

2. 构建生产版本

vite build            # 执行生产构建

常用 CLI 参数(CLI 参数的优先级高于配置文件):

参数 说明 示例
--target 编译目标 vite build --target es2015
--outDir 输出目录 vite build --outDir build
--assetsDir 资源目录 vite build --assetsDir static
--manifest 生成资源映射文件 vite build --manifest
-c, --config 指定配置文件 vite build -c vite.config.custom.js
--base 公共基础路径 vite build --base /myapp/

CLI 和配置文件同时指定相同参数时,CLI 的优先级更高

3. 依赖预构建(手动执行)

大多数情况下不需要手动执行,Vite 会自动处理。

4. 本地预览构建产物

vite preview          # 启动本地预览服务器,默认监听 4173 端口

vite preview 的作用是检测打包后的应用是否正常工作,相比以前需要手动用 express 启动服务器的方式,Vite 提供了开箱即用的预览能力。常用参数:

vite preview --host 0.0.0.0 --port 8080

Vite 插件生态

Q: 如何在 Vite 中使用和选择插件?

A: 使用插件很简单——安装后在配置文件的 plugins 数组中引入即可:

import vue from '@vitejs/plugin-vue';
import eslintPlugin from 'vite-plugin-eslint';

export default defineConfig({
  plugins: [vue(), eslintPlugin()],
});

关键细节

1. 传入配置选项

import legacy from '@vitejs/plugin-legacy';
export default defineConfig({
  plugins: [
    legacy({ targets: ['defaults', 'not IE 11'] }),
  ],
});

2. 执行顺序

多个插件按数组从前到后的顺序依次执行。

3. 按需引用

默认情况下插件在开发和生产模式都会调用。通过 apply 属性可按需启用:

plugins: [
  { ...typescript2(), apply: 'build' },  // 仅在构建时使用
],

插件分类

分类 代表插件 说明
官方插件 @vitejs/plugin-vue Vue 3 单文件组件支持
官方插件 @vitejs/plugin-react React 支持 + HMR
官方插件 @vitejs/plugin-legacy 旧浏览器兼容
社区插件 vite-plugin-compression 构建时 gzip/brotli 压缩
社区插件 vite-plugin-pwa 渐进式网络应用(PWA)
社区插件 vite-plugin-svg-icons SVG 图标管理
rollup 插件 @rollup/plugin-image 图片文件导入
rollup 插件 @rollup/plugin-commonjs CommonJS 转 ESM

因为 Vite 生产构建底层使用 rollup,所以 rollup 插件也可以在 Vite 中使用

自定义 Vite 插件

Vite 插件扩展了 rollup 插件接口,并添加了 Vite 独有的配置项。开发自定义插件需要:

  1. 了解 rollup 插件开发 基础
  2. 了解 Vite 的生命周期钩子方法,在不同时间点接入 Vite 内部机制

总结

本文全面解析了 Vite 构建工具的各个层面。Vite 的核心创新在于开发环境完全跳过打包,利用浏览器原生 ESM 实现按需加载,配合 esbuild 的极致编译速度;依赖预构建解决了 CommonJS 兼容和请求数爆炸问题,通过文件缓存和浏览器强缓存实现秒级冷启动;生产构建切换为 rollup,以更精准的 Tree Shaking、代码分割和成熟的插件生态来保证产物质量;环境变量系统通过 modeVITE_ 前缀机制实现了安全的配置管理;插件体系同时兼容 Vite 社区插件和 rollup 插件,覆盖从开发到构建的全流程。掌握这些知识后,可以根据项目需求灵活配置 Vite,充分发挥其性能优势。