esbuild的介绍、使用及配置
「esbuild」是一个「JavaScript」打包和压缩工具,核心目标是开创构建工具性能的新时代, 同时创建一个易于使用的现代构建工具。
主要特性:
- 极快的速度,无需缓存
- 支持 ES6 和 CommonJS 模块
- 支持对 ES6 模块进行 tree shaking
- API 可同时用于 JavaScript 和 Go
- 兼容 TypeScript 和 JSX 语法
- 支持 Source maps
- 支持 Minification
- 支持 plugins
js 脚本构建
可以通过如下两种方式调用 API:
-
在命令行中调用
echo 'let x: number = 1' | npx esbuild --loader=ts
如果是使用命令行调用API,参数设置有三种方式:
- –foo (主要用于设置 Boolean 类型的值)
- –foo=bar (设置单个 String 类型的值)
- –foo:bar (设置多个值,可以被重复设置)
-
在 JavaScript 中调用
require('esbuild').transformSync('let x: number = 1', {loader: 'ts', });// 输出 { warnings: [],code: 'let x = 1;\n',map: '',mangleCache: undefined,legalComments: undefined }
在 esbuild 的 API 中有两种主要的 API 调用:transform 与 build。
transform
transform API 操作单个字符串,而不访问文件系统。这使其能够比较理想地在没有文件系统的环境中使用(比如浏览器)或者作为另一个工具链的一部分(比如 vite 或者 snowPack)。
require('esbuild').transformSync('let x: number = 1', {loader: 'ts',
});// 输出
{warnings: [],code: 'let x = 1;\n',map: '',mangleCache: undefined,legalComments: undefined
}
build
调用 build API 操作文件系统中的一个或多个文件。 它允许文件互相引用并且打包在一起。
require('fs').writeFileSync('in.ts', 'let x: number = 1');require('esbuild').buildSync({entryPoints: ['in.ts'],bundle: true,outfile: 'out.js',
});
require('fs').readFileSync('out.js', 'utf8');// 输出
('let x = 1;\n');
如果我们使用命令行去打包,请注意 esbuild 不会 默认打包,必须传递 --bundle 标志启用打包。
配置项
platform
- browser(默认):使用立即执行函数包裹,避免变量泄露到全局作用域中。
- node: ES6-风格的导出使用的 export 语句将会被转换为 CommonJS exports 对象中的 getters。
- neutral:也是浏览器端运行,使用 ECMAScript 2015 (即 ES6) 中引入的 export 语法。
默认情况下,esbuild 的打包器为浏览器生成代码。 如果打包好的代码想要在 node 环境中运行,应该设置 platform 为 node。
bundle
打包一个文件意味着将任何导入的依赖项内联到文件中。默认情况下,esbuild 不会打包输入的文件。
require('esbuild').build({entryPoints: ['in.js'],bundle: true,outfile: 'out.js',
});
打包与文件连接不同。在启用打包时向 esbuild 传递多个输入文件将创建两个单独的 bundle 而不是将输入文件连接在一起。 为了使用 esbuild 将一系列文件打包在一起, 在一个入口起点文件中引入所有文件, 然后就像打包一个文件那样将它们打包
动态import
动态 import 不会打包到结果当中,而是当作 external 进行配置。
import 'pkg';
import('pkg');
require('pkg');// 非静态import,不会打包处理,而是直接生成到结果文件中
import(`pkg/${foo}`);
require(`pkg/${foo}`);
['pkg'].map(require);
webpack 会把所有可能访问到的文件都进行打包,然后在运行时中模拟一个文件系统。
而 esbuild 并不会模拟一个文件系统。
Define
该特性提供了一种用常量表达式替换全局标识符的方法。 它可以在不改变代码本身的情况下改变某些构建之间代码的行为。
let js = 'name = dynamicValue';
const result = require('esbuild').transformSync(js, {define: { dynamicValue: 'leopord' },
});
console.log(result);// 输出
{warnings: [],code: 'name = leopord;\n',map: '',mangleCache: undefined,legalComments: undefined
}
替换表达式必须是一个 JSON 对象(null、boolean、number、string、array 或者 object) 或者一个标识符。除了数组和对象之外,替换表达式是内联替换的,这意味着他们可以参与常数折叠。 数组与对象替换表达式会被存储在一个变量中,然后被标识符引用而不是内联替换, 这避免了替换重复复制一个值,但也意味着该值不能参与常数折叠。
如果我们想用字符串字面值替换某些东西,记住传递给 esbuild 的替换值本身必须包含引号。 省略引号意味着替换的值是一个标识符。
require('esbuild').transformSync('id, name', {define: { id: 'text', name: '"text"' },
})
{code: 'text, "text";\n',map: '',warnings: []
}
entryPoints
入口文件,数组|对象 形式
require('esbuild').buildSync({entryPoints: ['index.js', 'home.js'],bundle: true,outdir: 'out',
})// 生成文件结构
|- out
|--home.js
|--index.js
默认是根据入口文件名来生成打包后的文件名,但是可以通过传递对象来实现自定义文件名。
require('esbuild').buildSync({entryPoints: {out1: 'index.js',out2: 'home.js',},bundle: true,outdir: 'out',
})// 生成文件
-out
--out1.js
--out2.js
external
可以标记一个文件或者包为外部(external),从而将其从我们的打包结果中移除。 导入将被保留(对于 iife 以及 cjs 格式使用 require,对于 esm 格式使用 import),而不是被打包,并将在运行时进行计算引入。
用处:
- 去除在打包文件中生成永远不会执行的代码(比如浏览器端不需要node端的包),直接通过 external 移除
require('fs').writeFileSync('index.js', 'require("fs")')
require('esbuild').buildSync({entryPoints: ['index.js'],outfile: 'out.js',bundle: true,platform: 'node',external: ['fs'],
})
- 我们也可以在外部(external)路径中使用 * 通配符标记所有符合该模式的为外部(external)。 例如,我们可以使用 .png 移除所有的 .png 文件或者使用 /images/ 移除所有路径以 /images/ 开头的路径。
format
为生成的 JavaScript 文件设置输出格式。有三个可选的值:iife、cjs 与 esm。
inject
可以自动替换从另一个文件引入的全局变量。当我们为无法控制的代码适配新环境时是非常有用的。
// process-shim.js
export let process = {cwd: () => ''
}// index.js
console.log(process.cwd())
可以使用 inject 特性将一个 import 置于文件中以替换所有的全局标识符 process。这样可以替换 node 的 process.cwd() 函数的使用,解决在浏览器中运行时因缺失变量而导致崩溃的问题。
require('esbuild').buildSync({entryPoints: ['index.js'],bundle: true,inject: ['./process-shim.js'],outfile: 'out.js',
})// 打包结果
// out.js
let process = {cwd: () => ""};
console.log(process.cwd());
结合 define 一起使用
require('esbuild').buildSync({entryPoints: ['entry.js'],bundle: true,define: { 'process.cwd': 'dummy_process_cwd' },inject: ['./process-shim.js'],outfile: 'out.js',
})// 打包结果
// out.js
function dummy_process_cwd() {return "";
}
console.log(dummy_process_cwd());
loader
改变了输入文件解析的方式。例如, js loader 将文件解析为 JavaScript, css loader 将文件解析为 CSS。
transform API 调用仅使用一个 loader,因为它不涉及与文件系统的交互,因此不需要处理文件拓展名。build API 可以使用多个 loader,因为引入的文件可能有不同类型。
require('esbuild').buildSync({entryPoints: ['app.js'],bundle: true,loader: {'.png': 'dataurl','.svg': 'text',},outfile: 'out.js',
})
minify
启用该配置时,生成的代码会被压缩而不是格式化输出。压缩后的代码与未压缩代码是相等的,但是会更小。这意味着下载更快但是更难调试。 一般情况下在生产环境而不是开发环境压缩代码。
var js = 'fn = obj => { return obj.x }'
require('esbuild').transformSync(js, {minify: true,
}){code: 'fn=n=>n.x;\n',map: '',warnings: []
}
该配置项结合起来做三件独立的事情:移除空格、重写语法使其更体积更小、重命名变量为更短的名称。 一般情况下这三件事情我们都想做,但是如果有必要的话,这三个配置项可以单独启用:
var js = 'fn = obj => { return obj.x }'
require('esbuild').transformSync(js, {minifyWhitespace: true,
})
// 输出
{code: 'fn=obj=>{return obj.x};\n',map: '',warnings: []
}require('esbuild').transformSync(js, {minifyIdentifiers: true,
})
// 输出
{code: 'fn = (n) => {\n return n.x;\n};\n',map: '',warnings: []
}require('esbuild').transformSync(js, {minifySyntax: true,
})
// 输出
{code: 'fn = (obj) => obj.x;\n',map: '',warnings: []
}
outdir
该配置项为 build 操作设置输出文件夹名称。
require('esbuild').buildSync({entryPoints: ['index.js'],bundle: true,outdir: 'out',
})
输出文件夹如果不存在的话将会创建该文件夹,但是当其包含一些文件时不会被清除。 生成的文件遇到同名文件会进行静默覆盖。如果我们想要输出文件夹只包含当前 esbuild 运行生成的文件, 我们应该在运行 esbuild 之前清除输出文件夹。
outfile
该配置项为 build 操作设置输出文件名。这仅在单个入口点时适用。 如果有多个入口点,我们必须使用 outdir 配置项来制定输出文件夹。
require('esbuild').buildSync({entryPoints: ['index.js'],bundle: true,outfile: 'out.js',
})
sourcemap
Source map 可以使调试代码更容易。 它们编码从生成的输出文件中的行/列偏移量转换回 对应的原始输入文件中的行/列偏移量所需的信息。 如果生成的代码与原始代码有很大的不同, 这是很有用的(例如 我们的源代码为 Typescript 或者我们启用了 压缩)。 如果我们更希望在我们的浏览器开发者工具中寻找单独的文件, 而不是一个大的打包好的文件, 这也很有帮助。
有4种方式:
- linked:单独一个 .map 文件,在.js 文件末尾存在 //# sourceMappingURL= 注释
require('esbuild').buildSync({entryPoints: ['index.js'],sourcemap: true,outfile: 'out.js',
})
- external:单独一个 .map 文件,在 .js 文件末尾不带注释
require('esbuild').buildSync({entryPoints: ['index.js'],sourcemap: 'external',outfile: 'out.js',
})
- inline:插入整个 source map 到 .js 文件中,source map 通常是比较大的,因为他们包含所有的源代码,因此我们通常不会想让代码中包含 inline source maps。为了移除 source map 中的源代码(只保存文件名以及行/列映射关系), 请使用 sources content 配置项。
require('esbuild').buildSync({entryPoints: ['index.js'],sourcemap: 'inline',outfile: 'out.js',
})
- both:inline 和 external 的合并版,既插入整个 source map 到 .js 文件中,又生成单独一个 .map 文件
require('esbuild').buildSync({entryPoints: ['index.js'],sourcemap: 'both',outfile: 'out.js',
})
build API 支持上面4中,而 transform API 不支持 linked 模式,因为传入 transform 中的没有文件名字,只有内容。
浏览器仅在堆栈跟踪打印在控制台后才会使用 source maps
target
设置生成 JavaScript 代码的目标环境。默认的 target 为 esnext,这意味着默认情况下,esbuild 将假设所有最新的 JavaScript 特性都是受支持的。
require('esbuild').buildSync({entryPoints: ['app.js'],target: ['es2020','chrome58','firefox57','safari11','edge16','node12',],outfile: 'out.js',
})
write
build API 可以写入文件系统中,也可以返回本应作为内存缓冲区写入的文件。默认情况下 CLI 与 JavaScript API 写入到文件系统,GO API 不是。使用内存缓冲区:
let result = require('esbuild').buildSync({entryPoints: ['app.js'],sourcemap: 'external',write: false,outdir: 'out',
})for (let out of result.outputFiles) {console.log(out.path, out.contents)
}
allowOverwrite
可以让输出流覆盖输入流,也就是生成的打包文件直接替换掉入口文件。
require('esbuild').buildSync({entryPoints: ['index.js'],outdir: '.',allowOverwrite: true,
})
metafile
该配置告诉 esbuild 以 JSON 格式生成一些构建相关的元数据。 下面的例子就是将元数据置于名为 meta.json 的文件中
const result = require('esbuild').buildSync({entryPoints: ['index.js'],bundle: true,metafile: true,outfile: 'out.js',
})
require('fs').writeFileSync('meta.json',JSON.stringify(result.metafile))
analyzeMetafile
生成易读的打包结果
(async () => {let esbuild = require('esbuild')let result = await esbuild.build({entryPoints: ['example.jsx'],outfile: 'out.js',minify: true,metafile: true,})let text = await esbuild.analyzeMetafile(result.metafile, {verbose: true,})console.log(text)
})()// 打印out.js ────── 36b ── 100.0%└ index.js ─ 35b ─── 97.2%
资源名称
当 loader 设置为 file 时,该配置项控制额外生成的文件名称。它使用带有占位符的模板来配置输出路径,当生成输出路径时,占位符将被特定 于文件的值替换。例如,例如,指定 assets/[name]-[hash] 的资源名称模板,将所有资源放入输出目录内名为 assets 的子目录中,并在文件名中包含资产的内容哈希。
require('esbuild').buildSync({entryPoints: ['app.js'],assetNames: 'assets/[name]-[hash]',loader: { '.png': 'file' },bundle: true,outdir: 'out',
})
在资源路径模板中有4个可用占位符:
- [dir]:相对路径
- [name]:这是不带拓展名的原始资源文件名称。例如,如果一个资源原来名为 image.png,然后模板中的 [name] 就会被 image 替换。没有必要使用该占位符;它的存在只是为了提供对人类友好的资源 名称,使调试更容易。
- [hash]:这是资源的内容哈希,可以避免命名冲突。
- [ext]:通过文件后缀来生成一个目录,并把所有相同后缀的文件放在一起(–asset-names=assets/[ext]/[name]-[hash] 会把 image.png 重名为 assets/png/image-CQFGD2NG.png)
banner
使用它可以在生成的 JavaScript 和 CSS 文件的开头插入任意字符串。
require('esbuild').buildSync({entryPoints: ['index.js'],banner: {js: '// banner comment',css: '/* banner comment */',},outfile: 'out.js',
})
footer
使用它可以在生成的 JavaScript 和 CSS 文件的末尾插入任意字符串。这通常用于插入注释:
require('esbuild').buildSync({entryPoints: ['index.js'],footer: {js: '// footer comment',css: '/* footer comment*/',},outfile: 'out.js',
})
splitting
代码分割。
分割仍然处于开发状态。
当启用代码分割时,我们必须使用 outdir 配置输出文件夹。
require('esbuild').buildSync({entryPoints: ['home.js', 'index.js'],bundle: true,splitting: true,outdir: 'out',format: 'esm',
})
chunkNames
此选项控制在启用 代码分割 时自动生成的共享代码块的文件名。 它使用带有占位符的模板来配置输出路径,当生成输出路径时,占位符将被特定于 chunk 的值替换。 例如,指定 chunks/[name]-[hash] 的 chunk 名称模板, 将所有生成的块放入输出目录内的名为 chunks 的子目录中,并在文件名中包含 chunk 的内容哈希。
require('esbuild').buildSync({entryPoints: ['index.js'],chunkNames: 'chunks/[name]-[hash]',bundle: true,outdir: 'out',splitting: true,format: 'esm',
})
有3个可用占位符:
- [name]:目前这将始终是 chunk,尽管这个占位符在将来的版本中可能会有额外的值
- [hash]:这是 chunk 的内容哈希。在生成多个共享代码块的情况下,内容哈希是区分不同 chunk 的必要条件
- [ext]:后缀
resolveExtensions
node 使用的解析算法 支持隐式的文件扩展名。我们可以 require(‘./file’),然后他将会按照顺序检查 ./file、./file.js、./file.json 与 ./file.node。包括 esbuild 在内的现代打包器都将此概念拓展到了其他文件类型。在 esbuild 中可以使用解析插件设置对隐式文件拓展名进行自定义配置, 默认为 .tsx,.ts,.jsx,.js,.css,.json
require('esbuild').buildSync({entryPoints: ['index.js'],bundle: true,resolveExtensions: ['.ts', '.js'],outfile: 'out.js',
})
stdin
通常,build API 调用接受一个或多个文件名作为输入。但是,stdin 这个配置项可以用于在文件系统上根本不存在模块的情况下运行构建,因为它对应于在命令行上用管道将文件连接到 stdin。
let result = require('esbuild').buildSync({stdin: {contents: `export * from "./another-file"`,// 可选配置resolveDir: require('path').join(__dirname, 'src'),sourcefile: 'imaginary-file.js',loader: 'ts',},format: 'cjs',write: false,
})