Webpack 与 Vite:从 Loader / Plugin 到 Rolldown 统一引擎

前端构建工具面试里,"讲讲 webpack 的 loader 和 plugin"几乎是必考题,紧跟着大概率会追问"那 Vite 呢"。这篇笔记把这两个问题连起来讲清楚:webpack 的 Loader/Plugin 到底是什么、边界在哪;Vite 里有没有对应概念;以及 Vite 这几年在底层做了什么、为什么最近两个大版本(Vite 7、Vite 8)会成为构建工具圈的分水岭。

一、Webpack:先构建依赖图,再统一打包

理解 webpack 的 Loader 和 Plugin,得先理解它的两个核心对象:

  • Compiler:全局唯一,贯穿整个 webpack 生命周期,持有完整的配置信息。
  • Compilation:每一次构建(watch 模式下每次重新编译)都会创建一个新的 Compilation 实例,记录当次构建的模块、依赖图、chunk 和产物信息。

这两者都基于 Tapable ------一个专门做事件流的库------对外暴露各种 hook。Loader 和 Plugin,本质上就是挂在这套 hook 体系两个不同粒度上的扩展点。

Loader:单文件转换器

Loader 本质是一个 Node 函数,接收源码字符串(或 Buffer),返回转换后的代码(可以附带 source map)。它的作用范围是单个模块,感知不到整个依赖图,也不知道其他文件长什么样。

它要解决的问题是:webpack 原生只认识 JS,但项目里有 CSS、图片、TypeScript、Vue 单文件组件......这些"非 JS 语义"的资源需要先被转换成 webpack 能处理的 JS 模块。常见的有 babel-loader(ES6+ 转兼容语法)、ts-loadercss-loader/style-loadervue-loader,以及 webpack 5 里用内置 asset modules 取代的 file-loader/url-loader

执行顺序上,loader 数组是从右到左、从下到上链式调用的;另外每个 loader 还有一个 pitch 阶段,会先从左到右跑一遍,用来提前拦截或短路后续 loader。

ini 复制代码
// 一个最简单的 loader
module.exports = function (source) {
  const result = transform(source);
  return result; // 转换后的代码字符串
};

Plugin:贯穿全流程的扩展

Plugin 本质是一个带 apply(compiler) 方法的类,通过 tap/tapAsync/tapPromise 订阅 compiler、compilation 生命周期上的具体 hook。

它的能力边界比 Loader 大得多:启动、模块解析、chunk 划分、asset 生成、优化压缩......构建流程里几乎每个阶段都有对应的 hook 可以插入自定义逻辑。典型的比如生成 HTML 的 HtmlWebpackPlugin、抽取 CSS 的 MiniCssExtractPlugin、注入全局变量的 DefinePlugin,以及更重量级的 Module Federation。

Loader 做不到、只有 Plugin 能做的事情包括:重新划分 chunk、在打包结束后额外生成或修改产物文件、跨模块地注入信息。

javascript 复制代码
// 一个最简单的 plugin
class MyPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('MyPlugin', (compilation, cb) => {
      // 可以在这里读写整个 compilation 的产物
      cb();
    });
  }
}

一句话区分二者:Loader 解决"如何读懂一个文件",Plugin 解决"如何组织整条构建流水线"。

二、Vite:反过来,先启动,按需编译

Vite 的心智模型跟 webpack 正好相反。webpack 需要先把完整依赖图构建出来才能启动开发服务器,项目越大启动越慢;Vite 开发模式下利用浏览器原生支持的 ESM,浏览器请求哪个文件,Vite 才编译哪个文件返回------没有"打包"这一步,启动时间基本和项目里的模块总数无关。

依赖预构建:esbuild 上场的地方

第三方依赖体量大、更新频率低,而且很多还是 CommonJS/UMD 格式,没法直接被浏览器当 ESM 请求。Vite 用 esbuild (Go 编写,比纯 JS 工具快一到两个数量级)在启动时把 node_modules 里的依赖转成 ESM,并把一个包内部零散的模块合并成少数几个文件。

这一步解决两件事:CJS/UMD → ESM 的格式兼容;避免一个包内部几十上百个小模块被浏览器逐个请求形成瀑布。结果会缓存到 node_modules/.vite,依赖没变化时直接复用。注意这一步只处理 node_modules,业务代码不会被预构建。

Vite 插件:Rollup 接口的超集

Vite 的插件接口兼容 Rollup 的 resolveId/load/transform 等钩子,同时扩展了开发服务器专属的钩子:

  • configureServer:直接操作底层 connect 开发服务器实例,相当于中间件。
  • transformIndexHtml:转换 index.html
  • handleHotUpdate:自定义 HMR 行为。
  • apply: 'serve' | 'build'enforce: 'pre' | 'post':控制插件在开发/构建哪个阶段生效,以及执行顺序。
javascript 复制代码
// Vite 插件骨架
export default () => ({
  name: 'my-plugin',
  enforce: 'pre',
  transform(code, id) {
    // ...
  },
});

值得注意的是,Vite 并没有像 webpack 那样区分"Loader"和"Plugin"两套体系------单文件转换(对应 Loader)和全流程扩展(对应 Plugin)都是同一个插件对象上的不同钩子。

生产构建:Rollup,以及正在替换它的 Rolldown

开发环境不打包,但线上仍然需要产物。传统上 Vite 的生产构建交给 Rollup ------tree-shaking 彻底、输出干净。从 Vite 7 开始,官方推出了可选的 rolldown-vite 包,把 Rolldown(Rust 编写、兼容 Rollup 接口的打包器)作为可插拔的替代品;到 Vite 8,Rolldown 已经成为默认打包器。这段演进值得单独展开讲。

三、放在一起看

维度 Webpack Vite
单文件转换 Loader(链式函数,返回转换后源码) 插件的 transform 钩子(Rollup 风格)
全流程扩展 Plugin(订阅 Tapable hook) 插件的其余钩子(configureServertransformIndexHtml 等),不单独区分 Loader/Plugin
底层事件机制 Tapable(tap / tapAsync / tapPromise) Rollup 风格的函数式钩子 + Vite 自定义钩子
依赖图与构建产物 Compiler(全局)+ Compilation(每次构建一个实例) 开发模式无完整依赖图打包,只维护模块依赖关系用于 HMR 失效传播;生产构建时依赖图由 Rollup/Rolldown 管理
开发服务器启动 先构建完整依赖图再启动,项目越大越慢 先启动,请求到哪个文件才编译哪个,启动时间与模块总数基本无关
第三方依赖处理 和业务代码一起进入同一套打包流程 esbuild 预构建,转 ESM + 合并小模块,与业务代码路径分开

四、Vite 版本演进:从"双引擎"到"统一 Rust 引擎"

这条时间线的主线,是"开发用 esbuild、生产用 Rollup"这套务实的双引擎组合,如何被同一批 Rust 工具逐步统一替换。

  • v1(2020.12) :面向 Vue 单文件组件的实验性开发服务器,与 Vue 强绑定,还不是通用构建工具。
  • v2(2021.02) :架构重写,插件接口对齐 Rollup,变成框架无关的通用工具;开发阶段引入 esbuild 做依赖预构建与 TS/JSX 转译。今天所说的"Vite 架构"基本从这一版定型。
  • v3(2022.07) :优化配置解析与 base 路径处理,dev/build 行为进一步对齐,SSR 能力增强。
  • v4(2022.12) :升级 Rollup 3,构建性能与产物体积优化。
  • v5(2023.11) :升级 Rollup 4(核心 parser 用 Rust 重写以提速),要求 Node 18+,为后续多运行时能力打基础。
  • v6(2024.11) :引入实验性 Environment API,把"浏览器 client / SSR / Edge runtime / Worker"抽象成可插拔的多个 environment,为非浏览器运行时(比如 Cloudflare Workers)提供统一的构建心智模型。
  • v7(2025) :推出可选的 rolldown-vite 包,把 Rolldown 作为可插拔替代打包器;新增 buildApp 钩子支持跨 environment 的插件协作;默认浏览器 baseline 提升(Chrome 87→107 等),发布形态改为纯 ESM,要求 Node 20.19+/22.12+。
  • v8(2026) :Rolldown 转正为默认且唯一的打包器,取代"dev 用 esbuild、build 用 Rollup"的双引擎架构;与 Oxc 工具链(同一批 Rust parser/transformer)对齐,官方宣称构建速度提升 10--30 倍,同时保留对现有 Rollup/Vite 插件的兼容层。

五、为什么会走向统一:更长的技术脉络

把版本号背后的动机连起来看,会更清楚"Vite 8 默认切到 Rolldown"这件事在解决什么问题。

起点问题:JS 生态的构建工具长期是"用 JS 写 JS 工具"------webpack、Rollup 早期实现都是纯 JS,大规模 AST 遍历和字符串处理天然受制于 JS 本身的性能。

第一波系统语言重写:esbuild(Go,Evan Wallace)率先证明了用系统语言重写 transform/bundle 热点路径能带来数量级的性能提升;随后 swc(Rust)、Rspack(Rust,字节跳动出品,尽量兼容 webpack 的 loader/plugin 配置)、Turbopack(Rust,Vercel/Next.js)相继出现,走的是同一条路:不改变现有心智模型,替换掉性能瓶颈的执行引擎。

Vite 2 的务实选择:dev 用 esbuild(重启动速度、转译够用),build 用 Rollup(重产物质量、tree-shaking 更彻底)------本质是在当时 Rust/Go 工具链还不够成熟、esbuild 又不追求做到 Rollup 级别打包质量的情况下,用两套引擎覆盖两种场景的折中方案。代价是 dev 和 build 走两套转译/打包语义,长期存在"esbuild 转译结果和 Rollup 打包结果不完全一致"导致的边缘 bug。

走向统一 :Vite 作者 Evan You 创立 VoidZero,目标是用一套 Rust 工具链同时覆盖 dev 和 build 两个场景------Oxc 负责 parser/transformer/linter,Rolldown 负责打包,二者共享同一套底层设施。Vite 7 的 rolldown-vite 是过渡期的可选项,Vite 8 让它成为默认,标志着 Vite 从"胶水层 + 双引擎"演进为"自有 Rust 统一引擎 + 插件生态兼容层"。

和 webpack 生态的呼应:webpack 核心至今仍是 JS 实现,但 Rspack 在做的是相同方向的事------保留 webpack 的配置项、Loader/Plugin 心智模型,把执行引擎换成 Rust,尽量做到"改一行配置就能迁移"。两条生态路径(Vite→Rolldown、Webpack→Rspack)殊途同归,都是"接口不变、引擎换成系统语言"。


参考:Vite 7.0 is out!Vite 8.0 is out!

相关推荐
YHL2 小时前
🎯 Danci —— 用 AI 驱动开发一个全栈英语单词学习平台
前端·后端
平头哥~2 小时前
Day 20 _ 3D 翻卡_perspective 写错人,卡片就穿帮
前端·3d·css3·学习资料
小聪7082 小时前
elpis-core 前端 Webpack 工程化实践
前端
2601_953988072 小时前
Ricon组态实时监控 - 毫秒级数据可视化
前端·物联网·数学建模·信息可视化·架构·前端框架
V158897262013 小时前
从滴滴模式看机器人租赁:撮合型租赁平台开发源码的调度系统设计思路
linux·前端·机器人
szarron3 小时前
国产手持式频谱分析仪选型攻略:TFN RC系列 vs HTOOL SA8T频谱分析仪 专业参数对比(军工/路测/调试全覆盖)
开发语言·前端·状态模式
开开心心就好3 小时前
免费桌签打印工具,支持批量导入名字
前端·javascript·人工智能·docker·jupyter·智能手机·语音识别
涛涛ing3 小时前
2026 年 9 月,整个 npm 生态的「心脏」都被 Rust 换了
前端
IT_陈寒3 小时前
明明设了默认值,为什么我的JavaScript函数参数还是undefined?
前端·人工智能·后端