Bun 1.2 发布
Bun 是一个用于构建和测试全栈 JavaScript 和 TypeScript 应用的完整工具包。如果你还不熟悉 Bun,可以通过 Bun 1.0 的博客文章了解更多。
Bun 1.2 更新亮点
Bun 1.2 是一个重大更新,我们非常兴奋地与大家分享。以下是 Bun 1.2 的主要变化:
- Node.js 兼容性:Bun 在 Node.js 兼容性方面取得了重大进展。
- 内置 S3 对象存储 API:新增了
Bun.s3
,支持与 S3 存储的交互。 - 内置 Postgres 客户端:新增了
Bun.sql
,支持 Postgres 数据库操作(MySQL 支持即将推出)。 bun install
使用基于文本的锁文件:bun.lock
取代了之前的二进制锁文件。
此外,Bun 1.2 还让 Express 框架的性能提升了 3 倍。
Node.js 兼容性
Bun 旨在成为 Node.js 的直接替代品。在 Bun 1.2 中,我们开始为每次 Bun 的更改运行 Node.js 测试套件。自那时起,我们已经修复了数千个错误,以下 Node.js 模块在 Bun 中的测试通过率超过了 90%。
如何衡量兼容性?
在 Bun 1.2 中,我们改变了测试和改进 Bun 与 Node.js 兼容性的方式。以前,我们根据用户报告的 GitHub 问题来优先修复 Node.js 的 bug。虽然这解决了用户遇到的实际问题,但这种方式过于被动,难以实现 100% 的 Node.js 兼容性。
于是我们想到:为什么不直接运行 Node.js 的测试套件呢?
在 Bun 中运行 Node.js 测试
Node.js 的测试套件包含数千个测试文件,大部分位于 test/parallel
目录中。虽然“直接运行”这些测试听起来很简单,但实际上比想象中复杂得多。
内部 API
许多测试依赖于 Node.js 的内部实现细节。例如,以下测试中,getnameinfo
被模拟为总是出错,以测试 dns.lookupService()
的错误处理。
const { internalBinding } = require("internal/test/binding");
const cares = internalBinding("cares_wrap");
const { UV_ENOENT } = internalBinding("uv");
cares.getnameinfo = () => UV_ENOENT;
为了在 Bun 中运行这个测试,我们不得不用自己的模拟替换内部绑定。
Bun.dns.lookupService = (addr, port) => {
const error = new Error(`getnameinfo ENOENT ${addr}`);
error.code = "ENOENT";
error.syscall = "getnameinfo";
throw error;
};
错误信息
还有一些 Node.js 测试会检查错误信息的精确字符串。虽然 Node.js 通常不会更改错误信息,但它们并不保证在版本之间不会发生变化。
const common = require("../common");
const assert = require("assert");
const cp = require("child_process");
assert.throws(
() => {
cp.spawnSync(process.execPath, [__filename, "child"], { argv0: [] });
},
{
code: "ERR_INVALID_ARG_TYPE",
name: "TypeError",
message: 'The "options.argv0" property must be of type string.' + common.invalidArgTypeHelper([]),
}
);
为了解决这个问题,我们不得不修改一些测试的断言逻辑,改为检查 name
和 code
,而不是 message
。
{
code: "ERR_INVALID_ARG_TYPE",
name: "TypeError",
message: 'The "options.argv0" property must be of type string.' + common.invalidArgTypeHelper([]),
}
虽然我们尽量匹配 Node.js 的错误信息,但有时我们也会提供更有帮助的错误信息,只要 name
和 code
相同即可。
目前的进展
我们已经将数千个 Node.js 测试文件移植到 Bun 中。这意味着每次我们对 Bun 进行更改时,都会运行 Node.js 测试套件以确保兼容性。
每天,我们都在为 Bun 添加越来越多的 Node.js 测试通过率,我们很快会分享更多关于 Node.js 兼容性的进展。
除了修复现有的 Node.js API,我们还添加了对以下 Node.js 模块的支持。
node:http2
服务器
你现在可以使用 node:http2
来创建 HTTP/2 服务器。HTTP/2 也是 gRPC 服务器的必要条件,Bun 现在也支持 gRPC 服务器。之前,Bun 只支持 HTTP/2 客户端。
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 1.2 中,HTTP/2 服务器的性能比 Node.js 快 2 倍。当我们为 Bun 添加新 API 时,我们会花很多时间优化性能,确保它不仅能用,而且更快。
node:dgram
你现在可以使用 node: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();
这使得像 DataDog 的 dd-trace
和 @clickhouse/client
这样的包可以在 Bun 1.2 中正常工作。
node:cluster
你可以使用 node:cluster
来生成多个 Bun 实例。这通常用于通过跨多个 CPU 核心运行任务来提高吞吐量。
以下是一个使用 cluster
创建多线程 HTTP 服务器的示例:
- 主工作进程生成
n
个子工作进程(通常等于 CPU 核心数)。 - 每个子工作进程监听相同的端口(使用
reusePort
)。 - 传入的 HTTP 请求在子工作进程之间进行负载均衡。
import cluster from "node:cluster";
import { createServer } from "node:http";
import { cpus } from "node:os";
if (cluster.isPrimary) {
console.log(`Primary ${process.pid} is running`);
// 启动与 CPU 数量相等的工作进程
for (let i = 0; i < cpus().length; i++) {
cluster.fork();
}
cluster.on("exit", (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} exited`);
});
} else {
// 传入的请求由工作进程池处理,而不是主工作进程
createServer((req, res) => {
res.writeHead(200);
res.end(`Hello from worker ${process.pid}`);
}).listen(3000);
console.log(`Worker ${process.pid} started`);
}
注意,reusePort
仅在 Linux 上有效。在 Windows 和 macOS 上,操作系统不会像预期的那样对 HTTP 连接进行负载均衡。
node:zlib
在 Bun 1.2 中,我们重写了整个 node:zlib
模块,从 JavaScript 改为原生代码。这不仅修复了许多 bug,还使其性能比 Bun 1.1 快 2 倍。
我们还为 node:zlib
添加了对 Brotli 的支持,这在 Bun 1.1 中是缺失的。
import { brotliCompressSync, brotliDecompressSync } from "node:zlib";
const compressed = brotliCompressSync("Hello, world!");
compressed.toString("hex"); // "0b068048656c6c6f2c20776f726c642103"
const decompressed = brotliDecompressSync(compressed);
decompressed.toString("utf8"); // "Hello, world!"
使用 V8 API 的 C++ 插件
如果你想在 JavaScript 代码中使用 C++ 插件,最简单的方法是使用 N-API。
然而,在 N-API 出现之前,一些包使用了 Node.js 内部的 V8 C++ API。这使得事情变得复杂,因为 Node.js 和 Bun 使用不同的 JavaScript 引擎:Node.js 使用 V8(Chrome 使用的引擎),而 Bun 使用 JavaScriptCore(Safari 使用的引擎)。
以前,像 cpu-features
这样依赖这些 V8 API 的 npm 包在 Bun 中无法工作。
require("cpu-features")();
dyld[94465]: missing symbol called
fish: Job 1, 'bun index.ts' terminated by signal SIGABRT (Abort)
为了解决这个问题,我们进行了前所未有的工程努力,在 JavaScriptCore 中实现了 V8 的公共 C++ API,使得这些包可以在 Bun 中“直接运行”。这个实现非常复杂,我们甚至写了一篇 三部分的博客 来解释我们是如何在不使用 V8 的情况下支持 V8 API 的。
在 Bun 1.2 中,像 cpu-features
这样的包可以直接导入并运行。
$ bun index.ts
{
arch: "aarch64",
flags: {
fp: true,
asimd: true,
// ...
},
}
V8 C++ API 的支持非常复杂,因此大多数包仍然会有一些缺失的功能。我们将继续改进支持,以便像 node-canvas@v2
和 node-sqlite3
这样的包在未来能够正常工作。
node:v8
除了 V8 C++ API,我们还添加了对使用 node:v8
进行堆快照的支持。
import { writeHeapSnapshot } from "node:v8";
// 将堆快照写入当前工作目录,文件名为 `Heap-{date}-{pid}.heapsnapshot`
writeHeapSnapshot();
在 Bun 1.2 中,你可以使用 getHeapSnapshot
和 writeHeapSnapshot
来读取和写入 V8 堆快照。这使得你可以使用 Chrome DevTools 来检查 Bun 的堆。
Express 性能提升 3 倍
虽然兼容性对于修复 bug 很重要,但它也帮助我们修复了 Bun 中的性能问题。
在 Bun 1.2 中,流行的 express
框架处理 HTTP 请求的速度比 Node.js 快 3 倍。这是通过改进与 node:http
的兼容性以及优化 Bun 的 HTTP 服务器实现的。
S3 支持与 Bun.s3
Bun 旨在成为一个面向云的 JavaScript 运行时。这意味着它支持你在云中运行生产应用所需的所有工具和服务。
现代应用通常将文件存储在对象存储中,而不是本地的 POSIX 文件系统。当终端用户将文件附件上传到网站时,文件不会存储在服务器的本地磁盘上,而是存储在 S3 存储桶中。将存储与计算解耦可以避免一系列可靠性问题:磁盘空间不足、繁忙的 I/O 导致的高 p95 响应时间,以及共享文件存储的安全问题。
S3 是云中对象存储的事实标准。S3 API 由多种云服务实现,包括 Amazon S3、Google Cloud Storage、Cloudflare R2 等。
这就是为什么 Bun 1.2 添加了对 S3 的内置支持。你可以使用与 Web 标准兼容的 API(如 Blob
)从 S3 存储桶中读取、写入和删除文件。
从 S3 读取文件
你可以使用新的 Bun.s3
API 来访问默认的 S3Client
。客户端提供了一个 file()
方法,返回对 S3 文件的懒引用,这与 Bun 的 File
API 相同。
import { s3 } from "bun";
const file = s3.file("folder/my-file.txt");
// file instanceof Blob
const content = await file.text();
// 或者:
// file.json()
// file.arrayBuffer()
// file.stream()
比 Node.js 快 5 倍
Bun 的 S3 客户端是用原生代码编写的,而不是 JavaScript。与使用 @aws-sdk/client-s3
的 Node.js 相比,Bun 从 S3 存储桶下载文件的速度快 5 倍。
将文件写入 S3
你可以使用 write()
方法将文件上传到 S3。非常简单:
import { s3 } from "bun";
const file = s3.file("folder/my-file.txt");
await file.write("hello s3!");
// 或者:
// file.write(new Uint8Array([1, 2, 3]));
// file.write(new Blob(["hello s3!"]));
// file.write(new Response("hello s3!"));
对于较大的文件,你可以使用 writer()
方法获取一个文件写入器,进行多部分上传,这样你就不必担心细节。
import { s3 } from "bun";
const file = s3.file("folder/my-file.txt");
const writer = file.writer();
for (let i = 0; i < 1000; i++) {
writer.write(String(i).repeat(1024));
}
await writer.end();
预签名 URL
当你的生产服务需要让用户上传文件到服务器时,通常更可靠的做法是让用户直接上传到 S3,而不是通过服务器作为中介。
为了实现这一点,你可以使用 presign()
方法生成文件的预签名 URL。这会生成一个带有签名的 URL,允许用户安全地上传特定文件到 S3,而不会暴露你的凭证或授予他们不必要的访问权限。
import { s3 } from "bun";
const url = s3.presign("folder/my-file.txt", {
expiresIn: 3600, // 1 小时
acl: "public-read",
});
使用 Bun.serve()
由于 Bun 的 S3 API 扩展了 File
API,你可以使用 Bun.serve()
通过 HTTP 提供 S3 文件。
import { serve, s3 } from "bun";
serve({
port: 3000,
async fetch(request) {
const { url } = request;
const { pathname } = new URL(url);
// ...
if (pathname === "/favicon.ico") {
const file = s3.file("assets/favicon.ico");
return new Response(file);
}
// ...
},
});
当你使用 new Response(s3.file(...))
时,Bun 不会将 S3 文件下载到服务器并发送给用户,而是将用户重定向到 S3 文件的预签名 URL。
Response (0 KB) {
status: 302,
headers: Headers {
"location": "https://s3.amazonaws.com/my-bucket/assets/favicon.ico?...",
},
redirected: true,
}
这节省了内存、时间和下载文件到服务器的带宽成本。
使用 Bun.file()
如果你想使用与本地文件系统相同的代码访问 S3 文件,可以使用 s3://
URL 协议引用它们。这与使用 file://
引用本地文件的概念相同。
import { file } from "bun";
async function createFile(url, content) {
const fileObject = file(url);
if (await fileObject.exists()) {
return;
}
await fileObject.write(content);
}
await createFile("s3://folder/my-file.txt", "hello s3!");
await createFile("file://folder/my-file.txt", "hello posix!");
使用 fetch()
你甚至可以使用 fetch()
从 S3 读取、写入和删除文件。
// 上传到 S3
await fetch("s3://folder/my-file.txt", {
method: "PUT",
body: "hello s3!",
});
// 从 S3 下载
const response = await fetch("s3://folder/my-file.txt");
const content = await response.text(); // "hello s3!"
// 从 S3 删除
await fetch("s3://folder/my-file.txt", {
method: "DELETE",
});
使用 S3Client
当你导入 Bun.s3
时,它会返回一个默认客户端,该客户端使用已知的环境变量(如 AWS_ACCESS_KEY_ID
和 AWS_SECRET_ACCESS_KEY
)进行配置。
import { s3, S3Client } from "bun";
// s3 instanceof S3Client
你也可以创建自己的 S3Client
,然后将其设置为默认客户端。
import { S3Client } from "bun";
const client = new S3Client({
accessKeyId: "my-access-key-id",
secretAccessKey: "my-secret-access-key",
region: "auto",
endpoint: "https://<account-id>.r2.cloudflarestorage.com",
bucket: "my-bucket",
});
// 将默认客户端设置为你的自定义客户端
Bun.s3 = client;
Postgres 支持与 Bun.sql
与对象存储一样,生产应用通常需要的另一个数据存储是 SQL 数据库。
从一开始,Bun 就内置了 SQLite 客户端。SQLi