适合人群:天天
npm run dev却说不清中间发生了什么、看到webpack.config.js里十几行rules就想关电脑的同学。
本篇是系列的番外。上一篇收官讲了怎么用 Vite 上线,这篇往下一层,把 Vite、Webpack 以及挂在他们身上的每一个”零件”——loader、plugin、esbuild、Rollup、Babel、PostCSS——各自是什么、谁调用谁、为什么存在,一次讲清。
一、npm run dev 之后,浏览器到底收到了什么
浏览器是个挑剔的用户,它只直接消费三种东西:HTML、CSS、JavaScript。而我们的工程源码活在另一个世界:.vue 单文件组件、TypeScript、SCSS,还有 node_modules 里 CommonJS 格式的 npm 包——这些一口都咽不下去。
Java 世界里这道鸿沟靠一堆工具填平:javac 把 .java 编成字节码,Maven 把 class 打包成 jar,DevTools 提供热重启。前端的等价物更极端——上面所有职责被塞进了同一个物种:构建工具(build tool)。Webpack 和 Vite 就是这个物种的两个代表。
它们每天替你干的活归并起来就是一条五段流水线:
- ① 源码形态多样:
.vue要拆成 render 函数,TS 要抹掉类型,SCSS 要展开成 CSS; - ② 转译编译:由各种”专用转换器”按文件后缀分发处理——这就是后面要讲的 loader 和框架编译插件的地盘;
- ③ 依赖图谱与打包:顺着
import语句递归收集依赖,把散落的模块组装成一张图,再切成块; - ④ 体积优化:压缩字符、tree-shaking(把没被引用的导出摇掉)、按路由切分包;
- ⑤ 产物输出:产出文件名带内容 hash 的 HTML/CSS/JS,直接扔给 nginx。
所以别再说”前端不需要编译”——前端只是把 javac + maven + 热部署压进了一个命令里。理清这张图,后面所有的名词都能各归其位。
二、Webpack 的四大件:entry、loader、plugin、chunk
Webpack 的世界观只有一句话:一切皆模块。JS、CSS、图片甚至 JSON,都必须通过某种方式变成 JS 模块,然后拼进同一张巨大的依赖图。
四个词就能装下它的全部核心概念:
entry / output —— 主干。入口声明依赖图的根,出口描述产物怎么落到磁盘。这两项之外的一切,都由下面两件套承包。
loader —— 模块级的转换管道。loader 是一条条挂在匹配规则上的转换函数:文件命中正则(比如 test: /\.scss$/),就按规则指定的链条挨个过一遍,前一个的输出是后一个的输入。要点是链式执行从右往左:
1 | // webpack.config.js —— 四大件齐活的最小示例 |
写 Java 的同学可以把它理解为 Servlet Filter 链:请求(文件内容)进来,过滤器一个个穿过,每个只干自己那一件事。
plugin —— 生命周期的插手者。loader 只能碰单个文件的转换,但构建过程还有很多粗粒度的时机:即将开始编译、某个 chunk 生成完毕、所有文件落盘之前……Webpack 把这些时机做成一组事件钩子(内核模块叫 Tapable),plugin 通过订阅钩子在任意阶段改变构建行为。HtmlWebpackPlugin(自动生成 index.html)、MiniCssExtractPlugin(把 CSS 抽成独立文件)、DefinePlugin(编译期注入常量)都是这个思路下的产物。类比很现成:Maven 的插件挂 phase,或者 Spring 的 BeanPostProcessor——容器留好的扩展位。
chunk —— 最终切分的”份”。整张依赖图不一定一次性塞给用户:每个入口至少形成一块,动态 import() 会单独成块,splitChunks 还能把公共依赖拆出来复用。块再经过渲染,就成了你 dist 目录里的那些 bundle 文件。
一句话总结这套设计:loader 决定”每个文件长什么样”,plugin 决定”整个构建怎么走”,chunk 决定”最终切成几份”。灵活性的代价是上手成本——几乎所有 Webpack 劝退现场,都发生在这四个概念的排列组合里。
三、Vite 的组合拳:一个 server + 两个引擎
到了 Vite 这里,零件清单看起来反而简单——因为它压根没发明自己的打包内核,而是组了一支现成工具的全明星队:
- dev server(开发期主角):起一个本地服务器,浏览器根据页面上声明的
<script type="module">直接来请求源码,请求哪个文件就即时转换哪个文件——开发期不打包; - esbuild(依赖预构建):Go 写的极速编译器,负责两件事:把 CommonJS 格式的 npm 包转成浏览器认识的 ESM;再把零碎的小模块预合成少数大文件,减少请求数。结果缓存在
node_modules/.vite,依赖没变就不重跑; - Rollup(生产期主角):ESM 静态分析的祖师爷,tree-shaking 的天花板,
vite build底下跑的就是它; - 插件体系:Rollup 插件大多可以直接用,外加少量 Vite 专属钩子(比如能在 dev server 上挂中间件的那种)。
那你可能要问:.vue 文件是从哪儿钻进来的?答案是 @vitejs/plugin-vue——Vue SFC 的编译逻辑以 Vite 插件的形态挂载,接替了 Webpack 时代 vue-loader 的位置。
1 | // vite.config.ts —— 很多项目从头到尾就这么几行 |
同样是让 .vue 进入构建,Webpack 要装 loader 并自己动手串链,Vite 一行插件解决——不是插件本身有多神奇,而是内核默认内置了 JS/CSS/TS 的处理管线,约定取代了配置。至于 dev 代理、env 注入、部署那几件事,第 07 篇和收官篇都已经打过照面,这里只负责把原理这块拼图补上。
四、开发期对垒:全量打包 vs 按需服务
把两家的开发模式放到桌面上对比,差异一目了然:
这不是简单的快慢问题,而是架构分岔。开发期的 Vite 根本不在流水线上干活,它只是个即时翻译官;改动生效速度与项目大小基本脱钩,几十个模块和几千个模块的体验一致。而 Webpack 每次冷启动都要把全图搬进内存,项目越大代价越高。
顺带这就解释了收官篇反复敲的那句话——“dev 正常不代表 build 正常”:Webpack 开发和生产用的是同一个内核,行为容易一致;Vite 两边换了引擎(esbuild/Rollup),两条路径的行为天然可能分叉,上线前的本地 preview 一步都不能省。
那 Webpack 过时了吗? 没有。用哪儿顺手,看这张表:
| 场景 | 更顺手的选择 | 一句话理由 |
|---|---|---|
| 全新 Vue 3 项目 | Vite | 官方脚手架默认项,秒级冷启动 |
| 存量大型 Webpack 工程 | 维持现状,或渐进迁移 Rspack | 千余模块的迁移成本高,Rspack 提供兼容 API 无痛提速 |
| 组件库 / npm 包 | tsup 或 Vite 库模式 | 产物要干净的 ESM + CJS,不需要 dev server |
| 手头脚本需要快速打包 | esbuild 直接命令行 | 一行命令、零配置 |
表里冒出来的两个新面孔值得单独点名:
- Rspack:字节跳动用 Rust 重写的 Webpack 兼容内核,提供了足以覆盖主流用法的一揽子 Webpack API——它是存量工程”原地提速”的过渡桥梁,配套脚手架叫 Rsbuild;
- Turbopack:Vercel 用 Rust 写的增量打包器,深度绑定 Next.js 生态——离了 Next.js 它基本不出场。
五、全家福:流水线每一环到底是谁
现在回到第一节那张五段流水线,每一站都能点到具体的名字——把它们按职责叠放起来,就是前端工具链的全景地图:
顺着图读一遍:
- 脚手架层负责开局:
create-vite给你拉好 Vite 模板,Rsbuild 是 Rspack 的高层封装,Next.js 则自带整套专属链路; - 打包内核层是地图中央的重工业:Webpack(老牌全能选手)、Rollup(ESM 摇树祖师爷)、esbuild(Go 系速度担当)、Rspack 与 Turbopack(Rust 双雄)、Rolldown(Rust 写的 Rollup 兼容内核,Vite 团队的下一代实验方向,可以用
rolldown-vite提前体验); - 编译转换层是干细活的:Babel 做语法级转译(AST 级改造,preset-env 负责按目标浏览器降级),SWC 是它在 Rust 世界的等价物;TypeScript 官方的
tsc是唯一带完整类型检查的编译器;CSS 这边,Sass/Less 是预处理语言,PostCSS 是个 AST 加工平台——autoprefixer 和 Tailwind 都是它的插件; - 产物交付层负责临门一脚:Terser 或 esbuild 做压缩混淆,分包策略决定网络请求数,sourcemap 服务排查,polyfill(core-js)兜底老浏览器。
有心的话你会发现,这张图一夜之间多了好多 Go 和 Rust 的名字。原因不难理解:构建是 CPU 密集型的海量小任务,JS 单线程天然吃亏;Go/Rust 二进制并行能力直接碾过去,就是数量级的差距。而在性能追平之后,新一轮竞争的焦点变成了兼容性——Rspack 兼容 Webpack 的 API 与插件生态,Rolldown 兼容 Rollup 的插件协议。谁兼容旧世界,谁继承旧世界的用户。
几个最容易混着叫的东西,单独钉一下:
- Babel vs tsc:大多数打包场景里,TS 的类型检查交给 IDE 和
vue-tsc,构建期只需要”剥掉类型”,esbuild 顺手就做了;Babel 的不可替代之处在于旧语法定制转译和精细的浏览器降级目标,没这些需求就没必要引入; - PostCSS vs Sass:不是对手,是上下游——常见搭配是先写 SCSS,编译出的 CSS 再过一道 PostCSS 加前缀、删注释;
- Terser vs esbuild:压缩本质是字符串变换,纯粹看速度,Vite 生产打包的压缩器默认就能切给 esbuild。
六、高频坑清单
- loader 写了没生效:先查顺序——
use数组从右往左执行。把sass-loader放到最左边,等于让它去解析上一环节输出的 JS,链条当场断裂。 - 给 Vite 装 Webpack 插件(或反过来):两家的插件协议互不相通;连 Rollup 插件也要看钩子适用档位,有些钩子只在生产打包期触发,dev 下静默跳过——“配置写了却没反应”时优先怀疑这个。
- tree-shaking 摇掉了有副作用的引入:
import './reset.css'这类”只为产生效果”的导入可能被静默删除。在 package.json 的sideEffects里给这些路径留白名单。 - Babel 转了语法,API 还是红的:箭头函数换 ES5 是”语法”归 Babel 管;
Promise、Array.prototype.includes是”API”,得靠 core-js polyfill。esbuild 和原生 ESM 方案都不做 polyfill——browserslist 的目标里真有老浏览器就必须显式补。 - 缓存既是朋友也是雷:
node_modules/.vite的预构建强缓存,升级依赖后页面还跑旧实现时,vite --force清掉重来;Webpack 侧的同款陷阱是持久化缓存目录,表现为”我明明改了为什么没变化”,删缓存重建永远是最快的止损手段。
七、总结
- 构建工具是一条五段流水线:转译 → 依谱打包 → 优化 → 出产物,每个名词都能在图上找到位置;
- Webpack 靠四大件打天下:entry/output 定主干,loader 模块级转换(Filter 链),plugin 靠 Tapable 钩子全程插手(Maven 插件),chunk 管最终切片;
- Vite 没有自研内核,而是 dev server + esbuild 预构建 + Rollup 生产打包的组合拳,插件体系与 Rollup 血缘相通;
- 开发期哲学不同造成了”dev 正常不代表 build 正常”——Vite 双路径换引擎,上线前 preview 必跑;
- 工具层正在换代:Rust/Go 化是性能牌,兼容性才是入场券——使用者真正该掌握的不是某家配置,而是每类零件的职责边界。
系列目录:1. 组件生命周期 → 2. 模板语法与常用指令 → 3. 响应式系统 → 4. 组件通信 → 5. Vue Router → 6. Pinia → 7. Axios 联调 → 8. Vite 工程化与上线(完结)|番外:前端构建工具全景(本篇)