6
头图

esbuild是最近比较火的编译工具,在有些领域已经开始替代webpack或babel,下面一起来看看这个工具的详细内容。

一.速度优越性比较

这里是一份压测数据,从图中可以看出esbuild性能拔群
image.png
关于它的性能为何如此优越,在官方解释中有以下几点:

1.它是用Go语言编写的,编译成可执行代码

JavaScript必须基于解释器的node环境才能执行,所以当webpack等工具解释完本身的代码后,可能esbuild已经完成编译工作了,而这时候webpack才开始执行编译。

此外,Go的核心设计是并行的,而JavaScript不是。

Go有线程之间的共享内存,而JavaScript则必须在线程之间进行数据序列化。

Go和JavaScript都有并行的垃圾收集器,但Go的堆是在所有线程之间共享的,而JavaScript的每个线程都有一个独立的堆。JavaScript工作线程并行量减少了一半,因为还有一半CPU核心正忙于为另一半收集垃圾。

2.并行被大量使用

esbuild内部的算法是经过精心设计的,以尽可能使所有可用的CPU核心完全饱和。

大致有三个阶段:解析、连接和代码生成。解析和代码生成是大部分的工作,并且是完全可并行的。

由于所有线程都共享内存,在编译导入相同的JavaScript库的不同入口点时,工作可以很容易地被分享。大多数现代计算机都有很多内核,所以并行化是一个很大的性能提升。

3.esbuild中的所有内容都是从头开始编写的

自己编写而不是使用第三方库有很多性能上的好处。可以从一开始就考虑到性能问题,以确保所有的东西都使用一致的数据结构,避免昂贵的转换,而且在必要时可进行广泛的架构变更。当然,缺点是这是一个很大的工作量。

4.内存得到有效利用

编译器在理想情况下大多是输入长度的O(n)复杂性。因此,如果你正在处理大量的数据,内存访问速度可能会严重影响性能。你需要对数据处理的越少,编译器速度就越快。

二.加载器(loader)

esbuild加载器的作用与webpack中loader作用类似,都是对于某种类型的文件进行编译,具体功能介绍如下:

1.js-loader

这个加载器默认用于.js、.cjs和.mjs文件。.cjs扩展名被node用于CommonJS模块,而.mjs扩展名被node用于ECMAScript模块,尽管esbuild并没有对这两者进行区分。

esbuild支持所有现代JavaScript语法。然而,较新的语法可能不被旧的浏览器所支持,所以你可能想配置目标选项,告诉esbuild将较新的语法转换为适当的旧语法。

但请注意,ES5支持的不是很好,目前还不支持将ES6+语法转换为ES5。
2. ts-loader 或 tsx-loader

这个加载器对于.ts和.tsx文件是默认启用的,这意味着esbuild内置了对TypeScript语法的解析和丢弃类型注释的支持。然而,esbuild不做任何类型检查,所以你仍然需要在esbuild中并行运行tsc -noEmit来检查类型。
需要注意的是,esbuild在编译时不会进行类型检查,这应该在编译之前使用ide去检查

3. jsx-loader

将会把xml代码转换成js代码

4. json-loader

对于.json文件,这个加载器是默认启用的。它在构建时将JSON文件解析成一个JavaScript对象,并将该对象作为默认导出。

5. css-loader

对于.css文件,这个加载器是默认启用的。它以CSS语法的形式加载文件。CSS在esbuild中是一种第一类内容类型,这意味着esbuild可以直接编译CSS文件,而不需要从JavaScript代码中导入你的CSS。

你可以@导入其他CSS文件,用url()引用图片和字体文件,esbuild会把所有东西编译在一起。注意,你必须为图像和字体文件配置一个加载器,因为esbuild没有任何预配置。通常这是一个数据URL加载器或外部文件加载器。

请注意,esbuild还不支持CSS Module,所以来自CSS文件的导出名称集目前总是空的。未来计划支持CSS Module。

6. text-loader

对于.txt文件,这个加载器是默认启用的。它在构建时将文件加载为字符串,并将字符串导出为默认导出。使用它看起来像这样。

7. binary-loader

这个加载器将在构建时以二进制缓冲区的形式加载文件,并使用Base64编码将其嵌入到包中。文件的原始字节在运行时被从Base64解码,并使用默认的导出方式导出为Uint8Array。

8. Base64-loader

这个加载器将在构建时以二进制缓冲区的形式加载文件,并使用Base64编码将其嵌入到编译中的字符串。这个字符串将使用默认的导出方式导出。

9. dataurl-loader

这个加载器将在构建时作为二进制缓冲区加载文件,并将其作为Base64编码的数据URL嵌入到编译中。这个字符串是用默认的导出方式导出的。

10. file-loader

这个加载器会将文件复制到输出目录,并将文件名作为一个字符串嵌入到编译中。这个字符串是使用默认的导出方式导出的。

三.api 调用

为了更加方便的使用,esbuild提供了api调用的方式,在调用api时传入option进行相应功能的设置。在esbuild的API中,有两个主要的API调用方式:transformbuild。两者的区别在于是否最终生成文件。

1.Transform API

Transform API调用对单个字符串进行操作,不需要访问文件系统。非常适合在没有文件系统的环境中使用或作为另一个工具链的一部分。下面是个简单例子:

require('esbuild').transformSync('let x: number = 1', {
  loader: 'ts',
})
=>
{
  code: 'let x = 1;\n',
  map: '',
  warnings: []
}
2.Build API

Build API调用对文件系统中的一个或多个文件进行操作。这使得文件可以相互引用,并被编译在一起。下面是个简单例子:

require('fs').writeFileSync('in.ts', 'let x: number = 1')
require('esbuild').buildSync({
  entryPoints: ['in.ts'],
  outfile: 'out.js',
})

四.插件API

1.简介

插件API属于上面提到的API调用的一部分,插件API允许你将代码注入到构建过程的各个部分。与API的其他部分不同,它不能从命令行中获得。你必须编写JavaScriptGo代码来使用插件API。

插件API只能用于Build API,不能用于Transform API

如果你正在寻找一个现有的esbuild插件,你应该看看现有的esbuild插件的列表。这个列表中的插件都是作者特意添加的,目的是为了让esbuild社区中的其他人使用。

2.写插件

一个esbuild插件是一个包含name和setup函数的对象。它们以数组的形式传递给构建API调用。setup函数在每次BUILD API调用时都会运行一次。
下面我们来尝试自定义一个插件

import fs from 'fs'

export default {
  name: "env",
  setup(build) {
    build.onLoad({ filter: /\.tsx$/ }, async (args) => {
      const source = await fs.promises.readFile(args.path, "utf8");
      const contents = source.toString();
      console.log('文件内容:',contents)
      return {
        contents: contents,
        loader: "tsx",
      };
    });
  },
};
2.1 name

name通用代表这个插件的名称

2.2 setup函数
2.2.1. Namespace

每个模块都有一个相关的命名空间。默认情况下,esbuild在文件命名空间中操作,它对应于文件系统中的文件。但是esbuild也可以处理那些在文件系统上没有对应位置的 "虚拟 "模块。虚拟模块通常使用文件以外的命名空间来区分它们和文件系统模块。

2.2.2.Filters

每个回调都必须提供一个正则表达式作为过滤器。当路径与过滤器不匹配时,esbuild会跳过调用回调,这样做是为了提高性能。从esbuild的高度并行的内部调用到单线程的JavaScript代码是很昂贵的,为了获得最大的速度,应该尽可能地避免。

你应该尽量使用过滤器正则表达式,而不是使用JavaScript代码进行过滤。这样做更快,因为正则表达式是在esbuild内部评估的,根本不需要调用JavaScript。

2.2.3.Resolve callbacks

一个使用onResolve添加的回调将在esbuild构建的每个模块的每个导入路径上运行。这个回调可以定制esbuild如何进行路径解析。

2.2.4. Load callbacks

一个使用onLoad添加的回调可以对文件内容进行处理并返回。

3.如何解决插件与loader的执行顺序问题

esbuild中的loader是直接把某个格式的文件直接处理并返回,而插件api也具有接触文件内容的机会,这两者的执行时机在文档中并没有提到。

import fs from "fs";

export default {
  name: "env",
  setup(build) {
    build.onLoad({ filter: /\.tsx$/ }, async (args) => {
      const source = await fs.promises.readFile(args.path, "utf8");
      const contents = source.toString();
      //astHandle只能处理js内容,对ts或jsx不认识,编译报错
      const result = astHandle(contents)
      return {
        contents: result,
        loader: "tsx",
      };
    });
  },
};

从上面代码例子中看出,插件api在接受到文件内容后,并不能直接处理tsx的内容,因为我们可能不具备处理tsx的能力,这时候并不能显示定义插件在tsx转换成js之后执行。要想处理这种情况只能借助esbuild的transform api能力。

import fs from "fs";
import esbuild from "esbuild";

export default {
  name: "env",
  setup(build) {
    build.onLoad({ filter: /\.tsx$/ }, async (args) => {
      const source = await fs.promises.readFile(args.path, "utf8");
      const contents = source.toString();
      const result = astHandle(esbuild.transformSync(contents, {
        loader: 'tsx',
      }))
      return {
        contents: result.code,
        loader: "tsx",
      };
    });
  },
};
4.babel迁移

由于babel的社区插件较多,这给原本使用babel的项目迁移到esbuild设置了障碍,可以使用社区提供的esbuild-plugin-babel一键迁移,例如使用antd组件时需要配合使用antd-plugin-import插件,具体如下:

import babel from "esbuild-plugin-babel";
import esbuild from "esbuild";
import path, { dirname } from "path";
import { fileURLToPath } from "url";

const babelJSON = babel({
  filter: /\.tsx?/,
  config: {
    presets: ["@babel/preset-react", "@babel/preset-typescript"],
    plugins: [["import", { libraryName: "antd", style: "css" }]],
  },
});
const __dirname = dirname(fileURLToPath(import.meta.url));

esbuild
  .build({
    entryPoints: [path.join(__dirname + "/app.tsx")],
    outdir: "out",
    plugins: [babelJSON],
  })
  .catch(() => process.exit(1));
5.插件的使用限制

插件API并不打算涵盖所有的用例。它不可能关联编译过程的每个部分。例如,目前不可能直接修改AST。这个限制的存在是为了保持esbuild出色的性能特征,同时也是为了避免暴露出太多的API表面,这将是一个维护的负担,并且会阻止涉及改变AST的改进。

一种考虑esbuild的方式是作为网络的 "链接器"。就像本地代码的链接器一样,esbuild的工作是接收一组文件,解析并绑定它们之间的引用,并生成一个包含所有代码链接的单一文件。一个插件的工作是生成最终被链接的单个文件。

esbuild中的插件最好是在相对范围内工作,并且只定制构建的一个小方面。例如,一个自定义格式(如YAML)的特殊配置文件的插件是非常合适的。你使用的插件越多,你的构建速度就越慢,尤其是当你的插件是用JavaScript编写的时候。如果一个插件适用于你构建中的每一个文件,那么你的构建很可能会非常慢。如果缓存适用,必须由插件本身来完成。


石坚
413 声望14 粉丝