概述
本文是笔者的系列博文 《Bun技术评估》 中的第十七篇。
在本系列成文过程中,笔者发现,bun仍然还只是一个在快速发展和演进的系统。其中,Bun的1.2版本,看起来是一个比较重要的,标志性的系统,从这个版本开始,bun可以被认为是一个相对比较成熟的体系了。所以其在首页上,特别标注了这个版本的发布。
同时,官方网站上还有一个配套的博客专门讨论了这个问题,笔者也觉得是非常有必要了解一下的,也是本文主要要探讨的内容。这个博客的链接如下:
看了这篇博客之后,笔者发现,很多内容我们前面其实已经讨论过了,考虑到这些讨论都是基于最新版本(当前是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的兼容性项目和分值如下:
看起来,现在的状态还不错,在大多数板块中,都实现了90%以上的兼容性。那么,bun是如何评估和Nodejs之间的兼容性的问题的呢?
兼容性评估和测试方法
其实,这个兼容性评估方式本身,就是1.2版本的重大改进。
在之前的版本中,bun通常根据用户使用报告对Node.js错误进行优先级排序和修复(这些报告通常来自GitHub问题,特别是其中有人试图使用在Bun中无法使用的npm包),这种方式有点像一个无休止的“打地鼠”游戏,虽然很多问题是确实存在重要的,但对于真正的代码和功能兼容性提升帮助有限。
而现在改进的兼容性评估和演进的主要策略是,迁移和使用Node.js测试套件。这样可以在逻辑上做到和Nodejs原生代码几乎相同的兼容性,因为确实,在不同的版本之间,nodejs本身其实也存在一些兼容的问题,这个策略可以保证和nodejs主线发展的动态兼容。
具体而言,nodejs的代码仓库中有上千个测试文件,大部分位于 test/parallel 文件夹中(图)。虽然看起来只需 "运行" 这些测试就可以得到测试结果,但实际上比你想的要复杂得多。很多代码需要为Bun test进行相关的调整,这些具体内容,不是本文的重点。我们只需要理解,这一个持续进行和改进的过程就可以了。
在具体实现的更新方面,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版本的性能也提升了一倍。
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中加载分析使用:
S3支持
在bun1.2中,原生提供了S3和其兼容系统的支持(如MINIO),这个主题我们已经在前面进行了探讨,详见:
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数据库支持的具体内容,笔者在前面也有专门的博文讨论:
在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(下)》