内容类型
下面列出了所有内置的内容类型。每个内容类型都与一个关联的“加载器”相关联,该加载器告诉 esbuild 如何解释文件内容。一些文件扩展名默认情况下已经配置了加载器,尽管默认值可以被覆盖。
#JavaScript
加载器:js
此加载器默认情况下为 .js
、.cjs
和 .mjs
文件启用。.cjs
扩展名由 node 用于 CommonJS 模块,而 .mjs
扩展名由 node 用于 ECMAScript 模块。
请注意,默认情况下,esbuild 的输出将利用所有现代 JS 功能。例如,a !==
在启用最小化时将变为 a ?? b
,这利用了来自 ES2020 版本的 JavaScript 的语法。如果这是不希望的,您必须指定 esbuild 的 target 设置,以说明您的输出需要在哪些浏览器中正常工作。然后 esbuild 将避免使用对这些浏览器来说过于现代的 JavaScript 功能。
esbuild 支持所有现代 JavaScript 语法。但是,较旧的浏览器可能不支持较新的语法,因此您可能需要配置 target 选项,以告诉 esbuild 在适当的情况下将较新的语法转换为较旧的语法。
这些语法功能始终针对较旧的浏览器进行转换
语法转换 | 语言版本 | 示例 |
---|---|---|
函数参数列表和调用中的尾随逗号 | es2017 |
foo(a, b, ) |
数字分隔符 | esnext |
1_000_000 |
这些语法功能根据配置的语言 target 有条件地针对较旧的浏览器进行转换
语法转换 | 当 --target 低于以下版本时进行转换 |
示例 |
---|---|---|
指数运算符 | es2016 |
a ** b |
异步函数 | es2017 |
async () => {} |
异步迭代 | es2018 |
for await (let x of y) {} |
异步生成器 | es2018 |
async function* foo() {} |
展开属性 | es2018 |
let x = {...y} |
剩余属性 | es2018 |
let {...x} = y |
可选的捕获绑定 | es2019 |
try {} catch {} |
可选链 | es2020 |
a?.b |
空值合并运算符 | es2020 |
a ?? b |
import.meta |
es2020 |
import.meta |
逻辑赋值运算符 | es2021 |
a ??= b |
类实例字段 | es2022 |
class { x } |
静态类字段 | es2022 |
class { static x } |
私有实例方法 | es2022 |
class { #x() {} } |
私有实例字段 | es2022 |
class { #x } |
私有静态方法 | es2022 |
class { static #x() {} } |
私有静态字段 | es2022 |
class { static #x } |
符合人体工程学的品牌检查 | es2022 |
#x in y |
类静态块 | es2022 |
class { static {} } |
导入断言 | esnext |
import "x" assert {} |
自动访问器 | esnext |
class { accessor x } |
using 声明 |
esnext |
using x = y |
这些语法功能目前始终被直接传递,未经转换
语法转换 | 当 --target 低于以下版本时不支持 |
示例 |
---|---|---|
RegExp dotAll 标志 |
es2018 |
/./s 1 |
RegExp 后向断言 | es2018 |
/(?<=x)y/ 1 |
RegExp 命名捕获组 | es2018 |
/(?<foo>\d+)/ 1 |
RegExp Unicode 属性转义 | es2018 |
/\p{ASCII}/u 1 |
BigInt | es2020 |
123n |
顶层 await | es2022 |
await import(x) |
任意模块命名空间标识符 | es2022 |
export {foo as 'f o o'} |
RegExp 匹配索引 | es2022 |
/x(.+)y/d 1 |
哈希邦语法 | esnext |
#!/usr/bin/env node |
装饰器 | esnext |
@foo class Bar {} |
RegExp 集合符号 | esnext |
/[\w--\d]/ 1 |
另请参阅 已完成的 ECMAScript 提案列表 和 活动的 ECMAScript 提案列表。请注意,虽然支持转换包含顶层 await 的代码,但仅当 输出格式 设置为 esm
时才支持捆绑包含顶层 await 的代码。
#JavaScript 注意事项
在将 JavaScript 与 esbuild 一起使用时,您应该牢记以下几点
#ES5 支持不佳
将 ES6+ 语法转换为 ES5 尚未得到支持。但是,如果您使用 esbuild 来转换 ES5 代码,您仍然应该将 target 设置为 es5
。这可以防止 esbuild 将 ES6 语法引入您的 ES5 代码。例如,如果没有此标志,对象字面量 {x: x}
将变为 {x}
,字符串 "a\nb"
在最小化时将变为多行模板字面量。这两个替换都是因为生成的代码更短,但如果 target 是 es5
,则不会执行这些替换。
#私有成员性能
私有成员转换(针对 #name
语法)使用 WeakMap
和 WeakSet
来保留此功能的隐私属性。这类似于 Babel 和 TypeScript 编译器中的相应转换。大多数现代 JavaScript 引擎(V8、JavaScriptCore 和 SpiderMonkey,但不是 ChakraCore)可能对大型 WeakMap
和 WeakSet
对象没有良好的性能特征。
使用此语法转换激活创建具有私有字段或私有方法的类的许多实例可能会导致垃圾收集器产生大量开销。这是因为现代引擎(除了 ChakraCore)将弱值存储在实际的映射对象中,而不是作为键本身的隐藏属性,而大型映射对象会导致垃圾收集的性能问题。有关更多信息,请参阅 此参考。
#导入遵循 ECMAScript 模块行为
您可能尝试在导入需要该全局状态的模块之前修改全局状态,并期望它能正常工作。但是,JavaScript(因此 esbuild)实际上将所有 import
语句“提升”到文件顶部,因此这样做将不起作用
window.foo = {}
import './something-that-needs-foo'
那里有一些 ECMAScript 模块的错误实现(例如 TypeScript 编译器),它们在这方面没有遵循 JavaScript 规范。使用这些工具编译的代码可能会“工作”,因为 import
被替换为对 require()
的内联调用,这忽略了提升要求。但是,此类代码在真正的 ECMAScript 模块实现(例如 node、浏览器或 esbuild)中将无法正常工作,因此不建议编写此类代码,因为它不可移植。
正确执行此操作的方法是将全局状态修改移到它自己的导入中。这样它将在其他导入之前运行
import './assign-to-foo-on-window'
import './something-that-needs-foo'
#在捆绑时避免直接 eval
虽然表达式 eval(x)
看起来像一个正常的函数调用,但它实际上在 JavaScript 中具有特殊行为。以这种方式使用 eval
意味着存储在 x
中的已评估代码可以通过名称引用任何包含范围中的任何变量。例如,代码 let y =
将返回 123
。
这称为“直接 eval”,在捆绑代码时存在问题,原因如下
现代捆绑器包含一个名为“作用域提升”的优化,它将所有捆绑文件合并到一个文件中,并重命名变量以避免名称冲突。但是,这意味着由直接
eval
评估的代码可以读取和写入捆绑包中任何文件中的任何变量!这是一个正确性问题,因为已评估的代码可能试图访问全局变量,但可能会意外地访问另一个文件中的具有相同名称的私有变量。如果另一个文件中的私有变量包含敏感数据,它甚至可能成为安全问题。当已评估的代码引用使用
import
语句导入的变量时,它可能无法正常工作。导入的变量是对另一个文件中的变量的实时绑定。它们不是这些变量的副本。因此,当 esbuild 捆绑您的代码时,您的导入将被替换为对导入文件中变量的直接引用。但是,该变量可能具有不同的名称,在这种情况下,由直接eval
评估的代码将无法通过预期名称引用它。使用直接
eval
会迫使 esbuild 对包含对直接eval
的调用的所有范围内的所有代码进行反优化。为了正确性,它必须假设已评估的代码可能需要访问从该eval
调用可达的任何其他代码。这意味着所有这些代码都不会被消除为死代码,并且所有这些代码都不会被最小化。由于由直接
eval
评估的代码可能需要通过名称引用任何可达的变量,因此 esbuild 被阻止重命名所有可由已评估的代码访问的变量。这意味着它无法重命名变量以避免与捆绑包中其他变量的名称冲突。因此,直接eval
会导致 esbuild 将文件包装在 CommonJS 闭包中,这通过引入新的范围来避免名称冲突。但是,这会使生成的代码更大,速度更慢,因为导出的变量使用运行时动态绑定而不是编译时静态绑定。
幸运的是,通常很容易避免使用直接 eval
。有两种常用的替代方法可以避免上述所有缺点
(0, eval)('x')
这被称为“间接 eval”,因为
eval
没有被直接调用,因此不会触发 JavaScript VM 中直接 eval 的语法特殊情况。您可以使用任何语法来调用间接 eval,除了eval('x')
的确切形式的表达式。例如,var eval2 =
和eval; eval2('x') [eval][0]('x')
和window.
都是间接 eval 调用。当您使用间接 eval 时,代码将在全局范围内评估,而不是在调用者的内联范围内评估。eval('x') new Function('x')
这在运行时构造一个新的函数对象。这就像您在全局范围内编写了
function()
一样,只是{ x } x
可以是任意代码字符串。这种形式有时很方便,因为您可以向函数添加参数,并使用这些参数将变量公开给已评估的代码。例如,(new Function('env',
就好像您编写了'x'))( someEnv) (function(env)
一样。当已评估的代码需要访问局部变量时,这通常是直接{ x })( someEnv) eval
的足够替代方案,因为您可以将局部变量作为参数传递。
#toString()
的值在函数(和类)上不会被保留
在 JavaScript 函数对象上调用 toString()
并将该字符串传递给某种形式的 eval
以获取新的函数对象,这是一种常见的做法。这实际上会将函数从包含文件中“剥离”,并破坏与该文件中所有变量的链接。使用 esbuild 进行此操作不受支持,并且可能无法正常工作。特别是,esbuild 经常使用辅助方法来实现某些功能,并且假设 JavaScript 范围规则没有被篡改。例如
let pow = (a, b) => a ** b;
let pow2 = (0, eval)(pow.toString());
console.log(pow2(2, 3));
当这段代码为 ES6 编译时,**
运算符不可用,**
运算符将被替换为对 __pow
辅助函数的调用
let __pow = Math.pow;
let pow = (a, b) => __pow(a, b);
let pow2 = (0, eval)(pow.toString());
console.log(pow2(2, 3));
如果您尝试运行这段代码,您将收到类似 ReferenceError:
的错误,因为函数 (a, b)
依赖于局部范围内的符号 __pow
,该符号在全局范围内不可用。对于许多 JavaScript 语言特性(包括 async
函数)以及一些 esbuild 特定特性(例如 保留名称 设置)来说,情况都是如此。
这个问题最常出现在人们使用 .toString()
获取函数的源代码,然后尝试将其用作 Web Worker 主体的代码中。如果您正在这样做,并且想要使用 esbuild,您应该在单独的构建步骤中构建 Web Worker 的源代码,然后将 Web Worker 源代码作为字符串插入创建 Web Worker 的代码中。 define 功能是构建时插入字符串的一种方法。
#从模块命名空间对象调用的函数不会保留 this
的值
在 JavaScript 中,函数中 this
的值会根据函数的调用方式自动填充。例如,如果使用 obj.fn()
调用函数,则函数调用期间 this
的值将为 obj
。esbuild 尊重这种行为,但有一个例外:如果您从模块命名空间对象调用函数,则 this
的值可能不正确。例如,考虑以下从模块命名空间对象 ns
调用 foo
的代码
import * as ns from './foo.js'
ns.foo()
如果 foo.js
尝试使用 this
引用模块命名空间对象,那么在使用 esbuild 捆绑代码后,它可能无法正常工作
// foo.js
export function foo() {
this.bar()
}
export function bar() {
console.log('bar')
}
造成这种情况的原因是,esbuild 会自动将大多数使用模块命名空间对象的代码重写为直接导入内容的代码。这意味着上面的示例代码将转换为以下代码,这将删除函数调用的 this
上下文
import { foo } from './foo.js'
foo()
这种转换极大地改善了 树摇(又名死代码消除),因为它使 esbuild 可以理解哪些导出符号未被使用。它的缺点是这会改变使用 this
访问模块导出的代码的行为,但这并不是问题,因为没有人应该在第一位编写这种奇怪的代码。如果您需要从同一个文件中访问导出的函数,只需直接调用它(例如,在上面的示例中使用 bar()
而不是 this.bar()
)。
#default
导出可能容易出错
ES 模块格式(即 ESM)有一个特殊的导出称为 default
,它有时与所有其他导出名称的行为不同。当具有 default
导出的 ESM 格式代码转换为 CommonJS 格式时,然后将该 CommonJS 代码导入到另一个 ESM 格式的模块中,就会出现两种不同的解释,这两种解释都被广泛使用(Babel 方式和 Node 方式)。这非常不幸,因为它会导致无休止的兼容性问题,尤其是因为 JavaScript 库通常在 ESM 中编写并作为 CommonJS 发布。
当 esbuild 捆绑 执行此操作的代码时,它必须决定使用哪种解释,并且没有完美的答案。esbuild 使用的启发式方法与 Webpack 使用的启发式方法相同(有关详细信息,请参见下文)。由于 Webpack 是使用最广泛的捆绑器,这意味着 esbuild 正在尽其所能与现有生态系统保持最大程度的兼容性,以解决此兼容性问题。因此,好消息是,如果您能够使具有此问题的代码与 esbuild 一起工作,那么它也应该与 Webpack 一起工作。
以下是一个演示问题的示例
// index.js
import foo from './somelib.js'
console.log(foo)
// somelib.js
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = 'foo';
以下是两种解释,这两种解释都被广泛使用
Babel 解释
如果使用 Babel 解释,这段代码将打印
foo
。他们的理由是somelib.js
从 ESM 转换为 CommonJS(正如您从__esModule
标记中可以看出的那样),并且原始代码看起来像这样// somelib.js export default 'foo'
如果
somelib.js
没有从 ESM 转换为 CommonJS,那么这段代码将打印foo
,因此无论模块格式如何,它都应该继续打印foo
。这是通过检测 CommonJS 模块何时通过__esModule
标记(所有模块转换工具都设置了该标记,包括 Babel、TypeScript、Webpack 和 esbuild)从 ES 模块转换而来,并在__esModule
标记存在时将默认导入设置为exports.
来实现的。这种行为很重要,因为它是在 CommonJS 环境中正确运行交叉编译的 ESM 所必需的,并且很长一段时间以来,这是在 Node 中运行 ESM 代码的唯一方法,直到 Node 最终添加了原生 ESM 支持。default Node 解释
如果使用 Node 解释,这段代码将打印
{ default:
。他们的理由是 CommonJS 代码使用动态导出,而 ESM 代码使用静态导出,因此将 CommonJS 导入 ESM 的完全通用方法是以某种方式公开 CommonJS'foo' } exports
对象本身。例如,CommonJS 代码可以执行exports[
,这在 ESM 语法中没有等效项。Math. random() ] = 'foo' default
导出用于此,因为这实际上是 ES 模块规范的制定者最初设计它的目的。这种解释对于正常的 CommonJS 模块来说是完全合理的。它只会在 CommonJS 模块(即__esModule
存在时)曾经是 ES 模块的情况下导致兼容性问题,在这种情况下,行为会与 Babel 解释不同。
如果您是库作者: 在编写新代码时,您应该认真考虑完全避免使用 default
导出。它不幸地被兼容性问题所污染,使用它很可能会在某个时候给您的用户带来问题。
如果您是库用户: 默认情况下,esbuild 将使用 Babel 解释。如果您希望 esbuild 改用 Node 解释,您需要将代码放在以 .mts
或 .mjs
结尾的文件中,或者您需要在 package.json
文件中添加 "type":
。其理由是,Node 的原生 ESM 支持只能在文件扩展名为 .mjs
或存在 "type":
的情况下运行 ESM 代码,因此这样做是一个很好的信号,表明代码旨在在 Node 中运行,因此应该使用 Node 对 default
导入的解释。这是 Webpack 使用的相同启发式方法。
#TypeScript
加载器:ts
或 tsx
此加载器默认情况下为 .ts
、.tsx
、.mts
和 .cts
文件启用,这意味着 esbuild 内置支持解析 TypeScript 语法并丢弃类型注释。但是,esbuild 不会 执行任何类型检查,因此您仍然需要并行运行 tsc -noEmit
来检查类型。这不是 esbuild 本身所做的。
这些 TypeScript 类型声明将被解析并忽略(非详尽列表)
语法特性 | 示例 |
---|---|
接口声明 | interface Foo {} |
类型声明 | type Foo = number |
函数声明 | function foo(): void; |
环境声明 | declare module 'foo' {} |
仅类型导入 | import type {Type} from 'foo' |
仅类型导出 | export type {Type} from 'foo' |
仅类型导入说明符 | import {type Type} from 'foo' |
仅类型导出说明符 | export {type Type} from 'foo' |
支持 TypeScript 仅语法扩展,并且始终转换为 JavaScript(非详尽列表)
语法特性 | 示例 | 备注 |
---|---|---|
命名空间 | namespace Foo {} |
|
枚举 | enum Foo { A, B } |
|
常量枚举 | const enum Foo { A, B } |
|
泛型类型参数 | <T>(a: T): T => a |
必须使用 tsx 加载器编写 <T,>( ... |
带有类型的 JSX | <Element<T>/> |
|
类型转换 | a as B 和 <B>a |
|
类型导入 | import {Type} from 'foo' |
通过删除所有未使用的导入来处理 |
类型导出 | export {Type} from 'foo' |
通过忽略 TypeScript 文件中缺少的导出来处理 |
实验性装饰器 | @sealed class Foo {} |
需要 experimentalDecorators ,不支持 emitDecoratorMetadata |
实例化表达式 | Array<number> |
TypeScript 4.7+ |
extends 在 infer 上 |
infer A extends B |
TypeScript 4.7+ |
方差注释 | type A<out B> = () => B |
TypeScript 4.7+ |
satisfies 运算符 |
a satisfies T |
TypeScript 4.9+ |
const 类型参数 |
class Foo<const T> {} |
TypeScript 5.0+ |
#TypeScript 注意事项
在将 TypeScript 与 esbuild 一起使用时,您应该牢记以下事项(除了 JavaScript 注意事项)
#文件独立编译
即使在转译单个模块时,TypeScript 编译器实际上仍然会解析导入的文件,以便它可以判断导入的名称是类型还是值。但是,esbuild 和 Babel(以及 TypeScript 编译器的 transpileModule
API)等工具会独立编译每个文件,因此它们无法判断导入的名称是类型还是值。
因此,如果您将 TypeScript 与 esbuild 一起使用,则应启用 isolatedModules
TypeScript 配置选项。此选项会阻止您使用可能导致在 esbuild 等环境中错误编译的功能,在这些环境中,每个文件都会独立编译,而不会跨文件跟踪类型引用。例如,它会阻止您使用 export
从另一个模块重新导出类型(您需要改用 export
)。
#导入遵循 ECMAScript 模块行为
出于历史原因,TypeScript 编译器默认情况下会将 ESM(ECMAScript 模块)语法编译为 CommonJS 语法。例如,import *
会编译为 const foo =
。据推测,这是因为 ECMAScript 模块在 TypeScript 采用该语法时仍然是一个提案。但是,这是与该语法在 node 等真实平台上的行为不匹配的遗留行为。例如,require
函数可以返回任何 JavaScript 值,包括字符串,但 import * as
语法始终会导致对象,而不能是字符串。
为了避免由于此遗留功能而导致的问题,如果您将 TypeScript 与 esbuild 一起使用,则应启用 esModuleInterop
TypeScript 配置选项。启用它会禁用此遗留行为,并使 TypeScript 的类型系统与 ESM 兼容。此选项默认情况下未启用,因为它会对现有的 TypeScript 项目造成重大更改,但 Microsoft 强烈建议将其应用于新项目和现有项目(然后更新您的代码),以更好地与生态系统的其他部分兼容。
具体来说,这意味着使用 ESM 导入语法从 CommonJS 模块导入非对象值必须使用默认导入,而不是使用import * as
。因此,如果一个 CommonJS 模块通过module.
导出一个函数,你需要使用import
而不是import *
。
#需要类型系统的功能不受支持
TypeScript 类型被视为注释,esbuild 会忽略它们,因此 TypeScript 被视为“类型检查的 JavaScript”。类型注释的解释由 TypeScript 类型检查器完成,如果你使用 TypeScript,你应该在 esbuild 之外运行它。这与 Babel 的 TypeScript 实现使用的相同编译策略。但是,这意味着某些需要类型解释才能工作的 TypeScript 编译功能无法与 esbuild 一起使用。
具体来说
不支持
emitDecoratorMetadata
TypeScript 配置选项。此功能将相应 TypeScript 类型的 JavaScript 表示传递给附加的装饰器函数。由于 esbuild 不会复制 TypeScript 的类型系统,因此它没有足够的信息来实现此功能。不支持
declaration
TypeScript 配置选项(即生成.d.ts
文件)。如果你正在用 TypeScript 编写库,并且想要将编译后的 JavaScript 代码作为包发布供其他人使用,你可能还想发布类型声明。这不是 esbuild 可以为你做的事情,因为它不保留任何类型信息。你可能需要使用 TypeScript 编译器来生成它们,或者手动编写它们。
#只尊重某些 tsconfig.json
字段
在捆绑过程中,esbuild 中的路径解析算法将考虑包含 tsconfig.json
文件的最近父目录的内容,并相应地修改其行为。也可以使用 esbuild 的tsconfig
设置通过构建 API 显式设置 tsconfig.json
路径,并使用 esbuild 的tsconfigRaw
设置通过转换 API 显式传入 tsconfig.json
文件的内容。但是,esbuild 目前只检查 tsconfig.json
文件中的以下字段
experimentalDecorators
此选项启用 TypeScript 文件中装饰器语法的转换。转换遵循 TypeScript 本身在启用
experimentalDecorators
时遵循的过时的装饰器设计。请注意,JavaScript 以及在禁用
experimentalDecorators
时用于 TypeScript 的装饰器有一个更新的设计。这不是 esbuild 目前实现的东西,因此 esbuild 目前不会在禁用experimentalDecorators
时转换装饰器。target
useDefineForClassFields
这些选项控制 TypeScript 文件中的类字段是使用“define”语义还是“assign”语义编译
Define 语义(esbuild 的默认行为):TypeScript 类字段的行为与普通的 JavaScript 类字段相同。字段初始化器不会触发基类的 setter。你应该以这种方式编写所有新的代码。
Assign 语义(你需要显式启用):esbuild 模拟 TypeScript 的传统类字段行为。字段初始化器将触发基类 setter。这可能需要让旧代码运行。
使用 esbuild 禁用 define 语义(因此启用 assign 语义)的方法与使用 TypeScript 禁用它的方法相同:在你的
tsconfig.json
文件中将useDefineForClassFields
设置为false
。为了与 TypeScript 兼容,esbuild 还复制了 TypeScript 的行为,其中当未指定
useDefineForClassFields
时,如果tsconfig.json
包含早于ES2022
的target
,它将默认为false
。但我建议如果你需要显式设置useDefineForClassFields
,而不是依赖于来自target
设置值的此默认值。请注意,tsconfig.json
中的target
设置仅由 esbuild 用于确定useDefineForClassFields
的默认值。它不会影响 esbuild 自身的target
设置,即使它们具有相同的名称。baseUrl
paths
这些选项影响 esbuild 对文件系统上文件进行
import
/require
路径的解析。你可以使用它来定义包别名,并以其他方式重写导入路径。请注意,使用 esbuild 进行导入路径转换需要启用bundling
,因为 esbuild 的路径解析仅在捆绑期间发生。还要注意,esbuild 还具有一个本地的alias
功能,你可能希望使用它。jsx
jsxFactory
jsxFragmentFactory
jsxImportSource
这些选项影响 esbuild 对 JSX 语法转换为 JavaScript 的转换。它们等效于 esbuild 对这些设置的本机选项:
jsx
、jsxFactory
、jsxFragment
和jsxImportSource
.alwaysStrict
strict
如果启用这两个选项中的任何一个,esbuild 将认为所有 TypeScript 文件中的所有代码都处于严格模式,并将生成的代码加上
"use strict"
前缀,除非输出format
设置为esm
(因为所有 ESM 文件都自动处于严格模式)。verbatimModuleSyntax
importsNotUsedAsValues
preserveValueImports
默认情况下,TypeScript 编译器在将 TypeScript 转换为 JavaScript 时会删除未使用的导入。这样,事实证明是类型专用导入的导入就不会在运行时导致错误。esbuild 也实现了这种行为。
这些选项允许你禁用此行为并保留未使用的导入,这在例如导入的文件具有有用的副作用时非常有用。你应该为此使用
verbatimModuleSyntax
,因为它替换了旧的importsNotUsedAsValues
和preserveValueImports
设置(TypeScript 现在已弃用它们)。extends
此选项允许你将
tsconfig.json
文件拆分为多个文件。此值可以是用于单继承的字符串,也可以是用于多继承的数组(TypeScript 5.0+ 中的新功能)。
所有其他 tsconfig.json
字段(即不在上述列表中的字段)将被忽略。
#你不能将 tsx
加载器用于 *.ts
文件
tsx
加载器不是 ts
加载器的超集。它们是两个不同的部分不兼容的语法。例如,字符序列 <a>1</a>/g
使用 ts
加载器解析为 <a>(1 < (/a>/g))
,使用 tsx
加载器解析为 (<a>1</a>) / g
。
这导致的最常见问题是无法使用 tsx
加载器对箭头函数表达式(如 <T>() => {}
)使用泛型类型参数。这是故意的,并且与官方 TypeScript 编译器的行为一致。tsx
语法中的那个空格是为 JSX 元素保留的。
#JSX
加载器:jsx
或 tsx
JSX 是 JavaScript 的 XML 类语法扩展,它是为React创建的。它旨在通过你的构建工具转换为普通的 JavaScript。每个 XML 元素都变成一个普通的 JavaScript 函数调用。例如,以下 JSX 代码
import Button from './button'
let button = <Button>Click me</Button>
render(button)
将转换为以下 JavaScript 代码
import Button from "./button";
let button = React.createElement(Button, null, "Click me");
render(button);
此加载器默认情况下为 .jsx
和 .tsx
文件启用。请注意,JSX 语法默认情况下在 .js
文件中未启用。如果你想启用它,你需要配置它
esbuild app.js --bundle --loader:.js=jsx
require('esbuild').buildSync({
entryPoints: ['app.js'],
bundle: true,
loader: { '.js': 'jsx' },
outfile: 'out.js',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Loader: map[string]api.Loader{
".js": api.LoaderJSX,
},
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
#JSX 的自动导入
使用 JSX 语法通常需要你手动导入你正在使用的 JSX 库。例如,如果你使用 React,默认情况下你需要像这样将 React 导入到每个 JSX 文件中
import * as React from 'react'
render(<div/>)
这是因为 JSX 转换将 JSX 语法转换为对 React.
的调用,但它本身不会导入任何内容,因此 React
变量不会自动存在。
如果你想避免必须手动将 JSX 库导入到每个文件中,你可能可以通过将 esbuild 的JSX转换设置为 automatic
来做到这一点,它会为你生成导入语句。请记住,这也会完全改变 JSX 转换的工作方式,因此如果你使用的是非 React 的 JSX 库,它可能会破坏你的代码。这样做看起来像这样
esbuild app.jsx --jsx=automatic
require('esbuild').buildSync({
entryPoints: ['app.jsx'],
jsx: 'automatic',
outfile: 'out.js',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.jsx"},
JSX: api.JSXAutomatic,
Outfile: "out.js",
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
#在没有 React 的情况下使用 JSX
如果你使用的是除 React 之外的库(如Preact)的 JSX,你可能需要配置JSX 工厂和JSX 片段设置,因为它们分别默认为 React
和 React
esbuild app.jsx --jsx-factory=h --jsx-fragment=Fragment
require('esbuild').buildSync({
entryPoints: ['app.jsx'],
jsxFactory: 'h',
jsxFragment: 'Fragment',
outfile: 'out.js',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.jsx"},
JSXFactory: "h",
JSXFragment: "Fragment",
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
或者,如果你使用的是 TypeScript,你只需为 TypeScript 配置 JSX,方法是在你的 tsconfig.json
文件中添加以下内容,esbuild 应该会自动获取它,而无需进行配置
{
"compilerOptions": {
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
}
}
你还必须在包含 JSX 语法的文件中添加 import
,除非你使用上面描述的自动导入。
#JSON
json
此加载器默认情况下为 .json
文件启用。它在构建时将 JSON 文件解析为 JavaScript 对象,并将该对象作为默认导出。使用它看起来像这样
import object from './example.json'
console.log(object)
除了默认导出之外,JSON 对象中的每个顶级属性还有命名导出。直接导入命名导出意味着 esbuild 可以自动从捆绑包中删除 JSON 文件的未使用部分,只留下你实际使用的命名导出。例如,此代码只包含 version
字段进行捆绑
import { version } from './package.json'
console.log(version)
#CSS
加载器:css
(也包括 global-css
和 local-css
,用于CSS 模块)
css
加载器默认情况下为 .css
文件启用,local-css
加载器默认情况下为 .module.css
文件启用。这些加载器将文件加载为 CSS 语法。CSS 是 esbuild 中的一级内容类型,这意味着 esbuild 可以直接捆绑 CSS 文件,而无需从 JavaScript 代码中导入 CSS
esbuild --bundle app.css --outfile=out.css
require('esbuild').buildSync({
entryPoints: ['app.css'],
bundle: true,
outfile: 'out.css',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.css"},
Bundle: true,
Outfile: "out.css",
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
你可以 @import
其他 CSS 文件,并使用 url()
引用图像和字体文件,esbuild 会将所有内容捆绑在一起。请注意,你必须为图像和字体文件配置一个加载器,因为 esbuild 没有任何预配置的加载器。通常,这要么是数据 URL加载器,要么是外部文件加载器。
这些语法功能根据配置的语言 target 有条件地针对较旧的浏览器进行转换
语法转换 | 示例 |
---|---|
嵌套声明 | a { &:hover { color: red } } |
现代 RGB/HSL 语法 | #F008 |
inset 简写 |
inset: 0 |
hwb() |
hwb(120 30% 50%) |
lab() 和 lch() |
lab(60 -5 58) |
oklab() 和 oklch() |
oklab(0.5 -0.1 0.1) |
color() |
color(display-p3 1 0 0) |
具有两个位置的颜色停止 | linear-gradient(red 2% 4%, blue) |
渐变过渡提示 | linear-gradient(red, 20%, blue) 1 |
渐变颜色空间 | linear-gradient(in hsl, red, blue) 1 |
渐变色调模式 | linear-gradient(in hsl longer hue, red, blue) 1 |
请注意,默认情况下,esbuild 的输出将利用现代 CSS 功能。例如,color:
在启用压缩时将变为 color:
,它使用了来自 CSS 颜色模块级别 4 的语法。如果这是不希望的,您必须指定 esbuild 的 目标 设置,以说明您需要输出在哪些浏览器中正常工作。然后 esbuild 将避免使用对这些浏览器来说过于现代的 CSS 功能。
当您使用 目标 设置提供浏览器版本列表时,esbuild 还会自动插入供应商前缀,以便您的 CSS 在这些浏览器版本或更高版本中正常工作。目前 esbuild 将对以下 CSS 属性执行此操作
appearance
backdrop-filter
background-clip: text
box-decoration-break
clip-path
font-kerning
hyphens
initial-letter
mask-composite
mask-image
mask-origin
mask-position
mask-repeat
mask-size
position: sticky
print-color-adjust
tab-size
text-decoration-color
text-decoration-line
text-decoration-skip
text-emphasis-color
text-emphasis-position
text-emphasis-style
text-orientation
text-size-adjust
user-select
#从 JavaScript 导入
您也可以从 JavaScript 导入 CSS。当您这样做时,esbuild 将收集从给定入口点引用的所有 CSS 文件,并将它们捆绑到与该 JavaScript 入口点相邻的 JavaScript 输出文件旁边的同级 CSS 输出文件中。因此,如果 esbuild 生成 app.js
,它也会生成包含 app.js
引用的所有 CSS 文件的 app.css
。以下是从 JavaScript 导入 CSS 文件的示例
import './button.css'
export let Button = ({ text }) =>
<div className="button">{text}</div>
esbuild 生成的捆绑 JavaScript 不会自动将生成的 CSS 导入您的 HTML 页面。相反,您应该将生成的 CSS 以及生成的 JavaScript 导入您的 HTML 页面。这意味着浏览器可以并行下载 CSS 和 JavaScript 文件,这是最有效的方式。看起来像这样
<html>
<head>
<link href="app.css" rel="stylesheet">
<script src="app.js"></script>
</head>
</html>
如果生成的输出名称不直观(例如,如果您已将 [hash]
添加到 入口点名称 设置,并且输出文件名具有内容哈希),那么您可能需要在 元文件中 查找生成的输出名称。为此,首先通过查找具有匹配 entryPoint
属性的输出找到 JS 文件。此文件位于 <script>
标签中。然后可以使用 cssBundle
属性找到关联的 CSS 文件。此文件位于 <link>
标签中。
#CSS 模块
CSS 模块 是一种 CSS 预处理器技术,用于避免无意的 CSS 名称冲突。CSS 类名通常是全局的,但 CSS 模块提供了一种方法,可以使 CSS 类名仅限于它们所在的代码文件。如果两个单独的 CSS 文件使用相同的本地类名 .button
,esbuild 将自动重命名其中一个,以避免冲突。这类似于 esbuild 如何自动重命名在单独的 JS 模块中具有相同名称的本地变量,以避免名称冲突。
esbuild 支持与 CSS 模块捆绑。要使用它,您需要启用 捆绑,为您的 CSS 文件使用 local-css
加载器(例如,通过使用 .module.css
文件扩展名),然后将您的 CSS 模块代码导入 JS 文件。该文件中的每个本地 CSS 名称都可以导入到 JS 中,以获取 esbuild 重命名的名称。以下是一个示例
// app.js
import { outerShell } from './app.module.css'
const div = document.createElement('div')
div.className = outerShell
document.body.appendChild(div)
/* app.module.css */
.outerShell {
position: absolute;
inset: 0;
}
当您使用 esbuild app.js
捆绑它时,您将得到以下结果(注意本地 CSS 名称 outerShell
如何被重命名)
// out/app.js
(() => {
// app.module.css
var outerShell = "app_outerShell";
// app.js
var div = document.createElement("div");
div.className = outerShell;
document.body.appendChild(div);
})();
/* out/app.css */
.app_outerShell {
position: absolute;
inset: 0;
}
此功能仅在启用捆绑时才有意义,因为您的代码需要 import
重命名的本地名称才能使用它们,并且因为 esbuild 需要能够在单个捆绑操作中处理所有包含本地名称的 CSS 文件,以便它能够成功地重命名冲突的本地名称以避免冲突。
esbuild 为本地 CSS 名称生成的名称是实现细节,不打算在任何地方硬编码。您在 JS 或 HTML 中引用本地 CSS 名称的唯一方法是使用 JS 中的导入语句,该语句与 esbuild 捆绑在一起,如上所示。例如,当启用 压缩 时,esbuild 将使用不同的名称生成算法,该算法生成尽可能短的名称(类似于 esbuild 如何压缩 JS 中的本地标识符)。
#使用全局名称
local-css
加载器默认情况下会使文件中的所有 CSS 名称变为本地。但是,有时您希望在同一个文件中混合本地名称和全局名称。有几种方法可以做到这一点
- 您可以将类名包装在
:global(...)
中以使它们变为全局,并将它们包装在:local(...)
中以使它们变为本地。 - 您可以使用
:global
使名称默认变为全局,并使用:local
使名称默认变为本地。 - 您可以使用
global-css
加载器来启用本地 CSS 功能,但使名称默认变为全局。
以下是一些示例
/* * This is a local name with the "local-css" loader * and a global name with the "global-css" loader */ .button { } /* This is a local name with both loaders */ :local(.button) { } /* This is a global name with both loaders */ :global(.button) { } /* "foo" is global and "bar" is local */ :global .foo :local .bar { } /* "foo" is global and "bar" is local */ :global { .foo { :local { .bar {} } } }
#composes
指令
CSS 模块规范 还描述了 composes
指令。它允许具有本地名称的类选择器引用其他类选择器。这可以用来分离常见的属性集,以避免重复它们。使用 from
关键字,它还可以用来引用其他文件中的具有本地名称的类选择器。以下是一个示例
// app.js
import { submit } from './style.css'
const div = document.createElement('div')
div.className = submit
document.body.appendChild(div)
/* style.css */
.button {
composes: pulse from "anim.css";
display: inline-block;
}
.submit {
composes: button;
font-weight: bold;
}
/* anim.css */
@keyframes pulse {
from, to { opacity: 1 }
50% { opacity: 0.5 }
}
.pulse {
animation: 2s ease-in-out infinite pulse;
}
使用 esbuild
捆绑它将为您提供类似以下内容
(() => {
// style.css
var submit = "anim_pulse style_button style_submit";
// app.js
var div = document.createElement("div");
div.className = submit;
document.body.appendChild(div);
})();
/* anim.css */
@keyframes anim_pulse {
from, to {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.anim_pulse {
animation: 2s ease-in-out infinite anim_pulse;
}
/* style.css */
.style_button {
display: inline-block;
}
.style_submit {
font-weight: bold;
}
注意,使用 composes
会导致导入到 JavaScript 中的字符串变为所有组合在一起的本地名称的空格分隔列表。这旨在传递给 DOM 元素上的 className
属性。还要注意,使用 composes
和 from
允许您(间接地)引用其他 CSS 文件中的本地名称。
请注意,来自不同文件的组合 CSS 类在捆绑输出文件中出现的顺序是故意未定义的(有关详细信息,请参阅 规范)。您不应该在两个单独的类选择器中声明相同的 CSS 属性,然后将它们组合在一起。您只应该组合声明不重叠 CSS 属性的 CSS 类选择器。
#CSS 注意事项
在使用 esbuild 与 CSS 时,您应该牢记以下几点
#有限的 CSS 验证
CSS 有一个 通用语法规范,所有 CSS 处理器都使用它,然后有 许多规范 定义了特定 CSS 规则的含义。虽然 esbuild 理解通用 CSS 语法,并且可以理解一些 CSS 规则(足以将 CSS 文件捆绑在一起并合理地压缩 CSS),但 esbuild 不包含完整的 CSS 知识。这意味着 esbuild 对 CSS 采取“垃圾进,垃圾出”的哲学。如果您想验证您的编译 CSS 是否没有错别字,您应该除了 esbuild 之外还使用 CSS linter。
#@import
顺序与浏览器匹配
CSS 中的 @import
规则与 JavaScript 中的 import
关键字的行为不同。在 JavaScript 中,import
大致意味着“确保导入的文件在评估此文件之前被评估”,但在 CSS 中,@import
大致意味着“在此处重新评估导入的文件”。例如,考虑以下文件
entry.css
@import "foreground.css";
@import "background.css";foreground.css
@import "reset.css";
body {
color: white;
}background.css
@import "reset.css";
body {
background: black;
}reset.css
body {
color: black;
background: white;
}
根据您对 JavaScript 的直觉,您可能认为这段代码首先将正文重置为白色背景上的黑色文本,然后将其覆盖为黑色背景上的白色文本。这不是发生的事情。 相反,正文将完全是黑色的(前景和背景都是)。这是因为 @import
应该表现得好像导入规则被导入的文件替换了(有点像 C/C++ 中的 #include
),这会导致浏览器看到以下代码
/* reset.css */
body {
color: black;
background: white;
}
/* foreground.css */
body {
color: white;
}
/* reset.css */
body {
color: black;
background: white;
}
/* background.css */
body {
background: black;
}
最终简化为以下内容
body { color: black; background: black; }
这种行为很不幸,但 esbuild 以这种方式运行,因为这就是 CSS 的规范,也是 CSS 在浏览器中的工作方式。了解这一点很重要,因为一些其他常用的 CSS 处理工具(例如 postcss-import
)错误地以 JavaScript 顺序而不是 CSS 顺序解析 CSS 导入。如果您将为这些工具编写的 CSS 代码移植到 esbuild(甚至只是切换到在浏览器中本机运行您的 CSS 代码),如果您的代码依赖于错误的导入顺序,您可能会出现外观更改。
#文本
加载器:text
此加载器默认情况下为 .txt
文件启用。它在构建时将文件加载为字符串,并将字符串作为默认导出导出。使用它看起来像这样
import string from './example.txt'
console.log(string)
#二进制
加载器:binary
此加载器将在构建时将文件加载为二进制缓冲区,并使用 Base64 编码将其嵌入到捆绑包中。文件的原始字节在运行时从 Base64 解码,并使用默认导出作为 Uint8Array
导出。使用它看起来像这样
import uint8array from './example.data'
console.log(uint8array)
如果您需要 ArrayBuffer
而不是 Uint8Array
,您可以直接访问 uint8array
。请注意,此加载器默认情况下未启用。您需要为相应的文件扩展名配置它,如下所示
esbuild app.js --bundle --loader:.data=binary
require('esbuild').buildSync({
entryPoints: ['app.js'],
bundle: true,
loader: { '.data': 'binary' },
outfile: 'out.js',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Loader: map[string]api.Loader{
".data": api.LoaderBinary,
},
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
#Base64
加载器:base64
此加载器将在构建时将文件加载为二进制缓冲区,并使用 Base64 编码将其嵌入到捆绑包中作为字符串。此字符串使用默认导出导出。使用它看起来像这样
import base64string from './example.data'
console.log(base64string)
请注意,此加载器默认情况下未启用。您需要为相应的文件扩展名配置它,如下所示
esbuild app.js --bundle --loader:.data=base64
require('esbuild').buildSync({
entryPoints: ['app.js'],
bundle: true,
loader: { '.data': 'base64' },
outfile: 'out.js',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Loader: map[string]api.Loader{
".data": api.LoaderBase64,
},
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
如果您打算将其转换为 Uint8Array
或 ArrayBuffer
,您应该使用 binary
加载器。它使用优化的 Base64 到二进制转换器,比通常的 atob
转换过程更快。
#数据 URL
加载器:dataurl
此加载器将在构建时将文件加载为二进制缓冲区,并将其嵌入到捆绑包中作为 Base64 编码的数据 URL。此字符串使用默认导出导出。使用它看起来像这样
import url from './example.png'
let image = new Image
image.src = url
document.body.appendChild(image)
数据 URL 包含根据文件扩展名和/或文件内容对 MIME 类型进行的最佳猜测,对于二进制数据,它看起来像这样
data:image/png;base64,iVBORw0KGgo=
...或对于文本数据,它看起来像这样
data:image/svg+xml,<svg></svg>%0A
请注意,此加载器默认情况下未启用。您需要为相应的文件扩展名配置它,如下所示
esbuild app.js --bundle --loader:.png=dataurl
require('esbuild').buildSync({
entryPoints: ['app.js'],
bundle: true,
loader: { '.png': 'dataurl' },
outfile: 'out.js',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Loader: map[string]api.Loader{
".png": api.LoaderDataURL,
},
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
#外部文件
有两种不同的加载器可用于外部文件,具体取决于您要查找的行为。两种加载器都在下面描述
#file
加载器
加载器:file
此加载器将文件复制到输出目录,并将文件名嵌入到捆绑包中作为字符串。此字符串使用默认导出导出。使用它看起来像这样
import url from './example.png'
let image = new Image
image.src = url
document.body.appendChild(image)
此行为与 Webpack 的 file-loader
包类似。请注意,此加载器默认情况下未启用。您需要为相应的文件扩展名配置它,如下所示
esbuild app.js --bundle --loader:.png=file --outdir=out
require('esbuild').buildSync({
entryPoints: ['app.js'],
bundle: true,
loader: { '.png': 'file' },
outdir: 'out',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Loader: map[string]api.Loader{
".png": api.LoaderFile,
},
Outdir: "out",
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
默认情况下,导出的字符串只是文件名。如果您想在导出的字符串前面加上基本路径,可以使用 公共路径 API 选项来完成此操作。
#copy
加载器
加载器:copy
此加载器会将文件复制到输出目录并重写导入路径以指向复制的文件。这意味着导入将在最终捆绑包中仍然存在,并且最终捆绑包仍然会引用该文件,而不是将文件包含在捆绑包中。如果您在 esbuild 的输出上运行其他捆绑工具,或者您想从捆绑包中省略一个很少使用的 data 文件以加快启动性能,或者您想依赖运行时触发的特定行为,这可能很有用。例如
import json from './example.json' assert { type: 'json' }
console.log(json)
如果您使用以下命令捆绑上面的代码
esbuild app.js --bundle --loader:.json=copy --outdir=out --format=esm
require('esbuild').buildSync({
entryPoints: ['app.js'],
bundle: true,
loader: { '.json': 'copy' },
outdir: 'out',
format: 'esm',
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Loader: map[string]api.Loader{
".json": api.LoaderCopy,
},
Outdir: "out",
Write: true,
Format: api.FormatESModule,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
生成的 out/app.js
文件可能看起来像这样
// app.js
import json from "./example-PVCBWCM4.json" assert { type: "json" };
console.log(json);
请注意,导入路径已重写为指向复制的文件 out/example-PVCBWCM4.json
(由于 资产名称 设置的默认值,已添加内容哈希),以及 导入断言 用于 JSON 已保留,以便运行时能够加载 JSON 文件。
#空文件
加载器:empty
此加载器告诉 esbuild 假装文件为空。在某些情况下,这可能是一种从捆绑包中删除内容的有效方法。例如,您可以将 .css
文件配置为使用 empty
加载,以防止 esbuild 捆绑导入到 JavaScript 文件中的 CSS 文件
esbuild app.js --bundle --loader:.css=empty
require('esbuild').buildSync({
entryPoints: ['app.js'],
bundle: true,
loader: { '.css': 'empty' },
})
package main
import "github.com/evanw/esbuild/pkg/api"
import "os"
func main() {
result := api.Build(api.BuildOptions{
EntryPoints: []string{"app.js"},
Bundle: true,
Loader: map[string]api.Loader{
".css": api.LoaderEmpty,
},
})
if len(result.Errors) > 0 {
os.Exit(1)
}
}
此加载器还允许您从 CSS 文件中删除导入的资产。例如,您可以将 .png
文件配置为使用 empty
加载,以便将 CSS 代码中对 .png
文件的引用(如 url(image.png)
)替换为 url()
。