本文列出了 esbuild 所以内置支持的文件类型, 每种文件类型都有一个对应的"解析器", 解析器的作用是告诉 esbuild 如何解读文件内容. esbuild 已经对一些文件扩展名添加了默认的解析器, 这些默认值都是可以覆盖的
JavaScript
laoder: js
这个解析器用来解析.js
,.cjs
,.mjs
文件, .cjs
扩展名表示用于 Node 环境的 CommonJS 标准模块. .mjs
扩展名表示符合 ECMAScript 标准的模块
默认情况下, 当启用Minify启用时, esbuild 将会使用"更现代"的 JavaScript 语法. 比如a!==void && a!== null ? a : b
将被转换成a??b
, 这种语法是ES2022版本中添加的新语法, 如果你不想使用这种新语法, 必须自行设置Target属性, 用来告诉 esbuild 你的项目需要在什么环境中工作, 然后 esbuild 将会避免使用一些"太过于现代的语法"
esbuild 支持 JavaScript 所有的新语法, 但是某些旧的浏览器可能不支持这些新语法, 处理这个问题时, 还是需要你去配置Target属性, 告诉 esbuild 适当的把新语法转换为旧语法.
以下两种语法始终都会被转换为旧语法:
Syntax transform | Language version | Example |
---|---|---|
Trailing commas in function parameter lists and calls | es2017 | foo(a, b, ) |
Numeric separators | esnext | 1_000_000 |
以下的语法特性会按照配置的目标浏览器环境有条件的进行转换
Syntax transform | Language version | Example |
---|---|---|
Exponentiation operator | es2016 | a ** b |
Async functions | es2017 | async () => {} |
Asynchronous iteration | es2018 | for await (let x of y) {} |
Spread properties | es2018 | let x = {...y} |
Rest properties | es2018 | let {...x} = y |
Optional catch binding | es2019 | try {} catch {} |
Optional chaining | es2020 | a?.b |
Nullish coalescing | es2020 | a ?? b |
import.meta | es2020 | import.meta |
Logical assignment operators | es2021 | a ??= b |
Class instance fields | es2022 | class { x } |
Static class fields | es2022 | class { static x } |
Private instance methods | es2022 | class { #x() {} } |
Private instance fields | es2022 | class { #x } |
Private static methods | es2022 | class { static #x() {} } |
Private static fields | es2022 | class { static #x } |
Ergonomic brand checks | es2022 | #x in y |
Class static blocks | es2022 | class { static {} } |
Import assertions | esnext | import "x" assert {} |
以下的语法特性始终不会进行转换:
Syntax transform | Language version | Example |
---|---|---|
Async generators | es2018 | async function* foo() {} |
BigInt | es2020 | 123n |
Top-level await | es2022 | await import(x) |
Arbitrary module namespace identifiers | es2022 | export {foo as 'f o o'} |
Hashbang grammar | esnext | #!/usr/bin/env node |
你也可以参考已完成的 ECMA 提案列表和正在进行的 ECMA 提案列表来了解更多新的语法特性.
请注意, 只有Format选项配置为esm
时, esbuild 才支持顶部上下文内的await
语法, 否则 esbuild 会报错
注意事项
在 esbuild 中使用 JavaScript 时, 你需要考虑以下几点:
对 ES5 的支持
目前, 尚不支持把 ES6+的语法转为 ES5 语法. 但是如果你希望使用 esbuild 来转换 ES5 代码, 仍然需要将Target选项配置为es5
. 这是为了防止 esbuild 把 ES6+ 的语法放入打包文件中. 比如如果没有这个配置, {x:x}
将会被转换为{x}
, esbuild 执行这种转换, 是因为转换后的内容会更简介, 如果target
设置为es5
就不会进行这种转换
对私有属性的处理
针对类似于#name
的私有属性, esbuild 使用WeakMap
和WrakSet
来保存这些私有属性. 这和Babel
,Typescript
编译器中的转换方式是一致的, 但是大多数现代 JS 引擎, 对于WeakMap
和WrakSet
这两种数据类型的支持性能并不是很好.
使用这种转换方式来保存私有字段, 可能为 JS 引擎的垃圾收集器带来更大的开销. 这是因为现代 JS 引擎(ChakraCore 除外)会都把WeakMap
和WrakSet
当做真实、独立的对象保存在浏览器中, 大量的对象可能会导致垃圾收集的性能问题, 此方面可以参考这个资料
遵循 ECMAScript 模块规范导入模块
有时候你可能希望在导入模块之前, 修改该模块它所以来的某个全局属性的状态, 但无论是 JavaScript 规范还是 esbuild 都会默认把import
语句的执行顺序提升到首位, 所以下面这种方式是行不通的:
JavaScript
window.foo = {}
import './something-that-needs-foo'
有一些工具(比如 Typescript 编译器)在这方面没有遵循 JavaScript 规范, 使用这类工具编译的代码可能会帮你解决此类问题, 但这样的代码不符合 ECMAScript 规范, 也不被那些遵循 ECMAScript 规范的环境或者工具(比如 Node, 浏览器, esbuild 等)所支持, 更是不可移植的, 所以不推荐使用这种反感.
如果你真的希望在导入模块之前, 修改它所依赖的某个全局属性的状态, 完全可以把这些操作放在另一个模块内, 然后将这个模块通过import
放到依赖该全局属性的模块之前引入即可, 就像这样:
JavaScript
import './assign-to-foo-on-window'
import './something-that-needs-foo'
eval
执行问题
表达式eval(x)
看起来像是一个函数调用, 但实际上它在 JavaScript 内有特殊的行为, 它意味着保存在变量x
中的字符串信息, 会被当做 JavaScript 语句执行. 比如let y=123; return eval(y)
将返回123
;
这种方式执行语句被称为direct eval(eval字面量)
, 使用它的时候将面临很多问题:
- 使用
eval
的文件, 会导致 esbuild 取消对所在文件的所有优化, 包括"移除死代码"与"压缩"等功能(下方为译者案例).
- 流行的打包器, 通常会把所有的文件, 合并到一个文件中, 并重命名这些文件内的变量名, 来防止名称冲突, 这就意味着通过
eval
执行的代码可以在打包文件中读取任意的变量或者修改任意变量的值, 如果被它修改的值包含敏感数据, 将会引发很严重的安全问题
译者注, 下面的案例中, 就体现了
eval
中的语句, "意外的"修改了main.js
中的age
属性值.
- 使用
eval
的文件, esbuild 会直接把文件放入一个 CommonJS 模式的闭包函数中, 来防止eval
执行过程中对其他文件的影响. 这也会导致降低构建速度
幸运的是, 有两种方案可以避免上述某些问题:
- 不直接使用
eval
: 比如(0,eval)('x')
、window.eval('x')
或者[eval][0]('x')
这几种, 都属于非直接调用eval
.
- 使用
new Function('x')
声明新的函数对象, 这种方式相当于在全局上下文中声明了一个新的函数, 并且可以向函数中添加任意参数(Function 构造函数)
The value of toString() is not preserved on functions (and classes)
It's somewhat common to call toString() on a JavaScript function object and then pass that string to some form of eval to get a new function object. This effectively "rips" the function out of the containing file and breaks links with all variables in that file. Doing this with esbuild is not supported and may not work. In particular, esbuild often uses helper methods to implement certain features and it assumes that JavaScript scope rules have not been tampered with. For example:
javascript
let pow = (a, b) => a ** b;
let pow2 = (0, eval)(pow.toString());
console.log(pow2(2, 3));
When this code is compiled for ES6, where the ** operator isn't available, the ** operator is replaced with a call to the **pow helper function:
javascript
let **pow = Math.pow;
let pow = (a, b) => **pow(a, b);
let pow2 = (0, eval)(pow.toString());
console.log(pow2(2, 3));
If you try to run this code, you'll get an error such as ReferenceError: **pow is not defined because the function (a, b) => **pow(a, b) depends on the locally-scoped symbol **pow which is not available in the global scope. This is the case for many JavaScript language features including async functions, as well as some esbuild-specific features such as the keep names setting.
This problem most often comes up when people get the source code of a function with .toString() and then try to use it as the body of a web worker. If you are doing this and you want to use esbuild, you should instead build the source code for the web worker in a separate build step and then insert the web worker source code as a string into the code that creates the web worker. The define feature is one way to insert the string at build time.
this 指向问题
在 Javascript 中, 函数中的 this 值通常指向调用者, 比如obj.fn()
执行时, fn
中的 this 会指向obj
, 默认情况下, esbuild 也遵循这种规则. 但是当你从其他模块中调用函数时, this 的指向可能并不是你希望的样子. 比如下面的案例:
上面的例子中,通过ns.foo()
调用this.bar()
失败了, 这是因为 esbuild 会自动把导入代码重写为import { foo } from './lib.js
. 这是 esbuild 的 tree-shaking 功能导致的. 但这不是问题, 因为使用模块化编程的开发人员通常不会这么做, 如果他们想使用 bar 函数, 通常会直接使用ns.bar()
来调用而不是通过ns.foo()
来间接调用
ESM 中的 default 问题
ESM 模块中有一个名为default
的导出项, 当具有default
导出项的 ESM 模块被转换为 CommonJS 格式, 再将这个 CommonJS 格式的代码导入另一个 ESM 模块时, 再不同的解释器中执行, 将会引起不同的结果.
esbuild 在处理这类代码时, 它必须决定使用哪种解释器来运行, esbuild 使用的方式与 webpack 是相同的.
比如你有下面的一份例子:
javascript
// somelib.js
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports["default"] = "foo";
// index.js
import foo from "./somelib.js";
console.log(foo);
- babel 中的解释器
在 babel 中, 上面的案例会输出字符串foo
, 这是因为 babel 发现了__esModule
关键字, 从而知道somelib.js
是从 ESM 转换到 CommonJS 语法的, 所以它把这份代码当做下面的样子来直接执行
javascript
// somelib.js
export default "foo";
- Node 中的解释器
在 node 中执行时, 将会输出{default:'foo'}
, 这是因为他们认为 CommonJS 支持动态导出, 而 ESM 仅支持静态导出, 如果将 CommonJS 导入 ESM 模块, 必须使用某种方式导出 CommonJS 的导出对象本身.
如果你是某个库的开发人员, 请尽量避免使用default
导出, 它确实会存在一些兼容性问题, 可能会在某些时候给用户带来不好的影响
默认情况下, esbuild 将会使用 babel 解释器来运行, 如果你希望 esbuild 使用 node 解释器, 可以把代码放在.mjs
或者.mts
结尾的文件中, 或者在packag.json
中指定type
字段为module
译者注: 关于 CommonJS 与 ESM 的导出与引用关系, 可以参考Node.js 如何处理 ES6 模块文章
Typescript
laoder: ts/tsc
遇到.ts
,.tsx
,.mts
,.cts
文件时, 默认使用ts
或者tsx
加载器, 这意味着 esbuild 内置了对 Typescript 语法的支持. 但是 esbuild 不会进行相关的类型检查, 如果需要你可以使用其他工具来配合 esbuild 进行类型检查的工作
下面的这些 Typescript 语法会被 esbuild 忽略(这里只是例子, 并不是全部)
语法 | 例子 |
---|---|
Interface declarations | interface Foo {} |
type declarations | type Foo = number |
Function declarations | function foo():void; |
Ambiend declarations | declare module 'foo' {} |
Type-only imports | import type {Type} from 'foo' |
Type-only exports | export type {Type} from 'foo' |
Type-only import specifiers | import {type Type} from 'foo' |
Type-only export specifiers | export {type Type} from 'foo' |
下面的这些 Typescript 语法会被 esbuild 编译为 Javascript(这里只是例子, 并不是全部)
语法 | 例子 | 备注 |
---|---|---|
Namespaces | namespace Foo {} |
|
Enums | enum Foo { A, B } |
|
Const | enums const enum Foo { A, B } |
|
Generic type parameters | <T>(a: T): T => a |
Not available with the tsx loader |
JSX with types | <Element<T>/> |
|
Type casts | a as B and <B>a |
|
Type imports | import {Type} from 'foo' |
Handled by removing all unused imports |
Type exports | export {Type} from 'foo' |
Handled by ignoring missing exports in TypeScript files |
Experimental decorators | @sealed class Foo {} |
The emitDecoratorMetadata flag is not supported |