esbuild是最近比较火的编译工具,在有些领域已经开始替代webpack或babel,下面一块儿来看看这个工具的详细内容。javascript
这里是一份压测数据,从图中能够看出esbuild性能拔群
关于它的性能为什么如此优越,在官方解释中有如下几点:css
JavaScript必须基于解释器的node环境才能执行,因此当webpack等工具解释完自己的代码后,可能esbuild已经完成编译工做了,而这时候webpack才开始执行编译。java
此外,Go的核心设计是并行的,而JavaScript不是。node
Go有线程之间的共享内存,而JavaScript则必须在线程之间进行数据序列化。react
Go和JavaScript都有并行的垃圾收集器,但Go的堆是在全部线程之间共享的,而JavaScript的每一个线程都有一个独立的堆。JavaScript工做线程并行量减小了一半,由于还有一半CPU核心正忙于为另外一半收集垃圾。webpack
esbuild内部的算法是通过精心设计的,以尽量使全部可用的CPU核心彻底饱和。git
大体有三个阶段:解析、链接和代码生成。解析和代码生成是大部分的工做,而且是彻底可并行的。github
因为全部线程都共享内存,在编译导入相同的JavaScript库的不一样入口点时,工做能够很容易地被分享。大多数现代计算机都有不少内核,因此并行化是一个很大的性能提高。web
本身编写而不是使用第三方库有不少性能上的好处。能够从一开始就考虑到性能问题,以确保全部的东西都使用一致的数据结构,避免昂贵的转换,并且在必要时可进行普遍的架构变动。固然,缺点是这是一个很大的工做量。正则表达式
编译器在理想状况下大可能是输入长度的O(n)复杂性。所以,若是你正在处理大量的数据,内存访问速度可能会严重影响性能。你须要对数据处理的越少,编译器速度就越快。
esbuild加载器的做用与webpack中loader做用相似,都是对于某种类型的文件进行编译,具体功能介绍以下:
这个加载器默认用于.js、.cjs和.mjs文件。.cjs扩展名被node用于CommonJS模块,而.mjs扩展名被node用于ECMAScript模块,尽管esbuild并无对这二者进行区分。
esbuild支持全部现代JavaScript语法。然而,较新的语法可能不被旧的浏览器所支持,因此你可能想配置目标选项,告诉esbuild将较新的语法转换为适当的旧语法。
但请注意,ES5支持的不是很好,目前还不支持将ES6+语法转换为ES5。
这个加载器对于.ts和.tsx文件是默认启用的,这意味着esbuild内置了对TypeScript语法的解析和丢弃类型注释的支持。然而,esbuild不作任何类型检查,因此你仍然须要在esbuild中并行运行tsc -noEmit来检查类型。
须要注意的是,esbuild在编译时不会进行类型检查,这应该在编译以前使用ide去检查
将会把xml代码转换成js代码
对于.json文件,这个加载器是默认启用的。它在构建时将JSON文件解析成一个JavaScript对象,并将该对象做为默认导出。
对于.css文件,这个加载器是默认启用的。它以CSS语法的形式加载文件。CSS在esbuild中是一种第一类内容类型,这意味着esbuild能够直接编译CSS文件,而不须要从JavaScript代码中导入你的CSS。
你能够@导入其余CSS文件,用url()引用图片和字体文件,esbuild会把全部东西编译在一块儿。注意,你必须为图像和字体文件配置一个加载器,由于esbuild没有任何预配置。一般这是一个数据URL加载器或外部文件加载器。
请注意,esbuild还不支持CSS Module,因此来自CSS文件的导出名称集目前老是空的。将来计划支持CSS Module。
对于.txt文件,这个加载器是默认启用的。它在构建时将文件加载为字符串,并将字符串导出为默认导出。使用它看起来像这样。
这个加载器将在构建时以二进制缓冲区的形式加载文件,并使用Base64编码将其嵌入到包中。文件的原始字节在运行时被从Base64解码,并使用默认的导出方式导出为Uint8Array。
这个加载器将在构建时以二进制缓冲区的形式加载文件,并使用Base64编码将其嵌入到编译中的字符串。这个字符串将使用默认的导出方式导出。
这个加载器将在构建时做为二进制缓冲区加载文件,并将其做为Base64编码的数据URL嵌入到编译中。这个字符串是用默认的导出方式导出的。
这个加载器会将文件复制到输出目录,并将文件名做为一个字符串嵌入到编译中。这个字符串是使用默认的导出方式导出的。
为了更加方便的使用,esbuild提供了api调用的方式,在调用api时传入option进行相应功能的设置。在esbuild的API中,有两个主要的API调用方式:transform和build。二者的区别在因而否最终生成文件。
Transform API调用对单个字符串进行操做,不须要访问文件系统。很是适合在没有文件系统的环境中使用或做为另外一个工具链的一部分。下面是个简单例子:
require('esbuild').transformSync('let x: number = 1', { loader: 'ts', }) => { code: 'let x = 1;\n', map: '', warnings: [] }
Build API调用对文件系统中的一个或多个文件进行操做。这使得文件能够相互引用,并被编译在一块儿。下面是个简单例子:
require('fs').writeFileSync('in.ts', 'let x: number = 1') require('esbuild').buildSync({ entryPoints: ['in.ts'], outfile: 'out.js', })
插件API属于上面提到的API调用的一部分,插件API容许你将代码注入到构建过程的各个部分。与API的其余部分不一样,它不能从命令行中得到。你必须编写JavaScript或Go代码来使用插件API。
插件API只能用于Build API,不能用于Transform API
若是你正在寻找一个现有的esbuild插件,你应该看看现有的esbuild插件的列表。这个列表中的插件都是做者特地添加的,目的是为了让esbuild社区中的其余人使用。
一个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", }; }); }, };
name通用表明这个插件的名称
每一个模块都有一个相关的命名空间。默认状况下,esbuild在文件命名空间中操做,它对应于文件系统中的文件。可是esbuild也能够处理那些在文件系统上没有对应位置的 "虚拟 "模块。虚拟模块一般使用文件之外的命名空间来区分它们和文件系统模块。
每一个回调都必须提供一个正则表达式做为过滤器。当路径与过滤器不匹配时,esbuild会跳过调用回调,这样作是为了提升性能。从esbuild的高度并行的内部调用到单线程的JavaScript代码是很昂贵的,为了得到最大的速度,应该尽量地避免。
你应该尽可能使用过滤器正则表达式,而不是使用JavaScript代码进行过滤。这样作更快,由于正则表达式是在esbuild内部评估的,根本不须要调用JavaScript。
一个使用onResolve添加的回调将在esbuild构建的每一个模块的每一个导入路径上运行。这个回调能够定制esbuild如何进行路径解析。
一个使用onLoad添加的回调能够对文件内容进行处理并返回。
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", }; }); }, };
因为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));
插件API并不打算涵盖全部的用例。它不可能关联编译过程的每一个部分。例如,目前不可能直接修改AST。这个限制的存在是为了保持esbuild出色的性能特征,同时也是为了不暴露出太多的API表面,这将是一个维护的负担,而且会阻止涉及改变AST的改进。
一种考虑esbuild的方式是做为网络的 "连接器"。就像本地代码的连接器同样,esbuild的工做是接收一组文件,解析并绑定它们之间的引用,并生成一个包含全部代码连接的单一文件。一个插件的工做是生成最终被连接的单个文件。
esbuild中的插件最好是在相对范围内工做,而且只定制构建的一个小方面。例如,一个自定义格式(如YAML)的特殊配置文件的插件是很是合适的。你使用的插件越多,你的构建速度就越慢,尤为是当你的插件是用JavaScript编写的时候。若是一个插件适用于你构建中的每个文件,那么你的构建极可能会很是慢。若是缓存适用,必须由插件自己来完成。