跳到主要内容
预计阅读 32 分钟

工程化篇 | Vite

前言

如果说 Webpack 定义了前端构建工具的”第一个时代”,那么 Vite 就是推开”第二个时代”大门的那个人。

2020 年,尤雨溪发布了 Vite 的第一个版本。当时很多人的第一反应是:“又一个打包工具?“但用过之后才发现,Vite 的开发体验和 Webpack 完全不在一个量级——冷启动从几十秒变成几百毫秒,改一行代码几乎瞬间就能看到效果。

但面试中被问到 Vite,很多人的回答还停留在”它比 Webpack 快”。面试官继续追问:

  • Vite 为什么快?快在哪里?
  • 开发模式和生产模式为什么用不同的方案?
  • 什么是”预构建”?为什么需要它?
  • Vite 能不能完全替代 Webpack?

就答不上来了。

本章,我们从 Vite 的设计哲学出发,深入理解它的开发模式生产模式两套方案,再对比 Webpack 的本质差异,最后聊聊插件系统和常见配置。


诊断自测

Q1:Vite 在开发模式下为什么不打包?浏览器怎么处理那些 import 语句?

点击查看答案

Vite 在开发模式下利用了浏览器原生的 ES Module<script type="module">)能力。当浏览器解析到 import 语句时,会向 Vite 的开发服务器发起 HTTP 请求,Vite 按需编译并返回对应的模块。这样就不需要像 Webpack 那样在启动时打包所有模块,从而实现了极快的冷启动。

Q2:Vite 的”预构建(Pre-bundling)“是做什么的?为什么需要它?

点击查看答案

预构建有两个目的:

  1. 兼容 CommonJS/UMD:node_modules 中大量的包是 CJS 格式的(如 React),浏览器不认识 require,需要先把它们转成 ESM 格式
  2. 合并模块请求:有些包(如 lodash-es)有几百个独立的小模块文件,如果不合并,浏览器会发出几百个 HTTP 请求,性能反而更差

Vite 使用 esbuild 在首次启动时快速预构建这些依赖,把它们各自合并成单个 ESM 文件,并缓存到 node_modules/.vite 中。

Q3:Vite 在生产模式下用的是 Rollup 而不是 esbuild 来打包,为什么?

点击查看答案

虽然 esbuild 速度极快,但在生产打包所需的一些高级特性上不够成熟,比如:代码分割(Code Splitting)的灵活策略、CSS 代码分割、遗留浏览器兼容(需要生成不同的 polyfill 版本)等。Rollup 在这些方面更成熟、生态更丰富、产物质量更高。所以 Vite 选择了”开发用 esbuild(快),生产用 Rollup(稳)“的双引擎策略。


一、Vite 的设计哲学:为什么不打包?

1.1 Webpack 的问题

在传统的 Webpack 开发流程中,每次启动开发服务器都要经历这样的过程:

全部源码 → 依赖分析 → 模块转换 → 打包 → 启动 Dev Server → 页面可用

项目越大,这个过程越慢。一个中大型项目冷启动 30 秒是常态,有些巨型项目甚至要好几分钟。

更痛苦的是 HMR。Webpack 的 HMR 虽然不用整页刷新,但它需要重新打包受影响的模块链,项目越大越慢。

1.2 Vite 的核心洞察

Vite 观察到一个关键事实:现代浏览器已经原生支持 ES Module 了

ES Module 的 import / export 语法不仅是 JavaScript 的语言特性,也是浏览器实际支持的模块加载方式。当你在 HTML 中写 <script type="module" src="./main.js">,浏览器会:

  1. 请求 main.js
  2. 解析其中的 import 语句
  3. 对每个 import 发起新的 HTTP 请求
  4. 递归加载所有依赖

既然浏览器自己就能处理模块加载,那开发时为什么还要打包?

Vite 的核心思路:在开发模式下,不打包,让浏览器自己加载模块。 服务器只负责”按需编译”——浏览器请求哪个文件,服务器就编译哪个文件。


二、开发模式:原生 ESM + esbuild 预构建

2.1 按需编译

浏览器请求 main.js → Vite 编译 main.js(如果是 .vue/.ts,先转换)→ 返回 ESM 格式的 JS
                   → 浏览器解析 import,请求 App.vue
                   → Vite 编译 App.vue → 返回
                   → ......

这个过程是按需的——只有浏览器实际请求到的文件才会被编译。如果你的项目有 1000 个文件,但当前页面只用到了 50 个,那就只编译 50 个。

这就是 Vite 冷启动快的根本原因:Webpack 需要在启动前打包所有模块,而 Vite 把”编译”推迟到了浏览器请求的那一刻。

2.2 预构建(Dependency Pre-Bundling)

但是,“让浏览器直接加载 node_modules”会遇到两个问题:

问题一:CJS 不兼容

很多 npm 包还是 CommonJS 格式的(require / module.exports),浏览器不认识。

问题二:模块过多导致请求爆炸

有些包(比如 lodash-es)有几百个独立的小模块文件。如果浏览器一个一个地请求,性能会很差。

import { debounce } from 'lodash-es';
// lodash-es 内部还会 import 几十个其他模块
// 浏览器可能要发出上百个请求

为了解决这两个问题,Vite 会在首次启动时esbuild 对 node_modules 中的依赖做预构建

  1. 把 CJS/UMD 格式的包转成 ESM
  2. 把一个包的几百个小模块合并成一个文件
  3. 结果缓存到 node_modules/.vite 目录
node_modules/lodash-es/(几百个文件)
    ↓ esbuild 预构建
node_modules/.vite/deps/lodash-es.js(一个文件)

为什么选 esbuild?因为 esbuild 是用 Go 写的,速度比 JavaScript 写的打包器快 10-100 倍。预构建通常在几百毫秒内就能完成。

2.3 HMR:比 Webpack 更快

Vite 的 HMR 同样基于原生 ESM。当一个文件发生变化:

  1. Vite 只需要精确地让该模块失效
  2. 浏览器重新请求这一个模块
  3. 不需要重新打包整条依赖链

而 Webpack 的 HMR 需要重新构建受影响的 chunk,项目越大越慢。Vite 的 HMR 速度和项目大小几乎无关——因为无论项目有多大,每次更新都只处理变更的那个模块。


三、生产模式:Rollup 打包

3.1 为什么生产环境不能也用原生 ESM?

你可能会问:既然开发模式下原生 ESM 这么好用,生产环境为什么不也这样?原因有几个:

  1. 网络请求过多:原生 ESM 在运行时递归加载模块,意味着大量的 HTTP 请求。生产环境下,这些请求的延迟会严重影响用户体验
  2. 缺少优化:Tree Shaking、代码压缩、代码分割等优化,都需要打包器在构建时做静态分析,运行时加载做不了这些
  3. 兼容性:虽然现代浏览器都支持 ESM,但生产环境可能还需要兼容老浏览器

所以 Vite 在生产模式下使用 Rollup 做传统的打包:

源码 → Rollup 打包 → Tree Shaking → 代码分割 → 压缩 → 输出产物

3.2 为什么选 Rollup 而不是 esbuild?

esbuild 虽然快,但在以下方面不够成熟:

  • 代码分割策略不够灵活:生产环境需要精细地控制 chunk 的拆分策略
  • CSS 代码分割:Rollup 生态有成熟的方案
  • 插件生态:Rollup 的插件生态更丰富、更稳定

Vite 的创造者尤雨溪在博客中也提到:当 esbuild 在这些方面成熟后,不排除未来会切换。事实上,Vite 团队也在推进 Rolldown(用 Rust 重写的 Rollup 兼容构建工具),目标是统一开发和生产的构建引擎。

3.3 Rollup 的核心优势

Rollup 天然适合库和应用的打包,它的几个特点:

  • ESM 优先:Rollup 从诞生之初就以 ESM 为核心,Tree Shaking 能力强
  • 产物干净:相比 Webpack 的运行时代码,Rollup 的产物更精简
  • 插件生态好:大量成熟的 Rollup 插件可以直接在 Vite 中使用

四、Vite vs Webpack:本质差异

维度WebpackVite
开发模式原理先打包所有模块,再启动 Dev Server不打包,基于原生 ESM 按需编译
冷启动速度慢(项目越大越慢)快(与项目大小几乎无关)
HMR 速度需要重建受影响的 chunk只处理变更模块,速度恒定
生产打包自身打包委托 Rollup
模块格式CJS / ESM 都支持ESM 优先
配置复杂度高(需要手动配置大量 loader/plugin)低(大量功能开箱即用)
生态成熟度极其成熟快速成长中
适合场景复杂的大型项目、需要精细控制的场景新项目、追求开发体验的场景

一句话总结: Webpack 是”先打包,再开发”;Vite 是”先开发,再打包(仅生产环境)“。

为什么 Vite 不能完全替代 Webpack?

  1. 生态差距:Webpack 有十年的生态积累,很多特殊需求只有 Webpack 的 loader/plugin 能满足
  2. 微前端:qiankun 等微前端方案对 Webpack 的支持更好
  3. 存量项目:大量已有项目基于 Webpack 构建,迁移成本不可忽视
  4. 开发/生产一致性:Vite 开发用 ESM + esbuild,生产用 Rollup,两套系统偶尔会有行为不一致的问题

五、Vite 插件系统

5.1 基于 Rollup 插件接口

Vite 的插件系统基于 Rollup 的插件接口设计,大部分 Rollup 插件可以直接在 Vite 中使用。同时 Vite 扩展了一些开发服务器特有的钩子。

// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import legacy from '@vitejs/plugin-legacy';

export default defineConfig({
  plugins: [
    vue(),
    legacy({
      targets: ['defaults', 'not IE 11']
    })
  ]
});

5.2 插件钩子分类

Vite 的插件钩子分为两类:

通用钩子(和 Rollup 共享):

  • resolveId:自定义模块解析
  • load:自定义模块加载
  • transform:转换模块内容

Vite 独有钩子:

  • configResolved:Vite 配置解析完成后调用
  • configureServer:配置开发服务器(可以添加自定义中间件)
  • transformIndexHtml:转换 index.html
  • handleHotUpdate:自定义 HMR 更新处理

5.3 一个简单的 Vite 插件

function myPlugin() {
  return {
    name: 'my-plugin',

    // Vite 独有:配置开发服务器
    configureServer(server) {
      server.middlewares.use('/api/hello', (req, res) => {
        res.end(JSON.stringify({ message: 'Hello from plugin!' }));
      });
    },

    // Rollup 通用:转换模块内容
    transform(code, id) {
      if (id.endsWith('.js')) {
        return code.replace(/__VERSION__/g, '"1.0.0"');
      }
    }
  };
}

六、常见配置

6.1 基础配置

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  // 开发服务器
  server: {
    port: 3000,
    open: true,       // 自动打开浏览器
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  },

  // 构建选项
  build: {
    outDir: 'dist',
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          utils: ['lodash-es', 'dayjs']
        }
      }
    }
  },

  // 路径别名
  resolve: {
    alias: {
      '@': '/src'
    }
  },

  // CSS 配置
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/variables.scss";`
      }
    }
  }
});

6.2 环境变量

Vite 使用 .env 文件管理环境变量,只有以 VITE_ 开头的变量才会暴露给客户端代码:

# .env.development
VITE_API_URL=http://localhost:8080
VITE_APP_TITLE=My App (Dev)
// 在代码中使用
console.log(import.meta.env.VITE_API_URL);
console.log(import.meta.env.MODE); // 'development' | 'production'

6.3 多页面应用

export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        main: 'index.html',
        admin: 'admin.html'
      }
    }
  }
});

常见误区

误区一:“Vite 比 Webpack 快是因为用了 esbuild 打包”

不准确。Vite 在开发模式下的”快”主要来自不打包——基于原生 ESM 按需编译,esbuild 只是用来做预构建和 TS/JSX 转译的。在生产模式下,Vite 用的是 Rollup 打包,速度和 Webpack 差不多。把 Vite 的速度优势简单归结为”esbuild 快”,说明没有理解 Vite 的核心设计。

误区二:“Vite 只适合小项目”

恰恰相反,Vite 的按需编译特性让它在大项目中的优势更明显。项目越大,Webpack 冷启动越慢,而 Vite 的冷启动时间几乎不受项目大小影响。很多大型项目(如 Vue 3 官方生态、SvelteKit、Nuxt 3、Astro)都在用 Vite。

误区三:“Vite 的开发模式和生产模式行为完全一致”

不一致。开发模式基于原生 ESM + esbuild 转译,生产模式基于 Rollup 打包。两套系统偶尔会有行为差异,比如某些模块在开发模式下正常但生产打包后报错(通常是因为 CJS/ESM 互操作的问题)。这也是 Vite 团队推进 Rolldown 的原因之一——用同一个引擎同时服务开发和生产。

误区四:“用了 Vite 就不需要了解 Rollup 了”

Vite 的生产打包就是 Rollup,插件系统也是基于 Rollup 的。如果你需要自定义打包行为、写 Vite 插件或者排查生产构建问题,Rollup 的知识是必须的。build.rollupOptions 这个配置项直接透传给 Rollup,不了解 Rollup 等于黑箱操作。


小结

本章我们从 Vite 的设计哲学出发,理解了它”开发不打包、生产用 Rollup”的双模式架构,深入分析了原生 ESM 按需编译和 esbuild 预构建的工作原理。

核心要点

  1. Vite 开发模式的核心:利用浏览器原生 ESM,按需编译,不做全量打包
  2. 预构建的两个目的:CJS → ESM 转换 + 合并小模块(用 esbuild 实现)
  3. 生产模式用 Rollup:因为 Rollup 在代码分割、CSS 处理、插件生态方面更成熟
  4. Vite vs Webpack 的本质差异:不是”用了更快的打包器”,而是”开发时根本不打包”
  5. 插件系统基于 Rollup:大部分 Rollup 插件可以直接用,Vite 额外扩展了 Dev Server 相关的钩子
  6. Rolldown 是未来方向:用 Rust 重写的 Rollup 兼容引擎,目标是统一开发和生产

本章思维导图

Vite
  • 设计哲学
    • 核心洞察:浏览器原生支持 ESM
    • 开发时不打包,按需编译
  • 开发模式
    • 原生 ESM:浏览器请求什么,编译什么
    • esbuild 预构建:CJS→ESM、合并小模块
    • HMR:模块级精确更新,速度与项目大小无关
  • 生产模式
    • 为什么需要打包:请求数量、优化、兼容性
    • 为什么选 Rollup:代码分割、CSS 处理、插件生态
    • 未来方向:Rolldown(Rust 重写的 Rollup)
  • vs Webpack
    • 开发:按需编译 vs 全量打包
    • HMR:模块级更新 vs chunk 级重建
    • 生态:快速成长 vs 极度成熟
  • 插件系统
    • 基于 Rollup 插件接口
    • 通用钩子:resolveId / load / transform
    • Vite 独有钩子:configureServer / handleHotUpdate
  • 常见配置
    • server(端口、代理)
    • build(产物、rollupOptions)
    • resolve.alias / css / 环境变量

练习挑战

第一题 ⭐(基础):概念理解

请解释以下三个概念的关系:原生 ESM、esbuild、Rollup。它们各自在 Vite 中承担什么角色?

点击查看答案与解析
  • 原生 ESM:Vite 开发模式的基础。Vite 利用浏览器原生的 ES Module 能力,让浏览器自己处理模块加载,服务器只做按需编译。
  • esbuild:Vite 开发模式的”加速器”。负责两件事:(1) 预构建第三方依赖(CJS→ESM 转换 + 合并小模块),(2) TypeScript 和 JSX 的快速转译。
  • Rollup:Vite 生产模式的打包引擎。负责代码分割、Tree Shaking、压缩等生产级优化,产物质量高、生态成熟。

三者分工明确:原生 ESM 是开发模式的”运行时策略”,esbuild 是开发模式的”编译加速器”,Rollup 是生产模式的”打包引擎”。

第二题 ⭐⭐(进阶):排查预构建问题

你在项目中安装了一个新的 npm 包 awesome-lib,在开发模式下 import { helper } from 'awesome-lib' 报错:Uncaught SyntaxError: Unexpected token 'exports'。请分析原因并给出解决方案。

点击查看答案与解析

原因: awesome-lib 是 CommonJS 格式的包(使用了 module.exports / exports),但浏览器只认 ESM 语法。这意味着 Vite 的预构建没有正确处理这个包。

可能的原因:

  1. 这个包没有被 Vite 识别为需要预构建的依赖(比如它是通过某种间接方式引入的)
  2. 预构建缓存过期或损坏

解决方案:

  1. 手动将该包加入预构建列表:
// vite.config.js
export default defineConfig({
  optimizeDeps: {
    include: ['awesome-lib']
  }
});
  1. 清除预构建缓存并重启:
rm -rf node_modules/.vite
npx vite --force  # 强制重新预构建
  1. 如果包本身有 ESM 版本,检查 package.json 中的 moduleexports 字段是否正确指向了 ESM 入口。

第三题 ⭐⭐⭐(综合):写一个 Vite 插件

请写一个 Vite 插件 vite-plugin-build-info,功能:

  1. 在开发模式下,启动服务器后在控制台打印 [BuildInfo] Dev server started at {时间}
  2. 在生产模式下,在打包后的 index.html 底部注入一段 HTML 注释:<!-- Built at: {时间} -->
点击查看答案与解析
function buildInfoPlugin() {
  return {
    name: 'vite-plugin-build-info',

    // 开发模式:服务器启动后打印信息
    configureServer(server) {
      server.httpServer?.once('listening', () => {
        console.log(`[BuildInfo] Dev server started at ${new Date().toISOString()}`);
      });
    },

    // 生产模式:在 HTML 底部注入构建时间注释
    transformIndexHtml(html) {
      // transformIndexHtml 只在 build 时生效(开发模式也会调用,但这里我们都注入也没关系)
      const buildTime = new Date().toISOString();
      return html.replace('</html>', `<!-- Built at: ${buildTime} -->\n</html>`);
    }
  };
}

export default buildInfoPlugin;

要点解析:

  • configureServer 是 Vite 独有钩子,只在开发模式下被调用,可以访问底层的 HTTP 服务器
  • transformIndexHtml 是 Vite 独有钩子,用于转换 index.html 的内容
  • 插件函数返回一个对象,必须有 name 属性(用于调试和错误报告)
  • 这道题考察的是对 Vite 插件系统两类钩子(通用钩子 vs Vite 独有钩子)的理解

自我检测

  • 能解释 Vite 在开发模式下”不打包”的原理,包括浏览器原生 ESM 如何工作
  • 能说清楚预构建(Pre-bundling)的两个目的和实现方式
  • 能解释为什么生产模式选择 Rollup 而不是 esbuild
  • 能列举 Vite 和 Webpack 在开发模式、HMR、生产打包三个维度的核心差异
  • 能区分 Vite 插件的两类钩子(Rollup 通用钩子 vs Vite 独有钩子)
  • 能配置 Vite 的开发服务器代理、路径别名、环境变量等常见选项
  • 能解释开发模式和生产模式”双引擎”带来的行为不一致风险
  • 能说出 Rolldown 项目的目标和它对 Vite 未来的意义

购买课程解锁全部内容

大厂前端面试通关:71 篇构建完整知识体系

¥89.90