专业编程教程与实战项目分享平台

网站首页 > 技术文章 正文

运行时 Bun 又添王炸,支持 Bun宏!

ins518 2024-09-24 18:02:44 技术文章 14 ℃ 0 评论

家好,很高兴又见面了,我是"高级前端?进阶?",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

今天给大家带来的主题是运行时 Bun v0.6.0版本发布后又添王炸,即支持Bun宏。关于JavaScript运行时的发展,以前有很多文章重点介绍过,下面是我已发布文章的传送门:

对于每个运行时不了解的可以仔细阅读上面的文章,甚至可以关注我在头条发布的关于运行时的合集。话不多说,直接进入正题。

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

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表