前端打包工具Esbuild--模块化、ESM、esbuild-loader、

模块化编程在前端领域已非常普遍,应用程序中将各种功能细分成独立的模块(单独文件)进行开发。module bundler 将所有文件串联起来变成了必须。

JavaScript 社区中有很多程序的打包工具,如 Webpack、Rollup、Parcle 等,它们都是使用 JavaScript 构建的,性能方面有很多不足。下面要介绍的 Esbuild,采用 Go 语言开发,运行速度得到了显著提高。

Esbuild 也被称为下一代构建工具(使用 Go 语言编写,基于 ESM)。

esbuild:An extremely fast JavaScript bundler
Our current build tools for the web are 10-100x slower than they could be. The main goal of the esbuild bundler project is to bring about a new era of build tool performance, and create an easy-to-use modern bundler along the way.

其定位为一款极快的 JavaScript 打包工具。“极快”是源于同当前市场上比较流行工具的对比(下图来自官方Github)。
前端打包工具Esbuild--模块化、ESM、esbuild-loader、
在 ESM 出现之前,在浏览器中运行 JavaScript 有两种方法:

第一种方式,引用一些脚本来存放每个功能;此解决方案很难扩展,因为加载太多脚本会导致网络瓶颈;
第二种方式,使用一个包含所有项目代码的大型 .js 文件,但是这会导致作用域、文件大小、可读性和可维护性方面的问题。总之,在浏览器支持 ES 模块之前,没有 JavaScript 的原生机制可以让开发者以模块化的方式开发。这是 webpack 等打包工具诞生的原因之一。

ESM 的出现后及相关主流浏览器的支持,ESM 模块成了首选,因为原生速度要于远远优于其他方式(不再需要引入额外的库来实现模块化)。

本文的重点是要讲述 esbuild,但在讲述之前,不得不提及ESM、Babel 和 Webpack中几个相关联的重要知识 。

ESM

Snowpack 是首次提出利用浏览器原生 ESM 能力的工具。开发过程中,Snowpack 为你的应用程序提供 unbundled server**。**每个文件只需要构建一次,就可以永久缓存。文件更改时,Snowpack 会重新构建该单个文件。在重新构建每次变更时没有任何的时间浪费,只需要在浏览器中进行HMR更新。

ESM 代表 ES 模块。这是 Javascript 提出的实现一个标准模块化解决方案。模块化的出现为了更好的维护代码的同时,对 scope(作用域)有了更好的管理,关于前端模块化的汇总,可以看这里这里

<script src="index.js" type="module"></script>

通过 type="module" 告诉浏览器,当前脚本使用 ESM 模式,浏览器会构建一个依赖关系图,借助浏览器原生的 ESM 能力完成模块的查找、解析、实例化到执行的过程。

这里也不再赘述 ESM 的使用方式及相关语法,重点介绍执行机制,详细内容可以看这篇

  1. Parsing(解析): 递归(深度优先后序遍历)的加载所有导入的模块,构建一个依赖关系图。每一个模块都只有一个module record,这也保证了每一个模块只会被执行一次。

  2. Instantiating(实例化): 对于每个新加载的模块,都会创建一个模块实例,并使用该模块中 export 的内容的 内存地址import 进行映射(导出的模块和导入的模块都指向同一段内存地址–实时绑定)。

  3. Evaluating(求值): 运行每个新加载模块的主体代码。

所有模块的静态依赖在该模块代码执行前都必须下载、解析并进行实例化。

前端打包工具Esbuild--模块化、ESM、esbuild-loader、
前端打包工具Esbuild--模块化、ESM、esbuild-loader、

浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入的代码,即只在当前屏幕上实际使用时才会被处理。

因此,引出了使用 ESM 最核心的两个特点:

1、构建复杂度非常低,修改任何组件都只需做单文件编译(不需要重新构建和重新打包应用程序的整个bundle),时间复杂度永远是 O(1)
2、借助 ESM 的能力,模块化交给浏览器端,不存在资源重复加载问题,如果不是涉及到 jsx 或者 typescript 语法,甚至可以不用编译直接运行

更加详细的,可以阅读 为什么选vite

Babel

Babel 是一个 JavaScript 编辑器,将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

这里详细的用法不赘述,相关可以看这里,AST相关看这里

重点介绍工作过程:

  1. 「Parse(解析)」:将源代码转换成更加抽象的表示方法(如抽象语法树
  2. 「Transform(转换)」:对(抽象语法树)做一些特殊处理,让它符合编译器的期望
  3. 「Generate(代码生成)」:将第二步经过转换过的(抽象语法树)生成新的代码

webpack

Webpack 的构建流程简单来说就是递归编译每一个模块文件,对于不同类型的文件使用不同的 webpack loader 进行处理。并可以自动构建并基于你所引用或导出的内容推断出依赖的图谱

Webpack 在很多方面处理的很好,特别是在大型项目中得到了实战测试,已成熟并且可以处理很多用例。

esbuild-loader 是一个构建在 esbuild 上的 webpack loader,且可以替代 babel-loader 或 ts-loader 来提高构建速度。

module.exports = {
    module: {
      rules: [
-       {
-         test: /\.js$/,
-         use: 'babel-loader',
-       },
+       {
+         test: /\.js$/,
+         loader: 'esbuild-loader',
+         options: {
+           loader: 'jsx',  // Remove this if you're not using JSX
+           target: 'es2015'  // Syntax to compile to (see options below for possible values)
+         }
+       },
        ...
      ],
    },
  }

esbuild

esbuild 重点提到的就是构建速度方面,为什么会比 webpack 快呢?而且不在同一个数量级。

  1. Go 语言与 JavaScript 语言差异(webpack 采用 JavaScript;esbuild 采用 Go)

    • Go 是为并行性而设计的,而 JavaScript 是单线程的
    • Go 在线程之间共享内存,而 JavaScript 必须在线程之间序列化数据
    • Go 可直接编译成机器码,不依赖其他库,必然比 JIT 快(JIT相关看这里
  2. 对构建流程进行了优化,充分利用 CPU 资源

    • 解析 => 链接 => 代码生成。解析和代码生成采用并行化
    • 当导入同一 JavaScript 的不同入口时,可以轻松共享(线程间共享内存)
  3. 尽量少做全 AST 传递以获得更好的缓存局部性(esbuild 中只有三次全量 AST pass)

    • Lexing + parsing + scope setup + symbol declaration
    • Symbol binding + constant folding + syntax lowering + syntax mangling
    • Printing + source map generation

前端打包工具Esbuild--模块化、ESM、esbuild-loader、

扫描阶段: 这个阶段从一组入口点开始,遍历依赖图以找到需要在包中的所有模块。这是bundler.ScanBundle()作为并行工作列表算法实现的。列表中的每个文件都在单独的 goroutine 上被解析为 AST,如果它有任何依赖项(ES6import语句、ES6import()表达式或 CommonJSrequire()表达式),可能会向工作列表添加更多文件。扫描继续,直到工作清单为空。

编译阶段: 这个阶段为每个入口点创建一个包,这涉及首先“链接”导入和导出,然后将解析的 AST 转换回 JavaScript,然后将它们连接在一起形成最终的 bundle。这发生在(*Bundle).Compile().

示例

index.js

import {add} from './math.js'

let result = add(1,2)
console.log(result)

math.js

const add = function (p1, p2) {
  return p1 + p2;
}
const sub = function (p1, p2) {
  return p1 - p2;
}
export { add, sub }

构建:

$ npx esbuild src/index.js --bundle

结果:

(() => {
  // src/math.js
  var add = function(p1, p2) {
    return p1 + p2;
  };

  // src/index.js
  var result = add(1, 2);
  console.log(result);
})();

默认输出 iife 格式,也可以通过 --format 指定其他格式输出(cjs、esm)

$ npx esbuild src/index.js --bundle --format=cjs
$ npx esbuild src/index.js --bundle --format=esm

针对 esm 格式,也支持 Tree Shaking

$ esbuild index.js main.js --bundle --splitting --outdir=dist --format=esm

main.js 内容同上述 index.js

import {add} from './math.js'
console.log(add(3,4))

结果

import { add } from "./chunk-2YHQ3R6P.js";

// main.js
console.log(add(3, 4));

chunk-2YHQ3R6P.js

// math.js
var add = function(p1, p2) {
  return p1 + p2;
};

export { add };

esbuild 构建默认开启了 Tree shaking(sub 并没有被编译输出),还可以进行 minify、 sourcemap,指定 platform ,以及 watch(监视模式)等等。具体可以查看官方 API

缺点

当然 esbuild 不是万能的,由于其为了保证编译效率,并没提供 AST 的操作能力,所以对一些处理 AST 的 plugin(如 babel-plugin-import) 暂时不能过渡到 esbuild 中。

总结

在当前前端环境中,直接使用 esbuild 代替 webpack 不现实;主流方案是在 webpack 中使用 esbuild 去做一些代码的 transform (代替 babel-loader)。当然随时后续 vite(采用 esbuild 预构建依赖) 、snowpack 等构建工具的发展,其会在某些场景下替代 webpack(vue3 的官方推荐构建工具为 vite)。esbuild 针对构建 应用 的重要功能仍然还在持续开发中 —— 特别是代码分割(可以获得最佳的加载性能)和 CSS 处理方面。当未来这些功能稳定后,不排除使用 esbuild 作为生产构建器的可能。

vite 中引用 esbuild: https://github.com/vitejs/vite/blob/main/packages/vite/src/node/optimizer/index.ts#L5

webpack 在 v5 版本中也是针对编译的性能做出了不少努力,除了提供了物理缓存的优化之外,还提供 Module Federation 的方案,这给我们上层的应用实践带来了很多想象的空间。以前 webpack 大有一统构建工具的趋势,而现在我们可以结合业务的特点有更多的选择。

最后

通过 ESM 构建,提到 esbuild,还有一个 swc;esbuild 采用 go 语言编写,而 swc 采用 rust 语言编写。他们的速度都比目前市面上成熟的打包工具要快太多,带来性能提升的关键是底层编写语言的天生特性导致。关于这个话题esbuild为什么不用Rust,而使用了Go? 在知乎上有专门讨论,感兴趣的小伙伴可以查看一下。

上一篇:002-Android使用APUConfig配网绑定ESP8266并通过阿里云物联网平台和ESP8266实现通信控制


下一篇:vue 动态生成按钮,@click 绑定方法名称,handler.apply is not a function