内容类型

下面列出了所有内置的内容类型。每个内容类型都与一个关联的“加载器”相关联,该加载器告诉 esbuild 如何解释文件内容。一些文件扩展名默认情况下已经配置了加载器,尽管默认值可以被覆盖。

JavaScript

加载器:js

此加载器默认情况下为 .js.cjs.mjs 文件启用。.cjs 扩展名由 node 用于 CommonJS 模块,而 .mjs 扩展名由 node 用于 ECMAScript 模块。

请注意,默认情况下,esbuild 的输出将利用所有现代 JS 功能。例如,a !== void 0 && a !== null ? a : b 在启用最小化时将变为 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 /./s1
RegExp 后向断言 es2018 /(?<=x)y/1
RegExp 命名捕获组 es2018 /(?<foo>\d+)/1
RegExp Unicode 属性转义 es2018 /\p{ASCII}/u1
BigInt es2020 123n
顶层 await es2022 await import(x)
任意模块命名空间标识符 es2022 export {foo as 'f o o'}
RegExp 匹配索引 es2022 /x(.+)y/d1
哈希邦语法 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" 在最小化时将变为多行模板字面量。这两个替换都是因为生成的代码更短,但如果 targetes5,则不会执行这些替换。

私有成员性能

私有成员转换(针对 #name 语法)使用 WeakMapWeakSet 来保留此功能的隐私属性。这类似于 Babel 和 TypeScript 编译器中的相应转换。大多数现代 JavaScript 引擎(V8、JavaScriptCore 和 SpiderMonkey,但不是 ChakraCore)可能对大型 WeakMapWeakSet 对象没有良好的性能特征。

使用此语法转换激活创建具有私有字段或私有方法的类的许多实例可能会导致垃圾收集器产生大量开销。这是因为现代引擎(除了 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; return eval('y') 将返回 123

这称为“直接 eval”,在捆绑代码时存在问题,原因如下

幸运的是,通常很容易避免使用直接 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: __pow is not defined 的错误,因为函数 (a, b) => __pow(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';

以下是两种解释,这两种解释都被广泛使用

如果您是库作者: 在编写新代码时,您应该认真考虑完全避免使用 default 导出。它不幸地被兼容性问题所污染,使用它很可能会在某个时候给您的用户带来问题。

如果您是库用户: 默认情况下,esbuild 将使用 Babel 解释。如果您希望 esbuild 改用 Node 解释,您需要将代码放在以 .mts.mjs 结尾的文件中,或者您需要在 package.json 文件中添加 "type": "module"。其理由是,Node 的原生 ESM 支持只能在文件扩展名为 .mjs 或存在 "type": "module" 的情况下运行 ESM 代码,因此这样做是一个很好的信号,表明代码旨在在 Node 中运行,因此应该使用 Node 对 default 导入的解释。这是 Webpack 使用的相同启发式方法。

TypeScript

加载器:tstsx

此加载器默认情况下为 .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+
extendsinfer 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 {T} from './types' 从另一个模块重新导出类型(您需要改用 export type {T} from './types')。

导入遵循 ECMAScript 模块行为

出于历史原因,TypeScript 编译器默认情况下会将 ESM(ECMAScript 模块)语法编译为 CommonJS 语法。例如,import * as foo from 'foo' 会编译为 const foo = require('foo')。据推测,这是因为 ECMAScript 模块在 TypeScript 采用该语法时仍然是一个提案。但是,这是与该语法在 node 等真实平台上的行为不匹配的遗留行为。例如,require 函数可以返回任何 JavaScript 值,包括字符串,但 import * as 语法始终会导致对象,而不能是字符串。

为了避免由于此遗留功能而导致的问题,如果您将 TypeScript 与 esbuild 一起使用,则应启用 esModuleInterop TypeScript 配置选项。启用它会禁用此遗留行为,并使 TypeScript 的类型系统与 ESM 兼容。此选项默认情况下未启用,因为它会对现有的 TypeScript 项目造成重大更改,但 Microsoft 强烈建议将其应用于新项目和现有项目(然后更新您的代码),以更好地与生态系统的其他部分兼容。

具体来说,这意味着使用 ESM 导入语法从 CommonJS 模块导入非对象值必须使用默认导入,而不是使用import * as。因此,如果一个 CommonJS 模块通过module.exports = fn导出一个函数,你需要使用import fn from 'path'而不是import * as fn from 'path'

需要类型系统的功能不受支持

TypeScript 类型被视为注释,esbuild 会忽略它们,因此 TypeScript 被视为“类型检查的 JavaScript”。类型注释的解释由 TypeScript 类型检查器完成,如果你使用 TypeScript,你应该在 esbuild 之外运行它。这与 Babel 的 TypeScript 实现使用的相同编译策略。但是,这意味着某些需要类型解释才能工作的 TypeScript 编译功能无法与 esbuild 一起使用。

具体来说

只尊重某些 tsconfig.json 字段

在捆绑过程中,esbuild 中的路径解析算法将考虑包含 tsconfig.json 文件的最近父目录的内容,并相应地修改其行为。也可以使用 esbuild 的tsconfig设置通过构建 API 显式设置 tsconfig.json 路径,并使用 esbuild 的tsconfigRaw设置通过转换 API 显式传入 tsconfig.json 文件的内容。但是,esbuild 目前只检查 tsconfig.json 文件中的以下字段

所有其他 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

加载器:jsxtsx

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 文件中未启用。如果你想启用它,你需要配置它

CLI JS Go
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.createElement 的调用,但它本身不会导入任何内容,因此 React 变量不会自动存在。

如果你想避免必须手动将 JSX 库导入到每个文件中,你可能可以通过将 esbuild 的JSX转换设置为 automatic 来做到这一点,它会为你生成导入语句。请记住,这也会完全改变 JSX 转换的工作方式,因此如果你使用的是非 React 的 JSX 库,它可能会破坏你的代码。这样做看起来像这样

CLI JS Go
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.createElementReact.Fragment

CLI JS Go
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 {h, Fragment} from 'preact',除非你使用上面描述的自动导入。

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-csslocal-css,用于CSS 模块

css 加载器默认情况下为 .css 文件启用,local-css 加载器默认情况下为 .module.css 文件启用。这些加载器将文件加载为 CSS 语法。CSS 是 esbuild 中的一级内容类型,这意味着 esbuild 可以直接捆绑 CSS 文件,而无需从 JavaScript 代码中导入 CSS

CLI JS Go
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: rgba(255, 0, 0, 0.4) 在启用压缩时将变为 color: #f006,它使用了来自 CSS 颜色模块级别 4 的语法。如果这是不希望的,您必须指定 esbuild 的 目标 设置,以说明您需要输出在哪些浏览器中正常工作。然后 esbuild 将避免使用对这些浏览器来说过于现代的 CSS 功能。

当您使用 目标 设置提供浏览器版本列表时,esbuild 还会自动插入供应商前缀,以便您的 CSS 在这些浏览器版本或更高版本中正常工作。目前 esbuild 将对以下 CSS 属性执行此操作

从 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 --bundle --outdir=out 捆绑它时,您将得到以下结果(注意本地 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 名称变为本地。但是,有时您希望在同一个文件中混合本地名称和全局名称。有几种方法可以做到这一点

以下是一些示例

/*
 * 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 app.js --bundle --outdir=dist --loader:.css=local-css 捆绑它将为您提供类似以下内容

(() => {
  // 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 属性。还要注意,使用 composesfrom 允许您(间接地)引用其他 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 大致意味着“在此处重新评估导入的文件”。例如,考虑以下文件

根据您对 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.buffer。请注意,此加载器默认情况下未启用。您需要为相应的文件扩展名配置它,如下所示

CLI JS Go
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)

请注意,此加载器默认情况下未启用。您需要为相应的文件扩展名配置它,如下所示

CLI JS Go
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)
  }
}

如果您打算将其转换为 Uint8ArrayArrayBuffer,您应该使用 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

请注意,此加载器默认情况下未启用。您需要为相应的文件扩展名配置它,如下所示

CLI JS Go
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 包类似。请注意,此加载器默认情况下未启用。您需要为相应的文件扩展名配置它,如下所示

CLI JS Go
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)

如果您使用以下命令捆绑上面的代码

CLI JS Go
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 文件

CLI JS Go
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()