前端捆绑器头脑风暴

2025-06-07

前端捆绑器头脑风暴

这是什么?

以下是我在过去一年中遇到的各种打包术语及其定义的集合。本文旨在介绍什么是前端打包器、它的作用是什么、它们存在的原因以及打包器使用的一些常用术语。本文不会
针对任何特定的打包器(例如 webpack、rollup、vite、snowpack 等),而是提供一些关于这些打包器的功能及其工作原理的背景信息。本文旨在为未来的自己提供参考,因为未来的我必然会忘记这些内容的大部分内容。

涵盖哪些内容?

为什么存在捆绑器?

捆绑器的存在是为了解决几个不同的问题,并且它们随着所解决的问题的发展而不断发展。

最初,打包器存在是为了解决 3 个问题(主要)

  • 级联
  • 最小化
  • 压缩(某种)

级联

连接是将多个文件合并为单个文件的过程。这一点非常重要,因为在 HTTP/2 之前,导入资源文件的网络成本明显更高,这意味着耗时更长。因此,为了提高性能,尽可能少地向最终用户发送资源文件至关重要。

最小化

压缩是指将文件压缩到尽可能小的过程。例如:将变量名缩写为简写、缩短函数调用、消除空格等等。

压缩

除了最小化之外,还有一个概念叫“压缩”。压缩是指使用某种压缩算法缩小文件大小,从而减少其整体大小的过程。

压缩有时也被称为“zipping”或“gzipping”。压缩的底层原理超出了本文的讨论范围,但它只是另一种减小文件大小的技术(请注意,使用 gzip 压缩的文件可以很容易地通过浏览器解压,而且解压后文件内的代码与压缩后的文件相同,这与
文件最小化不同)。

附加问题

随着时间的推移,开发者对打包工具的需求越来越大。他们希望使用可以“转译”为 JavaScript 的文件。开发者需要打包,而不是打包成一个庞大的文件。他们希望对文件进行“分块”或“代码分割”。随着 HTTP/2 连接多路复用的出现,打包
多个较小的文件实际上变得更具优势。

现在,捆绑器解决了这些额外的问题:

  • 源映射
  • 转译
  • 代码分割(分块)
  • 摇树(消除死代码)

由于上述主题相当深入,我们将在下文中介绍它们的具体内容。但首先,让我们回顾一下“连接”,或者换句话说,如何使用 JavaScript 在文件之间共享代码。

为什么我们有不同的导入语法?

如果您接触过 JavaScript,那么您一定见过类似
下面的内容:

require("module")
module.exports = {}
Enter fullscreen mode Exit fullscreen mode

然后你可能还看到了:

import "module"
export const x = {}
Enter fullscreen mode Exit fullscreen mode

并且一直想知道,到底有什么区别?

嗯,简单的答案是示例 1 使用“CommonJS”语法(也称为 CJS)

示例 2 使用“ES 模块”语法(也称为 ESM)

还有第三个模块定义,称为 UMD(通用模块定义),它利用 CommonJS。

简而言之,CommonJS 是 NodeJS 最初的导入语法。ES 模块是 ES 模块规范的一部分,该规范是浏览器定义的用于导入 JavaScript 文件的规范。UMD 的出现早于 ES 模块语法,后者试图猜测加载环境
并提供合适的文件共享。

本质上,UMD 旨在桥接 CommonJS 语法以便在浏览器中使用。需要注意的是,UMD 和 CJS 都早于 ESM 规范,这也是为什么尽管 ESM 是当时的标准,但它们仍然会存在。

在本文的其余部分,我们将主要关注 ESM 语法,因为它是标准,并且必须为每种可能的语法定义警告很累人。

什么是裸模块导入?

当我们讨论导入时,什么是“裸模块导入”以及它为什么特殊?

裸模块说明符是指你提供的是文件路径,而不包含“相对限定符”。例如,下面是一个裸模块导入:

import "jquery"
Enter fullscreen mode Exit fullscreen mode

现在,裸模块说明符的概念源自 NodeJS。当你不提供相对限定符时,Node 会自动查找你的“node_modules”目录。因此,上述内容大致可以理解为:

import "../node_modules/jquery"
Enter fullscreen mode Exit fullscreen mode

以上就是所谓的“相对模块说明符”,这意味着它被赋予一个“相对”文件路径来在系统中查找文件。

这很重要,因为 ESM 规范不支持“裸模块说明符”,这意味着开发人员需要做以下两件事之一来修复裸模块说明符:

A.) 设置导入映射来告诉浏览器在哪里找到模块。B
.) 将代码转换为相关模块。

选项 A 引入了“导入映射”的概念,导入映射是一个相当新的概念。导入映射的本质是“当你看到这个裸模块说明符时,这里是该模块的相对路径,这样你就知道在哪里可以找到它”。它本质上是提示浏览器如何解析裸模块。想要了解更多关于导入映射的信息,请查看 Modern Web 的导入映射文档。

https://modern-web.dev/docs/dev-server/plugins/import-maps/

选项 B 引入了“转译”的概念,我们将在讨论“加载器”时讨论它。

什么是入口点?

入口点是“bundle”的另一种说法。
入口点实际上可以有多种名称,例如在 Webpacker 5.0 以下版本中,它
被称为“pack”。虽然入口点可能有多种名称,但最终它会
告诉打包器“打包此文件”,换句话说,抓取
它导入的所有文件,创建所谓的“依赖关系图”,然后
创建一个打包文件(并且根据设置,还会创建“chunks”)。

你可能会问,什么是依赖图?依赖图本质上是打包器的一种方式,它能帮你找出“入口点”文件中有哪些包和文件,并将它们正确地打包到最终文件中。

这也引出了一个问题:“如果一个入口点导入了另一个入口点会发生什么?” 这就产生了所谓的“循环依赖”。换句话说,A 依赖 B,而 B 又依赖 A,那么谁会先被解析呢?

循环依赖也可能发生在常规包中,但通常可以通过捆绑程序解决,尽管一般建议尽量避免循环依赖。

https://spin.atomicobject.com/2018/06/25/circular-dependencies-javascript/

入口点的另一个概念是“加载器”或“转译器”通常会在这里执行其需要执行的操作。

什么是加载器?

加载器是打包器将非 JavaScript 文件转换为 JavaScript 兼容语法的一种方式。例如,假设我将 png 文件导入到 JavaScript 文件中。

import Circle from "./circle.png"

function render () {
  return `<img src="${Circle}">`
}
Enter fullscreen mode Exit fullscreen mode

实际情况是,如果你使用类似“Webpack”之类的工具,就会有一个叫做“加载器”的东西,它会把这个 png 图片转换成 JavaScript 兼容的对象,并允许你获取“圆圈”的最终位置,并将图片的 src 指向它。官方的 ESM 规范并不支持这种语法,而是
由打包器处理,允许用户在 JavaScript 文件中引用非 JavaScript 文件。

另一种需要“加载器”或“转译器”的文件类型是 TypeScript!假设我将一个 TypeScript 文件导入到一个 JavaScript 文件中。

import TSFile from "./tsFile"
Enter fullscreen mode Exit fullscreen mode

我省略了 ,.ts因为 TypeScript 本身不支持导入.ts文件。如果你.ts在浏览器中导入文件,它根本行不通。相反,打包器.ts会使用 TypeScript 转译器(或者编译器,随便你怎么叫)对文件进行转译,然后将其转换为
可用的 JavaScript 文件。

加载器、压缩器以及其他所有改变最终输出的工具,其重要之处在于,它们会掩盖初始代码的来源。为了解决这个问题,打包器实现了一个叫做“源映射”的东西。源映射是一种将转译后的代码映射到其原始源代码的方法。这对于追踪错误尤为重要,因为如果没有源映射,调试压缩/转译后的代码将非常困难。

既然说到这儿,现在正是讨论“目标”的好时机。“目标”的概念是告诉打包器“输出与此 EcmaScript (ES) 规范兼容的 JavaScript 语法,或输出与这些浏览器兼容的 JavaScript 语法”。

例如,您可能见过像这样写的目标:
targets: "es6"或者当针对浏览器时:
targets: "> 0.1%, not dead, not IE 11, supports-esmodules"

这是一种使用“现代” JavaScript 语法同时能够向后兼容旧版浏览器的方法。

关于“现代”这个话题,让我们继续讨论代码分割或分块。

什么是块?(代码分割)

一个块仅仅是从主包中分割出来的 JavaScript 文件。块的概念相对较新,是浏览器不断发展的产物。随着浏览器的发展,打包器也随之发展。浏览器对同时
下载资源文件的支持越来越好,因此在使用兼容 HTTP/2 的服务器时,多个较小的文件实际上可以提高性能。

让我们深入研究一下块是如何创建的。

创建代码块的方法有很多种。最常见的两种方法是“关键路径”代码拆分和“文件大小”代码拆分。

第一种分块形式称为“文件大小分块”,意思是“选择任意文件大小,并按该大小创建一个块”。例如,我们选择 20kb(因为 Webpack SplitChunks 插件就是用这个大小的https://webpack.js.org/plugins/split-chunks-plugin/)。这意味着我导入的任何
大于 20kb 的文件都会自动被转换成一个块。

第二种分块形式称为“关键路径代码拆分”,其含义是:

“仅首先导入最重要的渲染文件,然后在初始关键包加载后导入其他‘块’”。

这有助于让浏览您网站的人们实现更快的初始加载。

谈论关键路径代码拆分的另一种方式是“动态导入”。动态导入在运行时导入。静态导入和动态导入的区别如下:

import("mymodule") // => dynamic
import "mymodule" // => static
Enter fullscreen mode Exit fullscreen mode

当我们解释什么是 treeshaking 并谈论“静态可分析文件”时,这一点很重要。

什么是 treeshaking?

Treeshaking,也称为“死代码消除”,是打包器用来移除无用代码的一种方法。此过程容易出错,并且特定于您使用的打包器及其内部的 AST(抽象语法树)。

每个捆绑器实现 treeshaking 的方式略有不同,但核心概念如下:

要实现可摇树功能,文件至少应做到以下几点:

A.) 可静态分析
B.) 提供对导入的静态引用
C.) 不应有副作用

静态可分析意味着它不能使用插值字符串来导入文件。这里有一个例子

// Statically analyzable
import "file"

// Not statically analyzable
const file = "file" + Math.random.toString()
import(file)
Enter fullscreen mode Exit fullscreen mode

静态引用意味着你不能对对象使用“动态访问器”。这实际上不会影响 ESM,因为它有明确的“只抓取我需要的东西”的语法,但还是值得一提。例如:

// Treeshakeable!
import { onlyThis } from "large-module"

// hard to treeshake / possibly not treeshakeable (depends on bundler)
import * as Blah from "blah"

// Not treeshakeable
const x = require("blah")
x["dynamic"]()
Enter fullscreen mode Exit fullscreen mode

最后,让我们来谈谈副作用,下面将专门讨论副作用。

有哪些副作用?

副作用是指文件被
“导入”时运行的一段代码。如果你浏览过 Webpack 文档,你可能对副作用很熟悉。https ://webpack.js.org/guides/tree-shaking/#mark-the-file-as-side-effect-free

例如,让我们看两个文件:

// side-effect.js
class MyCustomElement extends HTMLElement {}
window.customElements.define("my-custom-element", MyCustomElement)

// entrypoint.js
import "side-effect.js"
Enter fullscreen mode Exit fullscreen mode

当我导入“side-effect.js”时,尽管导入时没有调用任何函数,代码也会自动运行。这使得打包器很难判断它是否可进行side-effect.js树形优化,因为即使用户没有实际执行导入操作,代码也会运行。因此
,带有副作用的文件通常很难进行树形优化,大多数打包器都不会尝试对其进行树形优化。

如果我想重写上述内容以使其“无副作用”,我会这样做:

// side-effect.js
class MyCustomElement extends HTMLElement {}

export function define() {
  window.customElements.define("my-custom-element", MyCustomElement)
}

// entrypoint.js
import { define } from "side-effect.js"
define()
Enter fullscreen mode Exit fullscreen mode

现在我们“没有副作用”了!还有最后一个话题要讨论,然后这篇参考就完成了!

什么是哈希?(指纹识别、摘要等)

文件哈希(也称为指纹识别或文件摘要)是分析
文件内容,然后生成一个“哈希值”并将其添加到文件末尾的过程。哈希文件的示例如下所示:

file.xj921rf.js(是的,这是一个虚构的哈希值)

哈希的大小(字符数)由你的打包器设置决定。数字越大,哈希值越“唯一”。唯一哈希值非常适合缓存,因为如果哈希值没有改变,
浏览器可以直接使用缓存的版本。哈希值应该是“幂等的”,也就是说,如果我运行同一个文件,内容相同,n 次,那么无论构建运行多少次,我都会得到相同的最终哈希值。这对于一致性至关重要。至此,我对自己的描述就结束了。

最后的想法

以上内容可能并非 100% 准确。这纯粹是我在过去一个多小时里凭空想象出来的。如果您有任何补充或更正,请随时提出。以上内容请谨慎看待。我只是一个人,而且我从未真正编写过打包工具。祝您拥有美好的一天,打包成功!

文章来源:https://dev.to/konnorrogers/frontend-bundler-braindump-10fj
PREV
Git 重置 --Hard git reset --explain
NEXT
为什么我们会收到那个愚蠢的 CORS 错误?