天天看點

運作時 Bun 又添王炸,支援 Bun宏!

大家好,很高興又見面了,我是"進階前端‬進階‬",由我帶着大家一起關注前端前沿、深入前端底層技術,大家一起進步,也歡迎大家關注、點贊、收藏、轉發!

運作時 Bun 又添王炸,支援 Bun宏!

今天給大家帶來的主題是運作時 Bun v0.6.0版本釋出後又添王炸,即支援Bun宏。關于JavaScript運作時的發展,以前有很多文章重點介紹過,下面是我已釋出文章的傳送門:

  • 《 JS Runtime vs. JS Engine!Deno/Bun/Node是運作時!》
  • 《 前有Deno、後有Bun、Node.js已窮途末路?》
  • 《 Node.js已死!Bun永生?》
  • 《 Node.js、Deno、Bun 6大典型場景性能大PK?》
  • 《 Deno v1.34 釋出!全面擁抱 npm 生态 》
  • 《 盤點全網最火的6+ JavaScript 運作時!Node/Deno/Bun 在列! 》

對于每個運作時不了解的可以仔細閱讀上面的文章,甚至可以關注我在頭條釋出的關于運作時的合集。話不多說,直接進入正題。

1.Bun v0.6.0 新特性概覽

Bun v0.6 号稱是迄今為止最大的 Bun 版本更新,在 v0.6 版本中,Bun 内置了一個 JavaScript 和 TypeScript 打包器和代碼壓縮器。可以使用 Bun v0.6 來打包前端應用程式或将代碼打包到獨立的可執行檔案中。

同時,Bun 官方團隊還一如既往地忙于提高運作時性能和修複錯誤,比如:

  • writeFile() 在 Linux 上的速度提高了 20%
  • 對 Node.js 相容性和 Web API 相容性的大量錯誤修複
  • 對 TypeScript 5.0 文法的支援
  • 修複 bun install 引起的各種問題

借助于這個新的 Bun 運作時,Bun 的代碼庫已經包含了快速解析、轉換源代碼的大量基礎工作(在 Zig 中實作)。打包測試結果表明: 在基準測試中(源自 esbuild 的 three.js 基準測試),Bun 比 esbuild 快 1.75 倍,比 Parcel 2 快 150 倍,比 Rollup + Terser 快 180 倍,比 Webpack 快 220 倍,比 rsPack 快 26 倍。

運作時 Bun 又添王炸,支援 Bun宏!

目前 Bun 在 Github 上有超過 41.7k 的 star、1.1k 的 fork、55.2k 的項目依賴量,代碼貢獻者 234+,妥妥的前端頂級開源項目。同時 Bun 在 2023 年也被評為最火的前端 10 大明星開源項目,引起了前端開發者的無限熱情。

2.什麼是 Bun 宏(Macros)

兩周前,Bun 官方釋出了 v0.6.0 版本, 推出了新的 JavaScript 打包器。 2023 年 5 月 31 日,Bun 官方又釋出了一項新功能,實作了 Bun 打包程式和運作時之間的緊密內建,即 Bun 宏(Bun Macros)。

運作時 Bun 又添王炸,支援 Bun宏!

宏是一種在打包時運作 JavaScript 函數的機制。 從這些函數傳回的值直接内聯到 JavaScript 包中。

比如下面的示例,其表示傳回随機數的簡單函數:

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

在源代碼中,可以使用 import 屬性文法将此函數導入為宏。如果以前沒有見過這種文法,它是第 3 階段 TC39 提案,可讓開發者将額外的中繼資料附加到 import 語句。

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

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

現在可以将這個檔案與 bun build 打包在一起,打封包件将列印到标準輸出。

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

如上面所見,随機函數的源代碼在包中沒有出現。 相反,它在打包期間執行,并且函數調用 (random()) 被替換為函數的結果。 由于源代碼永遠不會包含在最終的包中,是以宏可以安全地執行特權操作,例如:從資料庫中讀取。

3.何時使用宏

對于本來可以使用一次性建構腳本的事情,打包時候代碼執行可以更容易維護。 它與其餘代碼一起存在,與建構的其餘部分一起運作,自動并行化,如果失敗,建構也會失敗。

但是,如果發現自己在打包時需要運作大量代碼,可以考慮運作一個伺服器,接下來一起看看宏的一些使用場景。

嵌入最新的 git commit hash

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

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

下面是 getGitCommitHash.ts 的内容:

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

  return stdout.toString();
}           

當建構代碼時,getGitCommitHash 被替換為調用函數的結果:

//output.js
console.log(`The current Git commit hash is 3ee3259104f`);
// cli中輸入下面内容
bun build --target=browser ./in-the-browser.ts           

在打包時發出 fetch() 請求

在下面的示例中,使用 fetch() 發出 HTTP 請求,使用 HTMLRewriter 解析 HTML 響應,并傳回一個包含标題和元标記的對象,所有這些操作都是在打包時進行的。

// in-the-browser.tsx
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>;
};           

下面是 meta.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 請求發生在打包時,結果嵌入到打包産物中。 此外,抛出錯誤的分支也會被删除,因為它無法通路。

output.js 内容如下(打包指令為: bun build --target=browser --minify-syntax ./in-the-browser.ts ):

import { jsx, jsxs } from "react/jsx-runtime";
export const Head = () => {
  jsxs("head", {
    children: [
      jsx("title", {
        children: "Example Domain",
      }),
      jsx("meta", {
        name: "viewport",
        content: "width=device-width, initial-scale=1",
      }),
    ],
  });
};

export { Head };           

4.Bun 宏如何工作

import 屬性

Bun Macros 在 import 語句中注釋了 {type: 'macro'},比如下面的示例:

import { myMacro } from './macro.ts' with { type: 'macro' }           

import 屬性是第 3 階段 ECMAScript 提案,這意味着極有可能被添加為 JavaScript 語言的官方部分。當然,Bun 還支援導入斷言文法,導入斷言是導入屬性的早期化身,現在已廢棄(但已經被許多浏覽器和運作時支援)。

import { myMacro } from "./macro.ts" assert { type: "macro" };
// Chrome>91、Edge>91、Safari>15、火狐、Opera不支援,整體支援率87.42%
// 但是目前已經廢棄           

當 Bun 的轉譯器遇到這種特殊 import 時,它會使用 Bun 的 JavaScript 運作時調用轉譯器内部的函數,并将 JavaScript 的傳回值轉換為 AST 節點。 這些 JavaScript 函數是在打包時調用的,而不是運作時。

宏的執行順序

Bun 宏在周遊階段在轉譯器中同步執行,但是發生在插件和轉譯器生成 AST 之前。 它們按照被調用的順序執行。 轉譯器将等待宏完成執行後再繼續,轉譯器還将等待宏傳回的任何 Promise。

Bun 的打包器是多線程的, 是以,宏在多個生成的 JavaScript“worker”中并行執行。

死代碼消除

打包器在運作和内聯宏後執行無用代碼消除,是以給定以下宏:

export function returnFalse() {
  return false;
}
           

然後打包以下檔案将産生一個空的 bundle。

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

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

5.宏的安全性

支援禁用

宏必須使用 { type: "macro" } 顯式導入才能在打包時執行。 如果不調用這些導入,則不會産生任何效果,這與可能有副作用的正常 JavaScript 導入不同。

開發者可以通過将 --no-macros 标志傳遞給 Bun 來完全禁用宏,它會産生下面的建構錯誤:

error: Macros are disabled

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

宏在 node_modules 中被禁用

為了減少惡意包的潛在攻擊面,不能從 node_modules/**/*内部調用宏。如果包試圖調用宏,将看到如下錯誤抛出:

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

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

應用程式代碼仍然可以從 node_modules 導入宏并調用它們:

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

macro();           

6.宏的限制

宏的結果必須是可序列化的

Bun 的轉譯器需要能夠序列化宏的結果,以便将其内聯到 AST 中。不過,目前支援所有 JSON 相容的資料結構:

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

宏可以是異步的,也可以傳回 Promise 執行個體, Bun 的轉譯器将自動等待 Promise 并内聯結果。

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

轉譯器實作了用于序列化常見資料格式(如 Response、Blob、TypedArray)的特殊邏輯。

  • TypedArray:解析為 base64 編碼的字元串。
  • Response:在相關的地方,Bun 将讀取 Content-Type 并相應地序列化; 例如,類型為 application/json 的 Response 将被自動解析為一個對象,而 text/plain 将被内聯為一個字 符串。 具有未知或未定義類型的響應将采用 base-64 編碼。
  • Blob:與 Response 一樣,序列化取決于 type 屬性

fetch 的結果是 Promise,是以可以直接傳回。

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

大多數類的函數和執行個體(除了上面提到的那些)是不可序列化的,比如:

export function getText(url: string) {
  // 這行不通!
  return () => {};
}           

輸入參數必須是靜态可分析的

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

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 的值在打包時已知(例如,如果它是常量或另一個宏的結果),那麼它是允許的:

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");
}           

将會輸出以下結果:

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

7.本文總結

本文主要和大家介紹下 JavaScript 運作時 Bun v0.6.0版本釋出後又添王炸,即支援Bun宏。相信通過本文的閱讀,大家對 Bun宏 都會有一個初步的了解。

因為篇幅有限,文章并沒有過多展開,如果有興趣,可以在我的首頁繼續閱讀,同時文末的參考資料提供了大量優秀文檔以供學習。最後,歡迎大家點贊、評論、轉發、收藏!

參考資料

https://bun.sh/blog/bun-macros#make-fetch-requests-at-bundle-time

https://github.com/tc39/proposal-import-attributes

https://caniuse.com/mdn-javascript_statements_import_import_assertions

https://bun.sh/blog/bun-v0.6.0

https://www.toutiao.com/article/7234320367169913347/

https://github.com/oven-sh/bun