核心机制:原生 ESM + 依赖预构建

本节目标

跑起 npm run dev 后打开页面,看 Network 面板,你会发现浏览器真的在请求 /src/main.js、/src/App.jsx 这样的「源码路径」——Vite 把它们即时编译成 ESM 返回。

但 node_modules 里的依赖通常是 CommonJS(CJS) 写的,浏览器不认 require。于是 Vite 在「首次启动」时用 esbuild(一个极快的 Go 写的打包器)把这些依赖预构建成 ESM,并缓存到 node_modules/.vite。这带来两个好处:

  1. 把「成百上千个小文件」合成「几个 ESM 文件」,避免浏览器请求爆炸;
  2. CJS → ESM 转换,让浏览器能 import。
// vite.config.js 中可干预预构建行为
import { defineConfig } from 'vite'

export default defineConfig({
  optimizeDeps: {
    // 强制预构建这些依赖(即使 Vite 没自动探测到)
    include: ['some-cjs-pkg'],
    // 跳过某些不需要预构建的依赖
    exclude: ['already-esm-pkg'],
  },
})
# 预构建产物被缓存;当你改了依赖或想强制重算时
# npx vite optimize   # 手动触发一次预构建
# 或删除 node_modules/.vite 缓存目录

一个直观示意:当你的代码写 import React from 'react',浏览器实际拿到的是 Vite 预先打包好的 /node_modules/.vite/deps/react.js(ESM 版),而不是去读 react 源码里的几百个 CJS 文件。

名词解释

依赖预构建(Pre-bundling):Vite 在启动时用 esbuild 把 node_modules 里的依赖(尤其 CJS)转成 ESM 并合并缓存,解决「浏览器不认 CJS + 请求过多」的问题。

esbuild:用 Go 编写的超快 JS/TS 打包与转译工具,Vite 用它做依赖预构建和 TS/JSX 转译,速度比传统工具快一个量级。

CJS → ESM 转换:把 module.exports/require 写法的包,改写成浏览器能 import 的 ESM 形式,是预构建的核心动作之一。

optimizeDeps:Vite 配置项,控制哪些依赖要/不要被预构建(include/exclude),以及预构建的 esbuild 选项。

课后练习

练习 1:预构建缓存放在哪?改了依赖版本后要怎么让它重新生效?

答案:缓存在 node_modules/.vite。改了依赖后删掉这个目录(或 npx vite optimize 重新预构建),Vite 下次启动会按新依赖重新生成,否则会用到旧的预构建结果。

练习 2:为什么预构建用 esbuild 而不是 Rollup?

答案:预构建追求「快」,esbuild 是 Go 写的、速度极快,适合「启动期一次性把 node_modules 转成 ESM」;而 Rollup 追求「产出最优」,适合生产构建。两者各司其职。

总结

Vite 的「快」由两个齿轮咬合:原生 ESM 让开发时免打包,依赖预构建(esbuild)把 CJS 依赖变 ESM 并合并缓存。理解这一点,你就不会被「为什么有的包要配 optimizeDeps.include」这类问题难住——本质就是「确保该预构建的被正确预构建」。下一节我们写第一份 vite.config.js,把入口、别名、静态资源配明白。