Bun技术评估 - 17 Bun 1.2(上)

42 阅读11分钟

概述

本文是笔者的系列博文 《Bun技术评估》 中的第十七篇。

在本系列成文过程中,笔者发现,bun仍然还只是一个在快速发展和演进的系统。其中,Bun的1.2版本,看起来是一个比较重要的,标志性的系统,从这个版本开始,bun可以被认为是一个相对比较成熟的体系了。所以其在首页上,特别标注了这个版本的发布。

B1.png

同时,官方网站上还有一个配套的博客专门讨论了这个问题,笔者也觉得是非常有必要了解一下的,也是本文主要要探讨的内容。这个博客的链接如下:

bun.com/blog/bun-v1…

看了这篇博客之后,笔者发现,很多内容我们前面其实已经讨论过了,考虑到这些讨论都是基于最新版本(当前是1.2.18)进行的,看起来这个1.2版本确实是一个重量级的版本。已经讨论过的内容,笔者就不再重复了,文中内容主要涉及到Bun的新进展和一些比较边缘和系统化的问题。

此外,基于叙述的重点和方便,本文并不是原文的简单翻译和重现,而是根据需要调整了相关的结构和内容。并且,由于内容过多和篇幅的限制,笔者将本文分成了上下两篇(本文是上篇)。

Bun 1.2

概述中提到的博客,其实算是一个Bun 1.2版本的发布说明,成文日期是2025年1月22日,有很强的时效性。文中说明,这个版本是一个比较大的升级(Huge Update),核心要点如下:

  • 大大提升了Node.js的兼容性
  • 内置了S3存储API: Bun.s3
  • 内置了Postgres客户端: Bun.sql,很快会有兼容的MySQL客户端
  • 使用了文本化的锁文件: bun.lock
  • 性能改进,如实现了对于Express性能3倍的优势

随后我们将会了解到一些比较重大的功能增加和调整升级。

Nodejs兼容性

这个问题笔者没有专门研究和讨论过,所以这里刚好有一个很好的机会来促使我们来理解这个问题。

兼容性项目和分值

按照其对自己的兼容性评估,bun1.2相对于Nodejs的兼容性项目和分值如下:

83919108-1bfa-41e3-9001-fa9624f3c554.png

看起来,现在的状态还不错,在大多数板块中,都实现了90%以上的兼容性。那么,bun是如何评估和Nodejs之间的兼容性的问题的呢?

兼容性评估和测试方法

其实,这个兼容性评估方式本身,就是1.2版本的重大改进。

在之前的版本中,bun通常根据用户使用报告对Node.js错误进行优先级排序和修复(这些报告通常来自GitHub问题,特别是其中有人试图使用在Bun中无法使用的npm包),这种方式有点像一个无休止的“打地鼠”游戏,虽然很多问题是确实存在重要的,但对于真正的代码和功能兼容性提升帮助有限。

而现在改进的兼容性评估和演进的主要策略是,迁移和使用Node.js测试套件。这样可以在逻辑上做到和Nodejs原生代码几乎相同的兼容性,因为确实,在不同的版本之间,nodejs本身其实也存在一些兼容的问题,这个策略可以保证和nodejs主线发展的动态兼容。

具体而言,nodejs的代码仓库中有上千个测试文件,大部分位于 test/parallel 文件夹中(图)。虽然看起来只需 "运行" 这些测试就可以得到测试结果,但实际上比你想的要复杂得多。很多代码需要为Bun test进行相关的调整,这些具体内容,不是本文的重点。我们只需要理解,这一个持续进行和改进的过程就可以了。

ee1c977f-8d85-4038-9cab-8f38a1bc81d6.png

在具体实现的更新方面,bun1.2提供了如下比较新的node模块的兼容支持:

nodejs:http2

Bun并没有原生支持HTTP2,而是实现了nodejs:http2的兼容版本。而且现在实现的版本,完整的包括了客户端服务器。下面是一个简单示例:


import { createSecureServer } from "node:http2";
import { readFileSync } from "node:fs";

const server = createSecureServer({
  key: readFileSync("key.pem"),
  cert: readFileSync("cert.pem"),
});

server.on("stream", (stream, headers) => {
  stream.respond({
    ":status": 200,
    "content-type": "text/html; charset=utf-8",
  });
  stream.end("<h1>Hello from Bun!</h1>");
});

server.listen(3000);

这里笔者有一个疑问,就是现有的Bun内置Serve方法,是否能够在不改变编程形式的前提下,可以直接改造成为支持HTTP2,并且可以向下兼容老版的客户端(可以仅限制在API调用方式)。现在这个问题笔者还没有明确的答案。

nodejs:dgram

这个模块用于支持UDP的应用,这是一个比较基础和底层的特性,是UDP通信的基础。下面是一个简单的示例:

import { createSocket } from "node:dgram";

const server = createSocket("udp4");
const client = createSocket("udp4");

server.on("listening", () => {
  const { port, address } = server.address();
  for (let i = 0; i < 10; i++) {
    client.send(`data ${i}`, port, address);
  }
  server.unref();
});

server.on("message", (data, { address, port }) => {
  console.log(`Received: data=${data} source=${address}:${port}`);
  client.unref();
});

server.bind();


这个测试代码我们可以看到,bun支持UDP客户端和服务端的实现,是完备的。

node:cluster

现在,bun可以几乎完整的支持node:cluster的应用模式。这个问题笔者已经在另一篇博文中进行了探讨。

《 Bun技术评估 - 13 Worker和Child Process》

node:zlib

在Bun 1.2, 使用原生代码重写原来基于JavaScript的node:zlib库。修复了一些缺陷,支持了新的Brotli压缩算法,并且相对1.1版本的性能也提升了一倍。

cc3760d5-352c-4f7a-a75b-2f48fec52dfa.png

Bun zlib的简单应用方式如下:


import { brotliCompressSync, brotliDecompressSync } from "node:zlib";

const compressed = brotliCompressSync("Hello, world!");
compressed.toString("hex"); // "0b068048656c6c6f2c20776f726c642103"

const decompressed = brotliDecompressSync(compressed);
decompressed.toString("utf8"); // "Hello, world!"

当然,一般情况下zlib不会单独应用,而是会作为基础内嵌在一些应用场景当然,比如HTTP的数据传输,就可以选择透明的对传输信息进行压缩和解压,来节省传输流量。

V8 C++ API

这个特性还是挺奇怪的。因为我们知道,Bun和Nodejs的一个很大的差异在于它们使用不同的执行引擎(JSC vs V8)。这样就会导致一些比较基础和底层的npm,由于要应用到V8的一些特性和API,而可能无法在Bun的早期版本中执行。例如下面这段代码:

const features = require('cpu-features')();
console.log(features);

dyld[94465]: missing symbol called
fish: Job 1, 'bun index.ts' terminated by signal SIGABRT (Abort)

为了解决这个问题,bun开发团队进行了前所未有的工程工作,在JavaScriptCore中实现了V8的公共C++ API,因此很多程序包就可以简单的在Bun中运行了。

bun v8.ts
{
  arch: "aarch64",
  implementer: 65,
  variant: 0,
  part: 3336,
  revision: 2,
  flags: {
    fp: true,
    asimd: true,
    evtstrm: true,
    aes: true,
    pmull: true,
    sha1: true,
    sha2: true,
    crc32: true,
  },
}

这些程序的运行虽然确实不需要V8,但要理解。V8 C++ API 支持非常复杂,因此大多数软件包仍然会缺少功能。Bun技术团队仍然在继续改进支持,保证像node-canvas@v2和node-sqlite3这样的软件包将来可以正常工作。

node:v8

除了上面对于V8 C++ API的一些支持之外,bun实现了部分node:v8本身一些特性的支持,下面是一个简单的示例:

import { writeHeapSnapshot } from "node:v8";

// Writes a heap snapshot to the current working directory in the form:
// `Heap-{date}-{pid}.heapsnapshot`
writeHeapSnapshot();

这个示例可以输出堆内存的镜像,随后可以在Chrome DevTGools中加载分析使用:

d953c24c-b124-4baf-95e1-60482cb2d8e0.jpg

S3支持

在bun1.2中,原生提供了S3和其兼容系统的支持(如MINIO),这个主题我们已经在前面进行了探讨,详见:

《Bun技术评估 - 07 S3》

Postgres和SQLite支持

对Postgres的支持,也是Bun1.2的一个重要的特性的改进。在初始规划中,bun已经能够提供SQLite的支持。在新的版本中,增加了对PG的原生支持,随后很快还会增加MySQL支持,这样可以基本完整覆盖主流互联网Web应用的大部分场景。

和笔者预期的实现方式不太一样, bun是通过Bun.sql类(而非如Bun.pg?)来实现PG客户端和Postgres数据库的支持的,按照它的说法,Bun.sql虽然借鉴了pg npm的应用方式,但是基于完全的原生代码实现并进行了优化,主要包括:

  • Automatic prepared statements,自动化预备语句
  • Query pipelining,查询管线
  • Binary wire protocol support,二进制协议支持
  • Connection pooling,连接池
  • Structure caching,数据结构缓存

这些优化的结果就是在bun中对于postgres应用的开发更加简单方便,同时如果使用得当,就能得到更好的性能。并且由于使用的方式接近,移植来自pg npm的代码,也是非常简单的。

关于其他Bun.sql和对postgres数据库支持的具体内容,笔者在前面也有专门的博文讨论:

《Bun技术评估 - 05 SQL》

在SQLite方面,1.2版本也有不少改进,但由于笔者觉得这个数据库高强度使用的机会很少,就不展开探讨,读者有兴趣可以自己查阅相关文档。

Test 测试

我们已经知道,bun内置了一个简单易用的测试框架,可以用于JavaScript、TypeScript和JSX应用程序的测试工作。它使用和Jest/Vitest相同的API,让开发者可以快速的适应和移植。在新的1.2版本中,相关的增强和改进如下:

  • JUnit support

更好的JUnit支持。在更通用的CI/CD工具链中,JUnit可能是一个更普适的标准。Bun可选将测试结果,生成JUnit兼容的报告形式(使用 --reporter 参数)。

bun test --reporter=junit --reporter-outfile=junit.xml

// 报告示例
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="bun test" tests="1" assertions="1" failures="1" time="0.001">
  <testsuite name="index.test.ts" tests="1" assertions="1" failures="1" time="0.001">
    <!-- ... -->
  </testsuite>
</testsuites>

// 配置形式
[test.reporter]
junit = "junit.xml"

  • LCOV 支持

1.2版本中增加了LCOV格式的测试覆盖报告。这个格式被用于很多测试覆盖报告分析工具,如Codecov等等。

  • inline snapshots 内嵌快照

一般的toMatchSnapshot方法,会将测试快照存储在单独的文件中。新的toMatchInlineSnapshot方法,可以将快照信息,直接内置在测试用例代码当中。让测试代码更加直观清晰。

下面是一个简单的示例:

// 内嵌快照生成
import { expect, test } from "bun:test";

test("toMatchInlineSnapshot()", () => {
  expect(new Date()).toMatchInlineSnapshot();
});

// 执行快照更新测试, update snapshot 
bun test -u

// 自动修改的测试代码
test("toMatchInlineSnapshot()", () => {
--   expect(new Date()).toMatchInlineSnapshot();
++   expect(new Date()).toMatchInlineSnapshot(`2025-01-18T02:35:53.332Z`);
});

  • test.only

在开发阶段,开发者可能希望只专注于当前代码或者模块的测试,这时可以使用only测试选项,它将只运行指定的测试用例。在1.2中,这种测试更加简单,程序会自动识别并允许需要的only测试。

import { test } from "bun:test";

test.only("test a", () => {
  /* Only run this test  */
});

test("test b", () => {
  /* Don't run this test */
});

// 旧版本
bun test --only

// 新版本
bun test

  • 新的匹配器

1.2版本中增加了一些新的匹配器,让测试编写更加方便,或者适应更多的测试逻辑需求。

// 各种Contain匹配器
const object = new Set(["bun", "node", "npm"]);

expect(object).toContainValue("bun");
expect(object).toContainValues(["bun", "node"]);
expect(object).toContainAllValues(["bun", "node", "npm"]);
expect(object).not.toContainAnyValues(["done"]);

// Contain Key匹配器
const object = new Map([
  ["bun", "1.2.0"],
  ["node", "22.13.0"],
  ["npm", "9.1.2"],
]);

expect(object).toContainKey("bun");
expect(object).toContainKeys(["bun", "node"]);
expect(object).toContainAllKeys(["bun", "node", "npm"]);
expect(object).not.toContainAnyKeys(["done"]);

// mock返回值
import { jest, test, expect } from "bun:test";

test("toHaveReturned()", () => {
  const mock = jest.fn(() => "foo");
  mock();
  expect(mock).toHaveReturned();
  mock();
  expect(mock).toHaveReturnedTimes(2);
});

-- Custom error message 自定义错误信息

现在可以通过expect的第二个参数,自定义断言失败时的错误信息。

import { test, expect } from 'bun:test';
// 自定义信息参数
test("custom error message", () => {
--  expect(0.1 + 0.2).toBe(0.3);
++  expect(0.1 + 0.2, "Floating point has precision error").toBe(0.3);
});

// 执行测试
1 | import { test, expect } from 'bun:test';
2 |
3 | test("custom error message", () => {
4 |   expect(0.1 + 0.2, "Floating point has precision error").toBe(0.3);

// 原信息
error: expect(received).toBe(expected)

// 新信息
error: Floating point has precision error

Expected: 0.3
Received: 0.30000000000000004


  • Jest's setTimeout() 超时设置

测试用例中,可以使用jest.setTimeout或者bun的setDefaultTimeout设置全局超时参数。

// 使用 jest方法
jest.setTimeout(60 * 1000); // 1 minute

test("do something that takes a long time", async () => {
  await Bun.sleep(Infinity);
});


// 也可以使用bun方法
import { setDefaultTimeout } from "bun:test";

setDefaultTimeout(60 * 1000); // 1 minute

小结

本文讨论的内容基于Bun发布的一篇关于1.2版本的博客文章,文章讨论了新版本中重要的新功能和特性。本片作为作者对于这些内容的解读和分析的上篇,探讨了相关的内容包括版本概况,Nodejs兼容性,S3支持,Postgres支持,Test测试等相关的内容。其他内容在系列文章中的下篇进行探讨。

文章的下篇在: 《Bun技术评估 - 18 Bun 1.2(下)》