构建工具:从源码到可加载资源
构建成功只说明命令没有报错,不能证明用户拿到了正确应用。源码还要经过配置解析、模块查找、语法转换、依赖连接、代码拆分、资源生成和发布;浏览器或服务器随后按 URL、模块格式和运行时约定加载这些文件。任何一步出错,都可能表现为白屏、样式丢失、动态路由失败或线上堆栈无法定位。
这一部分用同一组输入观察 Babel、webpack、Rollup、Vite、esbuild、SWC、Rspack 和 Rolldown。目标不是选出一个全能工具,而是知道每个工具负责哪一段、产物怎样验证、迁移时哪些差异可以接受。
开始前需要具备什么
读者应理解 ESM、CommonJS、动态导入、浏览器脚本加载和 package exports,并维护过至少一份现代前端配置。若模块、HTTP 缓存或浏览器资源仍不熟悉,先回看 Web 基础 与 前端框架。
先分清三类任务
单文件转换、应用构建和库发布经常使用相似配置,却有不同完成条件。
单文件转换把一种 JavaScript/TypeScript 语法变成目标语法,可能处理 JSX、装饰器、模块形式和 Source Map。Babel、SWC 与 esbuild Transform 主要从这一层切入。
应用构建从 HTML、客户端入口或服务端入口建立完整模块图,生成 chunk、CSS、图片、字体、manifest 和运行时代码,还要支持开发服务器与更新。webpack、Vite、Rspack 和 esbuild Build 更常承担这类工作。
库发布面对的是另一个加载者。产物要声明公开入口、ESM/CJS 格式、external/peer、类型、CSS 和资源策略,并在干净消费者中安装验证。Rollup 与 Rolldown 更常用于这类静态 ESM 图和插件流程。
工具可以组合,但每一项责任只能有清楚的拥有者。若 Babel、TypeScript、SWC 和 bundler 同时转换同一种语法,或两条发布流程都生成 manifest,结果往往难以解释。
八篇文章怎样阅读
| 文章 | 主要职责 | 阅读重点 |
|---|---|---|
| Babel | AST 解析与 JavaScript 语法转换 | preset/plugin、targets、helper、polyfill 边界和 Source Map |
| SWC | Rust 实现的解析与转换 | target/env、插件、原生二进制、Babel 迁移和宿主集成 |
| esbuild | 快速转换、打包与嵌入 API | loader、platform/format、metafile、增量 context 和能力边界 |
| webpack | 应用模块图与丰富扩展体系 | resolver、Loader、Plugin、chunk/runtime、缓存和 Federation |
| Rspack | webpack 风格的 Rust 应用构建 | 兼容范围、内置能力、持久缓存和渐进迁移 |
| Vite | 开发服务器与生产构建工作流 | 开发请求图、依赖预处理、插件、SSR 和生产资源 |
| Rollup | 静态 ESM 图和库发布 | tree shaking、external、输出格式、插件与消费者验证 |
| Rolldown | Rust bundler 与 Vite 新内核 | Rollup 风格 API、阶段差异、宿主责任和迁移路径 |
如果问题是语法降级或 AST 插件,从 Babel、SWC、esbuild 开始;如果问题是开发更新、应用资源或历史配置,阅读 webpack、Rspack、Vite;如果要发布公共包,重点看 Rollup 与 Rolldown。
横向比较 按任务和迁移成本比较候选,阶段总结 给出从浏览器症状回溯到配置和产物的完整方法。
贯穿练习:一份输入,三种交付结果
共享示例位于 examples/build-tools。输入包含:
- 一个浏览器 HTML 与 TypeScript 入口;
- 一个按需加载的详情模块;
- CSS、图片、字体和 Worker;
- 一个带公开子入口的共享包;
- 客户端和服务端环境变量;
- 一个会在源码位置抛错的函数。
同一组输入分别生成浏览器应用资源、可安装库包和嵌入式转换结果。这样可以看清三类加载者的差异:浏览器依赖 HTML、CDN 和运行时代码;Node 与 SSR 关心模块格式和服务端边界;库消费者还会再次解析、tree shake 和打包公开入口。
从配置开始,而不是从按钮开始
构建器真正使用的配置可能来自命令行、模式、环境变量、工作目录、配置文件、框架插件和默认值。排查前先打印或保存最终配置,记录 Node、包管理器、锁文件、工具与插件版本、目标平台、入口和公开资源前缀。
环境变量要区分构建时公开值、服务端秘密和运行时配置。把 secret 交给客户端 define 或公开前缀,构建成功后也已经造成泄露。模式变化若影响模块条件、缓存或输出,同样要进入记录。
模块怎样找到真实文件
resolver 会综合相对路径、扩展名、alias、package exports、条件导出、symlink、平台和 external 规则。源码里同一段 import 在浏览器、Node、SSR、Worker、开发和生产条件下可能选中不同文件。
遇到“本地成功、CI 失败”或“开发正常、生产报错”,先比较真实解析结果、路径大小写、工作区链接和所选条件,不要用更多 alias 把差异盖住。公共包应在干净目录安装 tarball,避免工作区源码直连掩盖缺失文件和错误入口。
转换会不会改变语义
语法能被解析,不代表目标运行时能执行。转换还涉及 class fields、decorators、JSX、helper、模块互操作、目标环境和 polyfill。target 通常控制输出语法,并不自动提供 Promise、fetch 或其他缺失 API。
为高风险语法保留小型运行样例,分别在真实浏览器、Node 或 Worker 中执行。Source Map 也要从源码经过每次转换和压缩一路传递,最后用一个确定错误确认线上堆栈能回到原文件。
模块图怎样变成 chunk
入口、静态导入、动态导入、side effects、条件和插件共同决定模块图。tree shaking 依赖静态关系和正确的副作用声明;错误标记可能删掉注册代码或样式,保守标记又会让无用模块留在产物里。
chunk 数量本身没有好坏。共享过多会让首屏下载低频代码,拆分过细会增加请求、运行时代码和旧资源失败点。应从真实用户路径观察首屏、首次交互、低频功能、跨路由共享和缓存寿命,再决定拆分策略。
资源怎样到达浏览器
最终交付的不只有 JavaScript。HTML、CSS、图片、字体、Worker、manifest、Source Map 和运行时配置需要使用同一构建标识和资源前缀。子路径、多个 CDN、SSR、CSP、MIME 与缓存头都会改变资源是否可达。
发布顺序应是:先上传并验证新的 hash 资源,再切换引用它们的 HTML、SSR 或 manifest;旧资源在长会话和回滚窗口内继续保留。只回退源码而不恢复对应 HTML 和资源集合,无法让旧页面真正恢复。
开发更新和缓存怎样保持正确
开发服务器、watch、HMR 和持久缓存都通过复用工作提高速度。复用成立的前提是输入声明完整:源码、配置、插件、锁文件、目标、环境、生成文件和平台都会影响输出。
分别测试冷启动、热启动、源码修改、依赖修改、配置修改、失败后修复和清理缓存后的结果。热构建与干净构建若产生不同语义,应先视为正确性问题。HMR 还要验证旧监听、计时器和模块状态是否在 dispose 时清理。
怎样做一次可信比较
固定提交、锁文件、机器、CPU/内存限制、目标、入口、插件、缓存状态和输出策略。正确性先通过,再测首次启动、热修改、生产构建、内存、产物大小和用户任务。
一组可复查的记录至少包含:
- 精确命令、版本和最终配置。
- 模块解析轨迹或图数据。
- 产物清单、manifest 与资源 URL。
- 一个 Source Map 错误样例。
- 浏览器或 Node 中的真实运行结果。
- 冷、热与失效条件下的原始测量。
- 发布顺序和完整回滚步骤。
单台机器的一次耗时、一个 bundle 大小或“命令能跑”都不足以决定选型。工具差异只有改变用户加载、错误恢复、资源成本或团队维护责任时,才值得进入结论。
迁移时一次只换一个所有者
从 Babel 到 SWC、从 webpack 到 Rspack、从现有应用工作流到 Vite/Rolldown,都应先选择一个包、入口或路由。旧链和新链使用独立输出、缓存和资源前缀,对相同任务比较允许差异与阻断差异。
灰度期间保留回到旧链的开关。稳定后删除临时 alias、双构建脚本、兼容插件和过期资源路径,避免两套流程长期并存。迁移记录写清哪些事实保持不变、哪个阶段换了实现、如何验证、何时停止以及怎样恢复。
什么算学完
完成这一部分后,你应能从任意源码入口画出配置、解析、转换、模块图、chunk、资源生成、发布和运行时加载路径。看到资源 404、HMR 异常、类型与运行不一致、Source Map 错位或 CI 变慢时,也能找到第一个出错阶段,而不是从配置文件顶部开始随机修改。
下一步进入 JavaScript 运行时。那里不再讨论源码怎样打包,而是讨论这些产物进入 Node.js、Deno、Bun 或 Edge Runtime 后,如何被加载、调度、授权、观察和关闭。