前端打包工具 (bundler) 是什么? 为什么要用?

2025年12月6日

💎 加入 E+ 成長計畫 與超過 800+ 位工程師一同在社群成長,並獲得更多深度的軟體前後端學習資源

前端构建工具 (build tool) 是什么? 为什么要用? 一文中,我们谈到在现代前端的构建工具中,有打包工具这个重要角色(如果对这两个工具的定义不熟,觉得混淆的读者,推荐回顾构建工具那篇文)。

在前一篇文章中我们谈到,现代前端开发之所以需要构建与打包工具,最主要的点是要平衡开发体验与代码优化。

对开发者来说容易维护的代码,不代表对机器来说是最优的。举例来说,多数 JavaScript 开发团队会选择用 TypeScript,透过类型减少错误、提高可维护性。但浏览器本身无法处理 TypeScript,所以就需要有工具把 TypeScript 转成浏览器可处理的 JavaScript。

又或者多数前端团队不会写像下面这种代码(这是被最小化后的代码),但是从机器的角度来看,这种代码不仅功能没问题,因为体积较小,所需的传输时间也会比较短。

function calculatePositiveSum(n) {
  let t = 0;
  for (let r = 0; r < n.length; r++) {
    const c = n[r];
    if (c > 0) t += c;
  }
  return t;
}

由于对开发者与对机器,理想的代码不一样,因此我们需要这类工具来协助转换。在前一篇我们谈了构建工具这个大的概念,在这一篇文我们会专注在构建中的打包这个环节,来谈打包究竟是什么、具体怎么打包。

JavaScript 的模块演进历史

在谈模块打包工具之前,想先澄清一个词汇用法,所谓的打包,更完整的说法是模块打包 (module bundling)。要理解这个概念前,让我们先回顾一下 JavaScript 的模块演进历史,藉此更具体地理解要被打包的模块,究竟指的是什么。

在软件程序中,所谓的模块是指"具有明确边界、可独立维护并可重复使用的代码"。把这个概念拉到 JavaScript 与前端开发来看,当我们在谈模块时,就是指可以把类别或函数拆成不同的文件,然后透过 exportimport 来重复使用。

然而,exportimport 的语法,虽然在今天通用,但不能被视为理所当然,因为在 JavaScript 刚被发明时,这个语法与机制并不存在。

JavaScript 是个在 10 天内被写出第一版本的语言,可想而知在最开始有许多东西并不存在于这个语言中,原生的模块化机制也是。在最开始,假如想要把 JavaScript 拆成不同文件,最简单的做法就是直接拆,然后在前端的代码放入多个 <script> 标签。

但这样做有几个显而易见的问题。首先,这样做会让所有的 <script> 标签的东西都变成全局的,而全局意味着有冲突的可能。举例来说,如果两个 <script> 标签所引入的 JavaScript 文件中,有同样的变量名称,就会相互干扰。

所以在当时,社群中为了做到能把代码模块化,有开发者写了开源的套件,进而延伸出不同的标准。在 2009 年 Node.js 异军突起时,因为 Node.js 采用 CommonJS(简称 CJS)这个标准,所以当时 Node.js 相关的项目都是用 module.exports 搭配 require 的方式来输出与引用模块(现在许多有一定年纪的 Node.js 代码库,可能还看得到这种语法)。

然而 CommonJS 的语法除了浏览器不支持,因为是同步加载的,所以如果用 require 引入,就会导致浏览器必须等到加好该模块后,才会继续往下处理其他任务,这将导致如果引入的东西多,浏览器就会被长时间卡住,相当不理想。所以当时浏览器端的开发者,更多会用 Asynchronous Module Definition(简称 AMD)这个标准。

直到 2015 年 ES6 中,JavaScript 才有了原生支持的模块系统,而这个原生支持的又被称为 ES Modules(简称 ESM),也就是现在多数人熟知的 exportimport

透过这样的方式,今天假如想要使用某个第三方套件,例如很常会被用的 lodashdebounce 函数,就可以直接 import debounce from 'lodash',不需要自己重新造轮子。

打包 (bundling) 是什么? 为什么打包很重要?

上一段落我们谈了 JavaScript 当中的模块系统,相信这时读者会问,有了 ESM 后,为什么还需要打包呢? 要回答这问题,得先理解什么是打包,或者说什么是"模块打包"。

所谓的模块打包,就是把不同的模块包在一起;换句话说,是把多个 JavaScript 文件,包成数量比较少的 JavaScript 文件。

以上一段提到的,假如在某个网页应用的代码中,有引入 lodash 这个常见的效用函数库。例如在代码最上方有写 import debounce from 'lodash',虽然对开发者来说,不需要自己去写 debounce 这个函数,但对于浏览器在跑代码来说,还是需要实际有那一段代码。

这时问题就来了,浏览器不会直接去 npm 下载 lodash 这个套件,那要怎么拿到那段代码来跑?

打包就是在解决这个问题。假如今天使用者进到的页面,除了开发者自己写的代码外,还用了 lodash 这个套件,这时打包工具会把 lodash 中的代码,跟开发者的代码包成同一个模块(例如最后打包出 bundle.js 这个带有原始代码,加上 lodash 代码的最终产物),这样浏览器在读取 bundle.js 时,就也会有 lodash 的代码在其中。

为什么打包很重要?

但为什么要把不同的模块包在一起?

模块化让开发体验比较好(因为可以轻松地 import 要用的东西,以及避免全局命名冲突),但从技术的角度来看,其实也可以完全不需要打包。举例来说,假如要用 lodash,可以先下载到项目中,然后浏览器要的时候,可以再透过 ./node_modules/lodash 之类的方式引用到 <script> 当中。

既然这样也行,为什么还要把多个文件,包成比较少数量的文件呢?

这是因为从性能的角度来看,发送一个 HTTP 请求来拿模块包,会比发送多个来的快(HTTP 请求有各类成本,例如 TCP 三次握手、TLS 的连接等等)。进一步说,即使现在 HTTP/2 可以同时发多个请求,仍是有 25 个并行请求的上限,所以如果模块的数量超过 25 个,势必要把多个整合成一个模块,进而减少请求数量,压低到 25 个以内。

先前在社群中,甚至有一个演讲 《How Your Bundle Size Affects The Climate》 在谈打包产物对气候的影响。演讲中谈到,在古早时代的网页,基本上都是静态网站不太需要 JavaScript,但到了今天,多数的网站不是静态的,而是动态的应用程序,所以需要的 JavaScript 代码,远比过往来得多。

当传输的 JavaScript 越多,就代表传输造成的碳足迹越多,同时浏览器要处理的时间也会越多,这中间多做的运算处理,都是额外的耗电,对地球生态带来负面影响。试想,假如一个每月有千万人次造访的网页应用,每次使用者访问时所需传输的 JavaScript 都能透过打包最小化,不仅会带给使用者更快速的使用体验,也对环境更有益,可说是一举两得。

阅读更多

如果你对前端打包工具的主题感兴趣,我们在 E+ 当中有更完整的版本,包含会讨论社群中热门的打包工具、如何实现打包。除此之外,E+ 也有更多跟前端工具链相关的主题文。

本文为 E+ 成长计划的深度内容,截取前三分之一开放免费阅读。欢迎加入 E+ 成长计划阅读完整版本(点此了解 E+ 的详细介绍

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們