內容類型

所有內建的內容類型都列在下方。每個內容類型都有關聯的「載入器」,用來告訴 esbuild 如何詮釋檔案內容。有些檔案副檔名已經預設設定了載入器,不過預設值可以被覆寫。

JavaScript

載入器:js

此載入器預設啟用於 .js.cjs.mjs 檔案。.cjs 副檔名由節點用於 CommonJS 模組,而 .mjs 副檔名由節點用於 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 低於以下版本時轉換 範例
指數運算子 es2016 a ** b
非同步函數 es2017 async () => {}
非同步迭代 es2018 for await (let x of y) {}
非同步產生器 es2018 async function* foo() {}
擴散屬性 es2018 let x = {...y}
剩餘屬性 es2018 let {...x} = y
可選的 catch 繫結 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 低於以下版本時不支援 範例
正規表示法 dotAll 旗標 es2018 /./s1
正規表示法後向斷言 es2018 /(?<=x)y/1
正規表示法命名擷取群組 es2018 /(?<foo>\d+)/1
正規表示法 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
Hashbang 語法 esnext #!/usr/bin/env node
裝飾器 esnext @foo class Bar {}
RegExp 集合表示法 esnext /[\w--\d]/1

另請參閱 已完成 ECMAScript 提案清單正在進行中的 ECMAScript 提案清單。請注意,雖然支援轉換包含頂層 await 的程式碼,但僅當 輸出格式 設定為 esm 時才支援套件化包含頂層 await 的程式碼。

JavaScript 注意事項

使用 esbuild 搭配 JavaScript 時,您應注意以下事項

ES5 支援不佳

目前不支援將 ES6+ 語法轉換為 ES5。不過,如果您使用 esbuild 轉換 ES5 程式碼,您仍應將 目標 設定為 es5。這可防止 esbuild 在您的 ES5 程式碼中引入 ES6 語法。例如,沒有這個 flag,物件文字 {x: x} 會在縮小時變成 {x},字串 "a\nb" 會變成多行範本文字。這兩個替換都是因為產生的程式碼較短,但如果 目標es5,則不會執行這些替換。

私有成員效能

私有成員轉換(針對 #name 語法)使用 WeakMapWeakSet 來保留此功能的私有特性。這與 Babel 和 TypeScript 編譯器中的對應轉換類似。大多數現代 JavaScript 引擎(V8、JavaScriptCore 和 SpiderMonkey,但 ChakraCore 除外)對於大型 WeakMapWeakSet 物件可能沒有良好的效能特性。

建立許多具有私有欄位或私有方法的類別實例,並使用此語法轉換,可能會為垃圾收集器造成大量負擔。這是因為現代引擎(ChakraCore 除外)將弱值儲存在實際的 map 物件中,而不是將其儲存在鍵本身的隱藏屬性中,而大型 map 物件可能會對垃圾收集造成效能問題。請參閱 此參考 以取得更多資訊。

匯入遵循 ECMAScript 模組行為

您可能會嘗試在匯入需要該全域狀態的模組之前修改全域狀態,並預期它會正常運作。然而,JavaScript(因此 esbuild)實際上會將所有 import 陳述式「提升」到檔案的頂端,因此這樣做不會奏效

window.foo = {}
import './something-that-needs-foo'

有一些 ECMAScript 模組的破損實作(例如 TypeScript 編譯器)不遵循 JavaScript 規格的這方面規定。使用這些工具編譯的程式碼可能會「運作」,因為 import 已替換為對 require() 的內嵌呼叫,而後者忽略了提升的要求。但此類程式碼無法與真實的 ECMAScript 模組實作(例如 node、瀏覽器或 esbuild)搭配使用,因此撰寫此類程式碼並不可移植,也不建議這麼做。

正確的做法是將全域狀態修改移至其自己的 import 中。這樣它在其他 import 之前執行

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() 取得函式的原始碼,然後嘗試將它用作 網頁工作執行緒 的主體時。如果您正在執行此操作,並且您想使用 esbuild,您應該在一個獨立的建置步驟中建置網頁工作執行緒的原始碼,然後將網頁工作執行緒原始碼作為字串插入到建立網頁工作執行緒的程式碼中。 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 結尾的檔案中,或者您需要將 "type": "module" 新增到您的 package.json 檔案中。原因是 Node 的原生 ESM 支援只能在檔案副檔名為 .mjs 或存在 "type": "module" 時執行 ESM 程式碼,因此這樣做是一個良好的訊號,表示程式碼打算在 Node 中執行,因此應使用 default 匯入的 Node 解譯。這是 Webpack 使用的相同啟發法。

TypeScript

載入器:tstsx

此載入器預設啟用 .ts.tsx.mts.cts 檔案,這表示 esbuild 內建支援剖析 TypeScript 語法並捨棄類型註解。然而,esbuild 不會 進行任何類型檢查,因此您仍需要與 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')。推測這是因為 TypeScript 採用此語法時,ECMAScript 模組仍是提案。然而,這是一種舊有行為,與此語法在 node 等實際平台上的行為不符。例如,require 函式可以傳回任何 JavaScript 值,包括字串,但 import * as 語法總是會傳回物件,且無法是字串。

若要避免因這項舊有功能而產生的問題,請在將 TypeScript 與 esbuild 搭配使用時,啟用 esModuleInterop TypeScript 設定選項。啟用此選項會停用這項舊有行為,並使 TypeScript 的類型系統與 ESM 相容。此選項預設未啟用,因為這會對現有的 TypeScript 專案造成重大變更,但 Microsoft 強烈建議將其套用至新舊專案(然後更新您的程式碼),以提升與其他生態系統的相容性。

具體來說,這表示從 CommonJS 模組匯入非物件值時,必須使用預設匯入,而非使用 import * as。因此,如果 CommonJS 模組透過 module.exports = fn 匯出函式,您需要使用 import fn from 'path',而非 import * as fn from 'path'

需要類型系統的功能不受支援

esbuild 會將 TypeScript 類型視為註解並加以忽略,因此 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 欄位(即上述清單中沒有的欄位)都將被忽略。

您無法對 *.ts 檔案使用 tsx 載入器

tsx 載入器不是 ts 載入器的超集。它們是兩個不同的部分不相容的語法。例如,字元序列 <a>1</a>/gts 載入器中會解析為 <a>(1 < (/a>/g)),在 tsx 載入器中會解析為 (<a>1</a>) / g

這導致的最常見問題是無法在箭頭函式表達式上使用泛型類型參數,例如 <T>() => {}tsx 載入器。這是故意的,並符合官方 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 檔案啟用。請注意,預設情況下不會在 .js 檔案中啟用 JSX 語法。如果您想啟用,您需要設定它

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 函式庫 import 到每個檔案,您可以透過將 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,您可以透過將此新增到您的 tsconfig.json 檔案來設定 TypeScript 的 JSX,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(對於 CSS 模組 還有 global-csslocal-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)
  }
}

您可以使用 url() @import 其他 CSS 檔案並參考影像和字型檔案,esbuild 會將所有內容套件在一起。請注意,您必須為影像和字型檔案設定載入器,因為 esbuild 沒有任何預先設定的載入器。通常這是 資料 URL 載入器或 外部檔案 載入器。

這些語法功能會根據已設定的語言 目標 有條件地轉換為舊瀏覽器

語法轉換 範例
巢狀宣告 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 Color Module Level 4 中的語法。如果不需要這樣,您必須指定 esbuild 的 target 設定,以說明您需要輸出在哪些瀏覽器中正確運作。然後,esbuild 將避免使用對這些瀏覽器來說過於現代的 CSS 功能。

當您使用 target 設定提供瀏覽器版本清單時,esbuild 還會自動插入供應商前綴,以便您的 CSS 能在這些瀏覽器中於這些版本或更新版本中運作。目前,esbuild 會對下列 CSS 屬性執行此操作

從 JavaScript 匯入

您也可以從 JavaScript 匯入 CSS。當您這樣做時,esbuild 將收集從給定進入點引用的所有 CSS 檔案,並將其打包到 JavaScript 進入點的 JavaScript 輸出檔案旁的兄弟 CSS 輸出檔案中。因此,如果 esbuild 產生 app.js,它也會產生 app.css,其中包含 app.js 引用的所有 CSS 檔案。以下是從 JavaScript 匯入 CSS 檔案的範例

import './button.css'

export let Button = ({ text }) =>
  <div className="button">{text}</div>

esbuild 所產生的 JavaScript 捆綁檔不會自動將產生的 CSS 匯入到 HTML 頁面中。相反地,您應該自行將產生的 CSS 匯入到 HTML 頁面中,連同產生的 JavaScript。這表示瀏覽器可以並行下載 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 名稱的唯一方式是使用與 esbuild 捆綁在一起的 JS 中的 import 陳述式,如上所示。例如,當啟用 縮小 時,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 沒有錯字,你應該使用 CSS linter,以及 esbuild。

@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.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 的輸出上執行其他套件工具,或者想要從套件中省略一個鮮少使用的資料檔案以加快啟動效能,或者想要依賴由匯入觸發的執行時期的特定行為,這可能會很有用。例如

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()