大家好,很高興又見面了,我是"進階前端進階",由我帶着大家一起關注前端前沿、深入前端底層技術,大家一起進步,也歡迎大家關注、點贊、收藏、轉發!
今天給大家帶來的主題是運作時 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 在 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)。
宏是一種在打包時運作 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