普通视图

发现新文章,点击刷新页面。
昨天 — 2026年5月23日技术

一文搞懂 Vite 处理CommonJS包、按需编译逻辑及 Rollup 插件兼容规则

2026年5月23日 18:26

💡 引言

在如今的前端面试和日常开发中,Vite 的名字可以说是如雷贯耳。大家都知道它“快”,知道它“基于原生 ESM”。

但是,如果面试官再往深处追问: “既然基于原生 ESM,那面对 npm 生态里堆积如山的 CommonJS 历史老包,Vite 是如何让浏览器不报错的?”、“它宣称的按需编译,到底在什么时候触发,编译了什么?”、“Vite 的插件为什么能无缝复用 Rollup 的生态?”

如果你对这些底层的组合拳还感到模糊,没关系。本文将用最纯粹、最直观的语言,为你彻底拆解这三大核心机制的幕后真相。

一、 破局历史包袱:Vite 如何处理 CommonJS 包?

核心结论: Vite 处理 CommonJS 包的核心逻辑是:开发环境通过 esbuild 预构建将 CommonJS 转化为 ESM;而生产环境则是通过 Rollup 插件将 CommonJS 转化为 ESM。

1. 开发环境:esbuild 依赖预构建

在本地开发阶段,Vite 遇到 CommonJS 包时的整体处理流程如下:

  • 依赖扫描:Vite 启动时会先扫描项目中的依赖,找出 CommonJS 相关的第三方包(例如:包的 package.jsonmain / exports / module 字段指向的文件是 CJS 格式,或者代码中含有裸导入)。
  • 格式重构:Vite 使用 esbuild 对其进行预构建,转换成 ESM 规范输出。在转换过程中,esbuild 会深度分析 require() 调用和 module.exports 对象,将其精准映射成 ESM 的 importexport 语法。
  • 写盘缓存:最终把转换后的产物输出成 node_modules/.vite/deps 下的缓存文件,供浏览器直接加载。

2. 生产环境:Rollup 插件化处理

生产环境 Vite 采用 Rollup 进行整体打包,此时处理 CJS 包的逻辑转移到了插件层,主要通过 @rollup/plugin-commonjs 插件来实现:

  • 全量扫描:Rollup 扫描项目中所有的依赖,精准识别未被预构建或代码中潜藏的 CJS 模块。
  • 静态转换:该插件将 CJS 的独有语法(如 module.exportsexportsrequire静态转换为标准 ESM 语法
  • 极致剪枝:转换完成后,进一步结合 Rollup 强大的 Tree-Shaking(树摇) 机制剔除无用代码,最终完美打包到生产产物中。

二、 极致性能的秘密:Vite 如何实现按需编译?

概念: Vite 按需编译的核心是基于浏览器原生的 ESMWebSocket 文件监听实现的。Vite 在开发阶段不会全量编译打包任何业务代码,只有当文件被修改或者首次请求时,才会触发编译,且编译粒度极致精准到单文件

[浏览器解析 import] ──> [发起标准 HTTP 请求] ──> [Vite 服务器拦截] ──> [实时单文件编译] ──> [注入内存缓存并返回]

1. “按需”的核心触发条件

浏览器原生 ESM 会自动解析代码中的 import 语句。每遇到一个未加载的模块,浏览器就会向 Vite 开发服务器发起一个新的 HTTP 请求。

  • 这意味着,只有在当前页面真正运行、并执行到该导入语句时,请求才会发出。
  • 内存缓存:编译后的文件会直接缓存到内存(Memory)而非磁盘中。后续如果发起相同的请求,Vite 将直接返回内存缓存,完美避免了重复编译。

2. 哪些文件会触发编译?

文件的编译主要可以划分为以下两大触发场景:

触发场景 涉及的核心文件类型
首次请求触发 入口相关文件(如 index.htmlmain.js/main.ts)、业务代码文件.vue.tsx.jsx)、以及样式文件.css.less.scss)等。只要被页面 import,即刻触发。
文件修改触发 当本地文件发生改动时,Vite 监听并进行单文件重新编译,通过 WebSocket 精准推送更新。

三、 繁荣生态的基石:Vite 的插件体系规范

Vite 的插件体系并没有完全另起炉灶,而是选择直接继承了 Rollup 插件规范,并在此基础上扩展了一些 Vite 独有的专属钩子。

1. 完美继承:Rollup 核心结构与钩子

Vite 直接集成了 Rollup 插件的核心结构和生命周期,这使得开发者编写 Vite 插件的上手成本极低:

  • 插件结构:Rollup 插件是一个返回对象的普通函数,Vite 插件完全沿用该标准结构。

  • 核心构建钩子(Build Hooks)

    • resolveId:拦截并解析模块路径。
    • load:负责加载模块的具体内容。
    • transform:对模块代码进行核心转换(如将特定语法转为 JS)。
  • 核心输出钩子(Output Generation Hooks)

    • generateBundle:在生成产物、打包结束前修改产物内容。
    • writeBundle:在产物成功写入磁盘后进行后续处理。

💡 如何注册? 无论是 Vite 专属插件还是 Rollup 兼容插件,直接在 vite.config.jsplugins 数组中进行注册即可。

2. 特色增强:Vite 扩展的独有钩子

为了适应本地高效开发以及特有的开发服务器环境,Vite 额外扩展了以下专属生命周期钩子:

  • config:允许在插件内部修改或合并 Vite 的最终配置。
  • configureServer:用于配置开发服务器(如添加自定义中间件、拦截特定的路由请求)。
  • handleHotUpdate:专门用于自定义 HMR(热更新)的拦截与处理。

3. 与 Rollup 插件的兼容性如何?

  • 高兼容度:由于 Vite 内部的 build(生产环境打包)阶段仍完全使用 Rollup,大部分只使用 Rollup 核心构建钩子的插件,可以直接在 Vite 中无缝使用
  • 需额外处理的情况:如果某些特定的 Rollup 插件深度依赖了 Rollup 独有的早期生命周期钩子(例如 moduleParsed 模块解析完成钩子),由于 Vite 开发环境按需编译、不会全量解析的特性,这种插件在 Vite 中就需要进行额外的适配和处理。

📌 总结

Vite 的精妙之处在于既尊重历史,又拥抱未来。它通过双引擎(esbuild + Rollup)天衣无缝地抹平了 CommonJS 的历史鸿沟,借浏览器之手实现了真正的按需编译,同时近乎完美地继承了 Rollup 的庞大生态。理解了这三套组合拳,你在面对任何构建优化问题时都能游刃有余。

Vite 开发预构建机制详解,搞懂 esbuild 与 Rollup 分工差异

2026年5月23日 17:43

💡 引言

在传统构建工具(如 Webpack)统治的时代,项目一旦变大,本地开发启动和热更新往往需要数秒甚至更久。Vite 凭借“天生不用打包”的原生 ESM(ES Modules)机制横空出世,带来了毫秒级的极致体验。

然而,很多同学在刚接触 Vite 时都会有一个疑问:既然 Vite 宣称是不打包的构建工具,为什么在开发阶段启动时,终端里总会雷打不动地出现一行 Pre-bundling dependencies(依赖预构建)?

事实上,面对庞大复杂的第三方生态(node_modules),Vite 宁可破坏“不打包”的纯洁性,也要引入 esbuild 进行一次特殊的预加工。这既是 Vite 的妥协,也是它极其精妙的工程化智慧。本文将带你一层层剥开预构建的底层外衣,看透它的运行机制与破局之策。

一、 什么是依赖预构建?

概念: 依赖预构建是指 Vite 在开发阶段启动时,对第三方 node_modules 包进行一次“预加工”的过程。也就是利用基于 Go 语言编写的 esbuild 将第三方依赖文件转化为符合浏览器规范的 ESM 格式并进行打包合并。

核心作用:它解决了什么问题?

  1. 格式统一(兼容 CommonJS / UMD) :浏览器原生的 ESM 无法识别 CommonJS 或 UMD 格式的包。像 React 等依然采用传统格式的古董包,如果直接扔给浏览器会直接报错。预构建能将它们统一转化为标准 ESM 格式,解决了非 ESM 包无法被浏览器识别的问题。
  2. 减少 HTTP 请求数(提升页面加载速度) :部分 ESM 包(如 lodash-es)内部极其零碎,包含几百个小文件。如果直接在浏览器中加载,会触发几百个并发 HTTP 请求,直接卡死浏览器。预构建将这些相关依赖文件合并成一个或几个特定的 ESM 文件,突破了浏览器的网络并发限制,大幅提升了开发环境的页面加载速度。

二、 依赖预构建的四大核心流程

Vite 的依赖预构建是一个严密的流水线,整体流程分为四步:

[服务器启动] ──> 1. 缓存判断 (有效则直读)
             (缓存失效) │
               2. 依赖扫描 (esbuild 虚假构建)
                       │
               3. 依赖打包 (CJS转ESM/模块合并)
                       │
               4. 信息写盘 (_metadata.json)

1. 缓存判断(Cache Validation)

在服务器启动的一瞬间,Vite 首先会检查之前的预构建成果是否依然有效,以避免重复操作:

  • Vite 会查看 node_modules/.vite 文件夹是否存在,以及其中的 _metadata.json 元数据文件。
  • Vite 会根据 package.json 的 dependencies 依赖、包管理器的锁定文件(如 package-lock.json)以及 vite.config.ts 中的相关配置,计算出一个 Hash 值
  • 如果 Hash 值未变,Vite 直接跳过后续步骤,从本地磁盘读取缓存,实现秒级启动。

2. 依赖扫描(Dependency Scanning)

如果缓存失效或不存在,Vite 必须找出代码中到底用到了哪些第三方包,确定出一份精确的依赖清单(例如:vue, axios, lodash-es):

  • Vite 会从 index.html 入口开始,递归扫描所有的源代码(JS/TS/Vue/JSX)。
  • 扫描过程中,Vite 使用 esbuild 作为扫描器,根据代码中的 import 语句以及包含在 optimizeDeps.include 中的项,快速进行一次“虚假构建”。这次构建不生成最终代码,只为了捞出所有裸导入(Bare Imports)的依赖名称。

3. 依赖打包(Dependency Bundling)

这是预编译中最核心的一步。Vite 会调用 esbuild 对依赖清单进行格式转换和模块合并:

  • 极致速度:得益于 Go 语言编写的 esbuild,这一步比传统的 Webpack 快 10 到 100 倍
  • 格式转换:将 CommonJS 或 UMD 格式的包(如 React)转换为标准 ESM 格式,使浏览器能直接识别。
  • 模块合并:将 lodash-es 这种内部有几百个小文件的包,打包成一个或几个特定的 ESM 文件,突破浏览器并发请求限制。

4. 构建信息写入磁盘(Writing Metadata)

最后一步是将打包后的依赖存入 node_modules/.vite/deps 目录下,方便下次对比和加载,并更新元数据:

  • 元数据文件:更新 _metadata.json 文件。这个文件记录了文件的 Hash 值、源代码中的原始导入路径、以及预构建后的文件路径的映射关系
  • 路径重写:当服务器运行时,Vite 会拦截浏览器的请求,根据这份元数据将路径重写为指向 .vite/deps 缓存文件的路径。

💡 主动触发与强制刷新

  • 自动触发:开发中如果修改了 package.json 的依赖或 optimizeDeps 配置,Vite 会自动触发重新预构建。
  • 手动强制触发:如果需要手动强制重新预构建,可以删除 node_modules/.vite 目录,或执行 vite optimize 命令(也可以在启动时执行 vite --force)。

三、 深度拷问:为什么开发用 esbuild,生产用 Rollup?

这是大厂前端面试高频出现的经典架构题。Vite 采用双引擎架构(esbuild + Rollup),背后有着深层的工程化考量。

1. Vite 开发环境选择 esbuild 的原因

Vite 在开发环境选择 esbuild 主要是看中了其绝对的速度优势和开发效率

  • 编译速度极快:esbuild 是使用 Go 语言开发的,直接编译为机器码,执行效率比 JS 编写的构建工具快很多,通常快 10-100 倍。
  • 完美支持 ESM 转换:esbuild 可将 CommonJS、UMD 直接转化为 ESM,适合浏览器按需加载。而 Rollup 对 CommonJS 还需要使用插件 @rollup/plugin-commonjs,配置比较复杂,还会进一步降低速度。
  • 架构轻量化:esbuild 在这里仅关注依赖的转换,而不像 Rollup 需要完整的依赖构建图。
  • 极致的文件快译与低延迟 HMR:Vite 在开发环境基于原生 ES Modules 进行模块热更新(只更新被修改模块及其依赖,更新延迟低)。在这个过程中,esbuild 仅负责单个文件的快速转译(如 TS 转 JS),不涉及复杂的打包和全量依赖图重构。而相比之下,Rollup 需基于完整依赖图重新打包,热更新耗时久,并不适合高频更新的开发场景。

2. Vite 生产环境使用 Rollup 打包原因

既然 esbuild 这么快,为什么 Vite 生产环境依然选择 Rollup 打包?主要原因是生产环境构建需要更多的优化和兼容性支持。Rollup 在长期的工程化沉淀中拥有明显的应用优势:

  • 更强大的产物优化:Rollup 在生产环境下的 Tree-shaking(树摇) 、代码分割(Code Splitting)、代码合并的能力是远优于 esbuild 的,能生成体积较小的精简 bundle。
  • 灵活的拆包策略:Rollup 支持 manualChunks 和动态导入的优化策略,可极其灵活地拆分大型应用,减少首屏加载时间。
  • 庞大完善的插件生态:Rollup 的插件生态非常丰富,可以支持多种类型资源的处理(JS、CSS、图片、SVG、字体资源打包、打包产物压缩等),而 esbuild 的插件生态和定制化能力目前还较为年轻。
  • 稳健的浏览器兼容性:Rollup 对浏览器兼容性较好,能配合构建链轻松生成兼容旧版浏览器的代码,确保线上产物的绝对稳定性。

📌 总结

Vite 的双引擎架构是前端工程化的一种精妙平衡:在开发阶段,它追求极致的速度,因此用 esbuild 大刀阔斧地做快速转换;在生产环境,它追求最终产物的体积与兼容性,因此用 Rollup 慢工出细活。 这种“两头通吃”的策略,才成就了如今 Vite 无法撼动的统治地位。

❌
❌