概述
本文是笔者的系列博文 《Bun技术评估》 中的第四篇。
在上一章节中,我们探讨了作为bun作为一个Web应用开发体系,最重要的对于HTTP协议和服务器操作的能力。在本章节中,我们将探讨其作为HTTP客户端的实现和操作。这个功能特性,在现代Web应用系统越来越复杂,和外部应用的集成越来越频繁的情况下,也是非常重要的。
本章节讨论的主要内容来自bun官方技术文档的相关章节:
基础形式
在nodejs中,http的客户端,也是http模块,通过执行request方法,来实现客户端的功能。但在bun中,http的客户端,几乎就是使用和浏览器中相同的fetch方法。 下面就是其标准应用方式:
js
// http get
const response = await fetch("http://example.com");
console.log(response.status); // => 200
const text = await response.text(); /
// http post with headers
const response = await fetch("http://example.com", {
method: "POST",
body: "Hello, world!",
headers: {
"X-Custom-Header": "value",
},
});
可以看到,其实现了一般性的HTTP请求,需要控制的项目,包括:
- 提供请求地址
- 支持标准HTTP和HTTPS协议(在URL中标识)
- 可以设置请求方法,如GET(默认)、POST或者任何文本化的方法声明
- 可以提供请求体,和默认的转换形式
- 可以设置请求标头,包括标准标头和自定义标头
- 响应时,检查响应状态
- 随后,使用转换方法,获取响应内容
- Cookie,在HTTP协议规范中,本质上就是一种标准标头的应用
笔者没有深入研究现在浏览器中新引入的fetch方法,但感觉上,对于HTTP请求,bun基本上就是复刻了这个fetch方法。
错误处理
根据bun的文档,fetch方法在工作的时候,出现的错误将会使用标准方式抛出,所以,如果需要对错误进行处理,可能应该使用标准的try...catch方式。
响应体转换
前面的代码中,我们可以看到,fetch方法,返回的结果,是一个response对象,基于这个对象,bun内置了很多内置的处理方法,来获取或者转换响应的实际内容,例如:
- response.text(): 转换响应体为简单文本
- response.json(): 转换为JSON对象
- response.formData(): 转换为表单对象
- response.bytes(): 返回Uint8Array数组
- response.arrayBuffer(): 返回ArrayBuffer
- response.blob(): 返回Blob.
这基本上已经可以涵盖所有应用需求的形式。需要注意的是,在实际应用的代码中,这些方法返回的都是promise对象,需要进行await处理。
响应流
除了常见的响应内容之外,fetch其实还支持流化的响应内容。这种方式一般用于更高效的处理比较大的响应结果,如下载文件等场景。使用也非常简单,就是使用chunk来遍历响应的结果体,参考代码如下:
js
// support stream
const response = await fetch("http://example.com");
for await (const chunk of response.body) {
console.log(chunk);
}
// 连接读取流
const response = await fetch("http://example.com");
const stream = response.body;
const reader = stream.getReader();
const { value, done } = await reader.read();
超时和退出
结合AbortSignal和AbortController对象,在fetch可以方便的对HTTP请求的生命周期进行控制:
js
// 自动超时
const response = await fetch("http://example.com", {
signal: AbortSignal.timeout(1000),
});
// 可控撤销
const controller = new AbortController();
const response = await fetch("http://example.com", {
signal: controller.signal,
});
// in some case
controller.abort();
这里面展示了两个场景,就是设置超时时间,和主动中断请求。其实使用后者,结合一个定时器,也应该可以实现超时中断,或者条件中断的效果。
客户端认证
在HTTP请求响应需要双向认证的场景中,比如两个应用系统之间进行点对点的集成,为了保证更高的安全性,这一特性是非常必要的。使用也非常简单:
js
// 客户端配置
await fetch("https://example.com", {
tls: {
key: Bun.file("/path/to/key.pem"),
cert: Bun.file("/path/to/cert.pem"),
// ca: [Bun.file("/path/to/ca.pem")],
checkServerIdentity: (hostname, peerCertificate) => {
// Return an Error if the certificate is invalid
},
},
});
// 省略SSL验证,常用于开发和测试环境
await fetch("https://example.com", {
tls: {
rejectUnauthorized: false,
},
});
扩展协议
这个特性是比较令人满意的,除了标准的HTTP和HTTPS之外,bun内置的fetch还支持多种扩展协议。
- Unix Socket
我们都知道,在类Unix系统中,HTTP网络端口和Unix Sock端口基本是等效的。所以,理论如果有端口输出文件,可以使用类似的方式来使用(比如PHP-FPM就可以以Sock文件方式工作)。官方的示例是这样的:
js
const response = await fetch("https://hostname/a/path", {
unix: "/var/run/path/to/unix.sock",
method: "POST",
body: JSON.stringify({ message: "Hello from Bun!" }),
headers: {
"Content-Type": "application/json",
},
});
但实际上,笔者对此是有点疑问的。这种情况下,它使用了一个unix属性来指定端口文件,这时其实不应该设置默认的url地址;还有就是其实可以不用unix属性,而使用这种模式的url: "unix://path/sockfile" ,可能更加优雅简洁。
- S3
fetch支持标准S3对象存储协议和其方法。应该也支持S3兼容的系统,如minio。
js
// Using environment variables for credentials
const response = await fetch("s3://my-bucket/path/to/object");
// Or passing credentials explicitly
const response = await fetch("s3://my-bucket/path/to/object", {
s3: {
accessKeyId: "YOUR_ACCESS_KEY",
secretAccessKey: "YOUR_SECRET_KEY",
region: "us-east-1",
},
});
// put 对象
import { createReadStream } from "fs";
import { stat } from "fs/promises";
const filePath = "./test.jpg";
const fileStream = createReadStream(filePath);
const fileStat = await stat(filePath);
// 预签名 URL 或直连 S3 端点(带权限)
const uploadUrl = "https://your-s3-endpoint.com/bucket-name/test.jpg";
const res = await fetch(uploadUrl, {
method: "PUT",
body: fileStream, // 数据流
headers: {
"Content-Type": "image/jpeg",
"Content-Length": fileStat.size.toString(), // 非必须,但建议带上
// "Authorization": "..." // 如果没有预签名 URL,需要签名
},
});
if (res.ok) {
console.log("✅ 上传成功");
} else {
console.error("❌ 上传失败", res.status, await res.text());
}
需要说明,这个特性,笔者现在尚没有机会实践,但从代码不难理解其操作逻辑。对于大量使用文件进行集成的应用系统,这是一个非常重要的特性。
- File
可以使用一种通用的方式,处理来自网络和本地的文件的内容:
js
const response = await fetch("file:///path/to/file.txt");
const text = await response.text();
- Data URL
js
const response = await fetch("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==");
const text = await response.text(); // "Hello, Worl
- Blog URL
js
const blob = new Blob(["Hello, World!"], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const response = await fetch(url);
性能和选项
bun的fetch使用了很多方式来提升请求的性能,并增加了一些应用方面的选项,其中笔者觉得值得一提的如下:
- DNS prefetching, DNS预取
下面的代码,用于实现DNS的预取,可以在实际请求前操作,并缓存一段时间,节省真实请求时DNS查找的时间。
js
import { dns } from "bun";
dns.prefetch("bun.sh");
- Preconnect to a host
主机预连接,可以在实际连接前准备一下,这些准备工作包括DNS查询,TCP Socket连接和早期的TLS握手,用于简化随后请求的过程。
js
import { fetch } from "bun";
fetch.preconnect("https://bun.sh");
- Preconnect at startup
启动预连接,可以在启动时设计,简化在程序中请求连接的过程。
- Simultaneous connection 并发
在一个应用中,默认支持256个并发的fetch请求操作。
- 调试
请求时,设置debug: true 调试选项,可以在控制台展示整个请求过程的详细信息。
小结
本文总结和讨论了在bun中,操作和实现HTTP应用的另一个重要的部分:Client,其核心是fetch方法。文章列举了其一般方法和标准形式,请求选项。响应内容的处理,TLS应用,错误和超时,扩展协议,性能优化等方面的内容。覆盖了比较完整的请求响应生命周期的处理和管理。