完整的 JavaScript 模块打包器指南

更新于 2026-01-23

Diego Salinas Gardón 2022-03-21

JavaScript 世界在过去几年中发生了巨大变化。手动在每个页面上引入 jQuery、Bootstrap 和 React 的时代已经一去不复返了。如今,一切都趋向于将所有内容打包成一个静态文件,只需一行代码即可加载。

模块打包器(Module bundlers)是将多个 JavaScript 代码文件组织并合并为一个文件的工具。当你项目的规模大到无法塞进单个文件,或者你使用了具有多重依赖关系的库时,JavaScript 打包器就派上了用场。在本篇博客文章中,我们将详细探讨打包器的作用及其工作原理。

什么是 JavaScript 模块打包器?

打包器是一种开发工具,它将多个 JavaScript 代码文件合并成一个可在浏览器中直接加载的生产就绪文件。打包器的一个强大功能是在遍历你的源代码文件时自动生成依赖图(dependency graph)。这意味着,从你指定的入口文件开始,模块打包器会追踪你的源文件及其第三方依赖项之间的关系。这种依赖图确保所有源代码和关联代码文件始终保持最新且无错误。

你可以想象,在打包器出现之前,这一过程有多么复杂。保持所有文件及其依赖项同步更新是一项对 Web 开发者而言极其艰巨的任务。

以一个简单的 JavaScript CRUD(创建、读取、更新、删除)应用为例,比如一个购物清单应用。在没有打包器的时代,你可能会将这些功能分别写在不同的 JS 文件中。你甚至可能想通过引入第三方库让你的应用更“花哨”一些,这就需要在页面加载时发出多个请求,如下所示:

image

然而,使用打包器后,这些文件及其依赖项会被合并成一个单一文件:

image

假设你正在开发或维护一个大型应用,比如一个面向数千用户、提供成千上万商品的电商网站。在这种场景下,你很可能需要使用自定义或第三方库来处理某些更复杂的任务。如果不使用 JavaScript 模块打包器,要让所有依赖项保持最新版本将是一项极其耗时的工作。

除了提供一致的工具环境以避免依赖管理的痛苦之外,许多流行的模块打包器还具备性能优化功能。例如代码分割(code splitting)和热模块替换(hot module replacement)。JavaScript 打包器还提供提升开发效率的功能,如强大的错误日志记录,帮助开发者轻松调试和修复错误。

打包器是如何工作的?

在讨论了打包器是什么以及它们在当今 Web 开发生态系统中的重要性之后,让我们来看看这些依赖管理工具具体是如何工作的。总体而言,打包器的操作分为两个阶段:依赖图生成最终打包

构建依赖图

模块打包器首先要做的是生成所有被服务文件之间的关系图。这个过程称为依赖解析(Dependency Resolution)。为此,打包器需要一个入口文件——理想情况下就是你的主文件。然后它会解析这个入口文件,以理解其依赖关系。

接着,它会遍历这些依赖项,进一步确定这些依赖项自身的依赖项。听起来有点绕,对吧?在此过程中,它会为每个遇到的文件分配唯一的 ID。最后,它提取所有依赖项并生成一个依赖图,描绘出所有文件之间的关系。

为什么这个过程是必要的?

  • 它使模块能够构建依赖顺序,这对浏览器请求函数时正确加载至关重要。

    return { id, filename, dependencies, code };
    
  • 它防止命名冲突,因为 JS 打包器拥有所有文件及其依赖项的良好源码映射(source map)。

  • 它能检测未使用的文件,从而帮助我们移除不必要的文件。

打包(Bundling)

在依赖解析阶段接收输入并遍历完所有依赖后,打包器会输出浏览器可以成功处理的静态资源。这个输出阶段称为打包(Packing)。在此过程中,打包器会利用依赖图整合我们的多个代码文件,注入所需的 functionmodule.exports 对象,并返回一个可由浏览器成功加载的单一可执行包。


五大 JavaScript 模块打包器

既然我们已经讨论了 JavaScript 模块打包器的重要性及其工作原理,你可能会想知道哪种打包器最适合你。JavaScript 生态系统中有许多不同的模块打包器,每种都有其独特的打包方法。接下来,我们将介绍 JavaScript 生态中最流行的五种模块打包器,探讨它们的工作方式、优点和缺点。

Top 5 JavaScript module bundlers npm package download graph

Webpack

Webpack logo

Webpack 每周下载量超过 1800 万次,GitHub 上获得 6 万颗星,是目前最受欢迎的 JavaScript 模块打包器。作为一种静态模块打包器,它不仅功能强大且高度可定制,不仅能打包 JavaScript 文件,还能转换、压缩和优化各种类型的文件资源和资产。不仅如此,Webpack 还拥有非常丰富的插件(plugins)和加载器(loaders)生态系统。

它是如何工作的?

和所有现代 JavaScript 打包器一样,Webpack 从构建依赖图开始。要理解它是如何执行依赖解析步骤的,你必须先掌握六个关键概念:

  • Entry(入口):指定 Webpack 应从何处开始构建其依赖图。根据你的应用架构,你可以有一个或多个入口点。Webpack 会遍历 webpack.config.js 配置文件中列出的模块,识别入口点的直接和间接依赖。

    module.exports = {
      entry: './app/index.js',
    };
    
  • Output(输出):指定 Webpack 完成打包过程后最终输出的目标位置。Output 属性包含两个子值:文件路径(通常为 /dist 文件夹)和期望的文件名。

    const path = require('path');
    module.exports = {
      entry: './app/index.js',
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'webpack-app.bundle.js',
      },
    };
    
  • Loaders(加载器):允许 Webpack 转换和打包非 JS 文件。

  • Plugins(插件):允许 Webpack 执行更高级的操作,如自定义资源优化和管理。

  • Mode(模式):允许 Webpack 动态配置其操作为生产模式或开发模式。

  • Browser Compatibility(浏览器兼容性):允许 Webpack 构建支持现代和旧版浏览器的包,包括 Promise 和 polyfills 等特性。

在创建内部模块映射后,Webpack 会使用函数包装相关模块,将所有内容捆绑在一起,由一个名为 webpackStart 的单一运行时函数调用。

入门非常简单,只需运行 npm i webpack

优点

  • 多资源支持
    除了对 JS 文件提供开箱即用的支持外,Webpack 还依靠其丰富的插件生态系统来打包 CSS、图片等其他文件。

  • 资源优化
    诸如代码分割(code-splitting)等功能可将代码文件拆分为多个块,从而减少加载时间。热模块替换(Hot Module Replacement)帮助你在不完全重载浏览器的情况下管理模块。开发者还可以使用加载器预处理文件,从而加快应用运行时速度。这些高度可定制的优化功能使 Webpack 成为最流行的 JS 打包器。

  • 开发者生产力
    在处理像模块打包这样复杂的任务时,开发者需要:

    • 详尽的文档;
    • 可依赖的第三方工具生态系统;
    • 高效的错误调试流程。

    Webpack 通过提供庞大的插件和加载器生态系统,以及基于 source map 的调试功能,满足了这三项需求。此外,Webpack 还拥有内部缓存系统,帮助开发者快速构建应用。

缺点

  • 复杂
    Webpack 的强大是一把双刃剑,许多开发者对其又爱又恨。它非常复杂,学习曲线陡峭。

  • 易出错且速度慢
    Webpack “全功能集成”的方法有时会导致应用集成过度工程化。过度依赖插件执行简单功能可能导致打包器变慢,需要技术调试才能优化。


Browserify

Browserify logo

Browserify 是一个开源的 JavaScript 打包器,允许你打包 Node.js 文件以便在浏览器中运行。借助 Browserify,开发者可以在浏览器中使用类似 Node.js 的 require() 语法加载 npm 模块。最初发布于 2010 年,该打包器在开发者中取得了不错的成功,每周下载近 200 万次,GitHub 上拥有超过 1.3 万颗星。

它是如何工作的?

和其他 JavaScript 打包器一样,Browserify 在打包模块时也经过明确定义的阶段。首先是依赖图构建。在此阶段,Browserify 从指定的入口文件开始,递归地搜索你文件中的所有 require() 调用。每个 require() 调用都会解析为一个文件路径,而每个文件路径又会进一步被遍历以查找更多的 require() 调用。

在完整映射整个应用的依赖图后,它会创建一个自包含的包,其中包含已合并的文件,并映射到唯一 ID。值得注意的是,Browserify 还提供高级自定义选项,例如可以用哈希值替换这些 ID。

然后,你可以将最终的包放入单个 <script> 标签中供浏览器加载。使用 Browserify 非常简单,只需运行 npm i browserify(原文误写为 npm i webpack),然后对入口文件运行 Browserify:

$ browserify main.js > bundle.js

该打包器还提供一些内置选项,如 --debug--ignore-missing

优点

  • 简洁性
    对于功能较少的大多数应用,许多开发者认为 Browserify 完全满足需求。它提供直接的 npm 集成,允许你重用 Node 代码而无需原生 CLI。

  • 开发者生产力
    Browserify 最大的卖点是让你充分利用丰富的 npm 生态系统。它易于学习,文档优秀。此外,它还内置了自动构建系统,使模块构建快速简便。所有这些都提升了应用开发体验。

缺点

  • 不支持多资源类型
    与 Webpack 不同,Browserify 不提供多资产支持。你可以通过 Gulp 工作流绕过这个问题,但这会引入不必要的复杂性。

  • 缺乏高级管理功能
    Browserify 仅限于 Node.js 的 npm 生态系统,缺少强大的资产管理工具来优化模块,例如不支持动态加载。


Parcel

Parcel logo

Parcel 是一个“即插即用、零配置”的构建工具,允许开发者快速配置多资产(如 JS、CSS 和 HTML)模块以进行开发。它在 GitHub 上拥有超过 3.9 万颗星,是仅次于 Webpack 的第二受欢迎的 JS 打包器。

它是如何工作的?

Parcel 的打包过程包含三个步骤:

  1. 资产树构建(Asset Tree construction):在此阶段,Parcel 接收一个入口资产,并遍历该文件以识别所使用的依赖项,从而创建一棵类似于依赖图的资产树。
  2. 包树构建(Bundle Tree construction):在此阶段,资产树中的各个资产与其关联的依赖项组合,形成包树。
  3. 打包(Packaging):这是最后阶段,包树中的每个包都与其特定的打包器文件类型关联,并转换为最终的编译文件。

之后,你可以向 Parcel 提供一个单一入口资产。注意,Parcel 支持多个入口点。

入门只需运行 npm i parcel

假设你有一个简单的 HTML 模板:

<html>
  <body>
    <script src="./index.js"></script>
  </body>
</html>

你可以通过运行 parcel index.html 让 Parcel 构建该 HTML 文件。令人印象深刻的是,Parcel 不仅会编译指向的 HTML 文件,还会编译 HTML 中链接的 index.js

优点

  • 零配置
    Parcel 解决了 Webpack 和 Browserify 面临的配置问题,为开发者提供了快速 Web 开发所需的高性能架构。它还像 Webpack 一样支持多资产类型,可打包 CSS、HTML 和图片等非 JavaScript 资产。

  • 速度快
    Parcel 速度很快,提供优质的资源优化功能,如热模块替换和代码分割的懒加载。根据最新基准测试,Parcel 的打包速度为 9.98 秒,而 Browserify 为 22.98 秒,Webpack 为 20.71 秒。使用 Parcel 的内置缓存技术甚至可以获得更快的结果,基准时间为 2.64 秒。

缺点

  • 缺乏高级自定义能力
    作为一个高度“固执己见”的打包器,Parcel 非常适合中小型应用。然而,对于需要修改配置的复杂应用,使用它可能会很繁琐。在这种情况下,大多数开发者更倾向于使用 Webpack。

FuseBox

FuseBox logo

FuseBox 是一个开源的 JavaScript 和 TypeScript 打包器与加载器。它将 Webpack 的最佳优化技术融合到一个快速、轻量级的打包器中,提供丰富的 API 体验。

它是如何工作的?

FuseBox 的打包过程提供了一些默认配置,使得入门无需大量修改。

要开始使用,运行命令:npm i fuse-box。之后,你需要创建主配置脚本文件,通常命名为 fuse.jsfuse.ts。以下是一个示例代码片段,包含入口点、目标文件和所需模式:

import { fusebox } from 'fuse-box';

fusebox({
  target: 'browser',
  entry: 'src/index.tsx',
  webIndex: {
    template: 'src/index.html',
  },
  devServer: true,
}).runDev();

FuseBox 通过构建一个模拟依赖图的虚拟文件结构来启动打包过程。这些文件随后被输出并打包在一起。

优点

  • 出色的开发者体验
    FuseBox 采用最少默认配置的风格,初学者学习曲线平缓,可快速上手而无需太多配置。

  • 速度快
    它提供快速体验,得益于多项资产优化功能。例如,热模块替换(HMR)允许打包器在不完全刷新浏览器的情况下管理资产。它还拥有强大的缓存系统和内置代码分割(code-spilling),从而加快浏览器加载速度。

缺点

  • 多资产支持较弱
    FuseBox 专注于 JavaScript 和 TypeScript,对这两种文件提供内置支持。但处理 CSS 等其他文件需要集成 CSSPluginSassPlugin。由于它是一个较新的打包器,其生态系统不如 Webpack 成熟。

Rollup

Rollup logo

Rollup 发布于 2018 年,是一款新一代 JavaScript 打包器,其主要卖点是**摇树优化(tree-shaking)**功能,能够在打包前筛除未使用的资源,将单个小模块合并成更大的模块。凭借这一能力,它在开发者中获得了一定的关注,每周下载量超过 400 万次,GitHub 上拥有超过 2 万颗星。

它是如何工作的?

Rollup 使用主配置文件(通常命名为 rollup.config.js)来定义打包规范。接着,它分析入口文件,对依赖项进行排序并创建依赖顺序。在此解析过程中,也会执行摇树优化。最后,所有在指定模块中遇到的声明函数会被编译到一个单一的全局作用域中,同时注意潜在的命名冲突。

要开始使用,运行 npm i rollup 安装 Rollup。你可以通过 CLI(配合配置文件)或通过打包的 JavaScript API 执行打包过程。

以下是一个示例配置文件,包含入口点、输出文件目标和格式类型:

export default {
  input: 'src/app.js',
  output: {
    file: 'bundle.js',
    format: 'cjs'
  }
};

与其他 JavaScript 打包器一样,Rollup 也支持多个入口点。

优点

  • 资源优化
    Rollup 提供丰富的资产管理功能,允许你对包进行代码分割以加快浏览器加载速度。还有摇树优化功能,帮助开发者移除不必要的变量或函数。

  • 原生 ES6 支持
    为了更好地在浏览器中共享导入和导出,JavaScript 的 ES6 版本引入了模块系统。Rollup 支持这一新的 ES6 模块系统,在保留现有 import/export 函数的同时,允许你将其转换为 CommonJS、AMD 等其他模块格式。

缺点

  • 开发者生态系统尚在成长
    新开发工具的成长痛点之一是构建完整生态系统所需的时间。虽然 Rollup 适合快速任务,但在开发大型复杂应用时,由于缺少所需功能的插件,开发者可能会感到失望。

荣誉提及:Vite.js

Vite.js logo

Vite.js 是一款新一代开源前端构建工具。Vue.js 创始人尤雨溪(Evan You)于 2020 年创建了 Vite.js,旨在利用最新的 ES 模块改进,解决以往打包器在构建性能方面遇到的问题。

目前,Vite.js 在 GitHub 上拥有超过 3.39 万颗星,每周下载量超过 34 万次。

它是如何工作的?

Vite.js 的一个独特之处在于它同时包含一个开发服务器(dev server)和一个打包构建命令(build command)。

  • 开发服务器会解析你的应用模块,并将其分为两组:

    • 依赖项(通常不频繁更新)使用 esbuild(一种比 Webpack、Rollup 和 Parcel 快得多的 JavaScript 打包器)进行预打包;
    • 应用源代码(需要频繁更新)则利用浏览器强大的 ESM 模块能力,按需提供而不进行打包。
  • 构建命令则使用 Rollup(我们前面讨论过的 JS 打包器)来打包你的代码。Vite.js 从入口点开始遍历代码库,将其转换为生产就绪的静态资源。和其他 JS 打包器一样,Vite.js 也支持多个入口点。

// vite.config.js
const { resolve } = require('path')
const { defineConfig } = require('vite')

module.exports = defineConfig({
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'index.html'),
        nested: resolve(__dirname, 'nested/index.html')
      }
    }
  }
})

优点

  • 轻量且快速
    通过利用原生 ES6 模块系统,Vite.js 能够通过减少浏览器请求数量来更快地提供应用代码。不仅如此,Vite.js 还内置热模块替换(HMR),使编辑过程近乎即时。

  • 多框架支持
    Vite.js 是框架无关的,对许多流行的 JavaScript 框架(如 React.js、Vue.js、TypeScript 和 Preact)提供开箱即用的支持。最近的版本还集成了对 CSS 模块、预处理器和其他静态资产的支持。例如,你可以使用以下命令快速用 Vite 设置一个 Vue.js 应用:

    npm init vite@latest my-vue-app -- --template vue
    

    它还拥有丰富的插件生态系统,可利用 esbuild 和 Rollup 的插件生态,为开发者提供广泛的选择。

缺点

  • 依赖 ESM 模块
    Vite.js 严重依赖浏览器的原生 ESM 系统来实现其惊人的速度。这意味着在处理不支持这些新特性的旧版浏览器时,开发者可能会遇到问题。

结语

老实说,很难判断这些打包器中哪一个是“最好”的,因为每种都提供了可能适合你需求的独特功能。例如,如果你正在构建一个具有复杂功能的大型应用(如电商应用),并且希望完全控制配置,那么 Webpack 是一个绝佳选择。另一方面,如果你正在开发一个个人项目并且喜欢使用 TypeScript,FuseBox 可能更具吸引力。

无论你选择哪种方案,性能和开发工作流指标都应成为你的指南针。