esbuild是最近比拟火的编译工具,在有些畛域曾经开始代替webpack或babel,上面一起来看看这个工具的具体内容。
一.速度优越性比拟
这里是一份压测数据,从图中能够看出esbuild性能拔群
对于它的性能为何如此优越,在官网解释中有以下几点:
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调用形式:transform和build。两者的区别在于是否最终生成文件。
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的其余局部不同,它不能从命令行中取得。你必须编写JavaScript或Go代码来应用插件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编写的时候。如果一个插件实用于你构建中的每一个文件,那么你的构建很可能会十分慢。如果缓存实用,必须由插件自身来实现。