Skip to content

宏是一種在打包時運行 JavaScript 函數的機制。這些函數返回的值直接內聯到您的包中。

作為一個簡單的例子,考慮這個返回隨機數的簡單函數。

ts
export function random() {
  return Math.random();
}

這只是常規文件中的常規函數,但我們可以這樣將它用作宏:

tsx
import { random } from "./random.ts" with { type: "macro" };

console.log(`Your random number is ${random()}`);

NOTE

宏使用導入屬性語法表示。如果您之前沒有見過此語法,這是一個 Stage 3 TC39 提案,讓您可以將額外的元數據附加到導入語句。

現在我們將使用 bun build 打包此文件。打包的文件將打印到 stdout。

bash
bun build ./cli.tsx
js
console.log(`Your random number is ${0.6805550949689833}`);

如您所見,random 函數的源代碼在包中不存在。相反,它在打包期間執行,函數調用(random())被替換為函數的結果。由於源代碼永遠不會包含在包中,宏可以安全地執行特權操作,如從數據庫讀取。

何時使用宏

如果您有幾個用於小事情的構建腳本,否則您將有一個一次性的構建腳本,打包時代碼執行更容易維護。它與您的其余代碼一起存在,與構建的其余部分一起運行,它自動並行化,如果失敗,構建也會失敗。

如果您發現自己在打包時運行大量代碼,請考慮改為運行服務器。

導入屬性

Bun 宏是使用以下任一注釋的導入語句:

  • with { type: 'macro' } — 導入屬性,Stage 3 ECMA Script 提案
  • assert { type: 'macro' } — 導入斷言,早期版本的導入屬性已被放棄(但已得到許多瀏覽器和運行時的支持)

安全考慮

宏必須顯式使用 { type: "macro" } 導入才能在打包時執行。這些導入在不被調用時沒有效果,這與可能具有副作用的常規 JavaScript 導入不同。

您可以通過向 Bun 傳遞 --no-macros 標志完全禁用宏。它會產生如下構建錯誤:

error: Macros are disabled

foo();
^
./hello.js:3:1 53

為了減少惡意包的潛在攻擊面,宏不能從 node_modules/**/* 內部調用。如果包嘗試調用宏,您將看到如下錯誤:

error: For security reasons, macros cannot be run from node_modules.

beEvil();
^
node_modules/evil/index.js:3:1 50

您的應用程序代碼仍然可以從 node_modules 導入宏並調用它們。

ts
import { macro } from "some-package" with { type: "macro" };

macro();

導出條件 "macro"

當將包含宏的庫發布到 npm 或其他包注冊表時,使用 "macro" 導出條件為宏環境提供特殊版本的包。

json
{
  "name": "my-package",
  "exports": {
    "import": "./index.js",
    "require": "./index.js",
    "default": "./index.js",
    "macro": "./index.macro.js"
  }
}

使用此配置,用戶可以使用相同的導入說明符在運行時或打包時使用包:

ts
import pkg from "my-package"; // 運行時導入
import { macro } from "my-package" with { type: "macro" }; // 宏導入

第一個導入將解析為 ./node_modules/my-package/index.js,而第二個將被 Bun 的打包器解析為 ./node_modules/my-package/index.macro.js

執行

當 Bun 的轉譯器看到宏導入時,它使用 Bun 的 JavaScript 運行時在轉譯器內部調用函數,並將返回值從 JavaScript 轉換為 AST 節點。這些 JavaScript 函數在打包時調用,而不是運行時。

宏在訪問階段在轉譯器中同步執行——在插件之前和轉譯器生成 AST 之前。它們按導入順序執行。轉譯器將等待宏完成執行後再繼續。轉譯器還將等待宏返回的任何 Promise。

Bun 的打包器是多線程的。因此,宏在多個生成的 JavaScript "worker" 中並行執行。

死代碼消除

打包器在運行和內聯宏後執行死代碼消除。因此給定以下宏:

ts
export function returnFalse() {
  return false;
}

...然後打包以下文件將產生空包,前提是啟用壓縮語法選項。

ts
import { returnFalse } from "./returnFalse.ts" with { type: "macro" };

if (returnFalse()) {
  console.log("This code is eliminated");
}

可序列化

Bun 的轉譯器需要能夠序列化宏的結果,以便它可以內聯到 AST 中。支持所有 JSON 兼容的數據結構:

ts
export function getObject() {
  return {
    foo: "bar",
    baz: 123,
    array: [1, 2, { nested: "value" }],
  };
}

宏可以是異步的,或返回 Promise 實例。Bun 的轉譯器將自動等待 Promise 並內聯結果。

ts
export async function getText() {
  return "async value";
}

轉譯器實現用於序列化常見數據格式(如 ResponseBlobTypedArray)的特殊邏輯。

  • TypedArray:解析為 base64 編碼字符串。
  • Response:Bun 將讀取 Content-Type 並相應地序列化;例如,類型為 application/json 的 Response 將自動解析為對象,text/plain 將作為字符串內聯。具有未識別或未定義類型的 Response 將進行 base-64 編碼。
  • Blob:與 Response 一樣,序列化取決於 type 屬性。

fetch 的結果是 Promise<Response>,因此可以直接返回。

ts
export function getObject() {
  return fetch("https://bun.com");
}

函數和大多數類的實例(上面提到的除外)不可序列化。

ts
export function getText(url: string) {
  // 這不起作用!
  return () => {};
}

參數

宏可以接受輸入,但僅限於有限情況。值必須是靜態已知的。例如,以下是不允許的:

ts
import { getText } from "./getText.ts" with { type: "macro" };

export function howLong() {
  // `foo` 的值無法靜態已知
  const foo = Math.random() ? "foo" : "bar";

  const text = getText(`https://example.com/${foo}`);
  console.log("The page is ", text.length, " characters long");
}

但是,如果 foo 的值在打包時已知(比如說,如果它是常量或另一個宏的結果),則允許:

ts
import { getText } from "./getText.ts" with { type: "macro" };
import { getFoo } from "./getFoo.ts" with { type: "macro" };

export function howLong() {
  // 這有效,因為 getFoo() 是靜態已知的
  const foo = getFoo();
  const text = getText(`https://example.com/${foo}`);
  console.log("The page is", text.length, "characters long");
}

這將輸出:

js
function howLong() {
  console.log("The page is", 1322, "characters long");
}
export { howLong };

示例

嵌入最新 git 提交哈希

ts
export function getGitCommitHash() {
  const { stdout } = Bun.spawnSync({
    cmd: ["git", "rev-parse", "HEAD"],
    stdout: "pipe",
  });

  return stdout.toString();
}

當我們構建它時,getGitCommitHash 被替換為調用函數的結果:

ts
import { getGitCommitHash } from "./getGitCommitHash.ts" with { type: "macro" };

console.log(`The current Git commit hash is ${getGitCommitHash()}`);
ts
console.log(`The current Git commit hash is 3ee3259104e4507cf62c160f0ff5357ec4c7a7f8`);

在打包時進行 fetch() 請求

在此示例中,我們使用 fetch() 發出傳出 HTTP 請求,使用 HTMLRewriter 解析 HTML 響應,並返回包含標題和元標簽的對象——全部在打包時完成。

ts
export async function extractMetaTags(url: string) {
  const response = await fetch(url);
  const meta = {
    title: "",
  };
  new HTMLRewriter()
    .on("title", {
      text(element) {
        meta.title += element.text;
      },
    })
    .on("meta", {
      element(element) {
        const name =
          element.getAttribute("name") || element.getAttribute("property") || element.getAttribute("itemprop");

        if (name) meta[name] = element.getAttribute("content");
      },
    })
    .transform(response);

  return meta;
}

extractMetaTags 函數在打包時被擦除並替換為函數調用的結果。這意味著 fetch 請求在打包時發生,結果嵌入在包中。此外,拋出錯誤的分支被消除,因為它是不可達的。

jsx
import { extractMetaTags } from "./meta.ts" with { type: "macro" };

export const Head = () => {
  const headTags = extractMetaTags("https://example.com");

  if (headTags.title !== "Example Domain") {
    throw new Error("Expected title to be 'Example Domain'");
  }

  return (
    <head>
      <title>{headTags.title}</title>
      <meta name="viewport" content={headTags.viewport} />
    </head>
  );
};
jsx
export const Head = () => {
  const headTags = {
    title: "Example Domain",
    viewport: "width=device-width, initial-scale=1",
  };

  return (
    <head>
      <title>{headTags.title}</title>
      <meta name="viewport" content={headTags.viewport} />
    </head>
  );
};

Bun學習網由www.bunjs.com.cn整理維護