一、简介
由于 Bun 是服务端运行时,DOM 等相关的 Web API 与其无关,无需实现。以下是 Web API 在 Bun 运行时中的实现,如果你对这些 API 还不是很熟悉,那么这篇文章对你应该很有用!
本文更加适合:对 Bun 感兴趣和对 Web 标准干感兴趣的同学。
二、HTTP 相关
在 Bun 中使用 Fetch 标注实现 HTTP 服务,这点与 Node.js 服务器不同,靠近 web 标准是很好的选择,开发人员熟悉 Web 标准,就很快能上手 Bun 的服务器开发的内容。
2.1) 结构图
2.2) 最简单的示例
ts
Bun.serve({
port: 3000,
fetch(req, server) { // fetch 与 web fetch 的参数有所有不同
return new Response(req.url, server.port);
},
});
说明:在 Bun serve 的 fetch 函数中,返回一个简单的
Response 实例
即可。
方法 | 描述 |
---|---|
fetch(input, init?) |
用于发起网络请求并返回一个 Promise,以便处理响应数据。 |
input |
要请求的资源的 URL 或一个 Request 对象。 |
init |
一个可选的配置对象,用于指定请求的各种选项,例如请求方法、头部信息等。 |
Response |
从服务器返回的响应数据,包括响应的状态、头部信息和主体内容。 |
Request |
一个 HTTP 请求,可以自定义请求的方法、头部信息等。 |
2.3) 更换 Headers 请求头
ts
Bun.serve({
fetch(req) {
return new Response("<h1>This is html</h1>", {
headers: new Headers({
'Content-Type': 'text/html',
})
});
},
});
Headers 构造函数属性和方法
类型 | 名称 | 描述 |
---|---|---|
构造函数 | Headers() | 创建一个新的Headers对象。 |
实例方法 | append() | 向Headers对象中添加一个新的头部信息。 |
实例方法 | delete() | 删除Headers对象中指定名称的头部信息。 |
实例方法 | entries() | 返回一个包含所有头部信息的迭代器。 |
实例方法 | forEach() | 遍历Headers对象中的所有头部信息。 |
实例方法 | get() | 获取Headers对象中指定名称的头部信息的值。 |
实例方法 | getSetCookie() | 获取Headers对象中的"Set-Cookie"头部信息的值。 |
实例方法 | has() | 检查Headers对象中是否存在指定名称的头部信息。 |
实例方法 | keys() | 返回一个包含所有头部信息名称的迭代器。 |
实例方法 | set() | 设置Headers对象中指定名称的头部信息的值。 |
实例方法 | values() | 返回一个包含所有头部信息值的迭代器。 |
2.4) 控制器/信号:请求与取消
- AbortController
属性/方法 | 描述 |
---|---|
signal |
返回一个与控制器相关联的 AbortSignal 对象,用于监听和中止异步操作。 |
abort() |
用于中止与控制器相关的所有异步操作,并触发与 AbortSignal 关联的 abort 事件。 |
addEventListener() |
用于注册事件监听器以监听与控制器关联的 abort 事件。 |
removeEventListener() |
用于删除已注册的事件监听器。 |
- 使用 AbortController 实例化一个控制器,控制器的
Controller
的 signal 属性与 fetch 的 signal 属性链接,控制器的abort
方法可以与事件函数进行绑定。
以下是用 fetch + AbortController 封装的一个简单的 API, 用于请求与取消:
ts
class Api {
controller = new AbortController();
signal = this.controller.signal;
fetchData(url: string) {
fetch(url, { signal: this.signal })
.then((response) => {
console.log("Download complete", response);
})
.catch((err) => {
console.error(`Download error: ${err.message}`);
});
}
}
const apis = new Api()
apis.fetchData('you_url')
setTimeout(() => {
apis.controller?.abort(); // 30 ms 之后发生取消
}, 30)
- 直接使用
AbortSignal
进行超时取消
属性/方法 | 描述 |
---|---|
aborted |
表示信号是否已中止的布尔值。 |
addEventListener() |
用于注册事件监听器以监听信号的abort 事件。 |
removeEventListener() |
用于删除已注册的事件监听器。 |
abort() |
用于中止与信号相关的异步操作,触发abort 事件。 |
- 使用超时函数
ts
await fetch("your_url", { signal: AbortSignal.timeout(100) })
三、URLs 相关
3.1) 结构图
3.1) 最简单的示例
ts
Bun.serve({
fetch(req) {
const url = new URL(req.url); // 得到一个标准的 URL 对象
return new Response(url);
},
});
说明:目标是获取 url
中 pathname
。在 req
中,获取 req.url
, 借此实例化 URL
后可以访问 pathname
。
3.2) 获取查询参数
ts
Bun.serve({
fetch(req) {
const params = new URL(req.url).searchParams; // 从 url 中获取 searchParams, 得到一个标准的 URL 对象
const name = params.get("name"); // 从 params 中获取 name 属性
return new Response(name); // 在 bun 中返回 Response
},
});
Bun.serve({
fetch(req) {
// 新构建一个 paramsString
const paramsString = "name=xiao&age=10";
const searchParams = new URLSearchParams(paramsString);
searchParams.append('sex', 'male')
return new Response(searchParams.toString()); // 返回新的 name=xiao&age=10&sex=male 字符串
},
});
四、Web Workers
4.1) 结构图
4.2) Bun 中 worker使用示例:模拟一个耗时的任务
- 定义 s1.ts 没有错误 Bun 中可以指定使用 ts 文件定义 worker:
ts
// 监听主线程发送的消息
global.addEventListener('message', function(event) {
if (event.data === 'start') {
const result = cal(); // 模拟一个耗时的计算任务
global.postMessage(result); // 向主线程发送计算结果
}
});
function cal() {
let result = 0;
for (let i = 0; i < 10000000; i++) { // 模拟一个大量计算时间
result += i;
}
return result;
}
- 定义主进程
ts
const worker = new Worker('./s1.ts')
// 监听 s1 发来的信息
worker.addEventListener('message', (message) => {
console.log("message", message)
})
// 2s 后向 s1 发送信息
setTimeout(() => {
worker.postMessage('start');
}, 2000)
- 使用 bun 运行:
ts
bun run index.tx
bun run s1.ts
- 输出结果:
ts
message MessageEvent {
type: "message",
data: 49999995000000
}
说明:本示例主要使用了 Worker 和 global.postMessage, 此处是 Bun 运行时 self 改成 global 即可。同时在 worker 传递数据的时候自动进行 structuredClone 操作,数据不会共享引用。
4.3) MessagePort
- portMessage 的两种用法:
ts
postMessage(message)
postMessage(message, transfer) // transfer 转移所有权 (被转移所有权之后,发送上下文不可用,只有接收的才可用)
- 定义主进程
ts
const worker = new Worker('./s1.ts');
const messageChannel = new MessageChannel();
// 将 MessagePort 与 Web Worker 关联
worker.postMessage({ port: messageChannel.port1 }, [messageChannel.port1]);
console.log(messageChannel)
// 监听从 Web Worker 返回的消息
messageChannel.port2.onmessage = function(event) {
const receivedDataFromWorker = event.data;
console.log('receivedDataFromWorker', receivedDataFromWorker)
};
// 向 Web Worker 发送消息
setTimeout(() => {
messageChannel.port1.postMessage('messageChannel port 1 发出'); // 被转移了所有权,不可用
messageChannel.port2.postMessage('messageChannel port 2 发出');
}, 2000)
- 定义 worker
ts
global.addEventListener('message', function(event) {
// event.data.port 包含了从主线程传递过来的 MessagePort
const messagePort = event.data.port;
// 监听来自主线程的消息
messagePort.onmessage = function(event) {
const receivedDataFromMain = event.data;
// 在这里处理来自主线程的数据
console.log("receivedDataFromMain", receivedDataFromMain)
};
// 向主线程发送消息
setTimeout(() => {
messagePort.postMessage('Web Worker 发出!');
}, 3000)
});
- 运行并得到结果
sh
receivedDataFromMain messageChannel port 2 发出
receivedDataFromWorker Web Worker 发出!
五、 Stream 流
5.1) 结构图
5.2) 可读流:从 fetch 开始并模拟 ChatGPT 的行为
- 一个简单的示例: 模拟 ChatGPT 输出流:
ts
// 创建配置对象
const streamConfig = {
start(controller) {
let index = 0
const contents = ['Hello', '', 'world']
const id = setInterval(() => {
if(index < 3) {
controller.enqueue(contents[index])
index += 1;
} else {
controller.close()
clearInterval(id)
}
}, 1000)
},
cancel(reason) {
console.log(`Stream canceled with reason: ${reason}`);
},
};
// 使用构造函数创建可读流
const readableStream = new ReadableStream(streamConfig);
// 使用读取器从流中读取数据
const reader = readableStream.getReader();
async function readStream(reader) {
const { done, value } = await reader.read();
if (!done) {
console.log(value);
// 递归调用 readStream 函数以继续读取下一个数据块
await readStream(reader);
} else {
console.log('Reached the end of the stream.');
}
}
// 递归调用 readStream 函数来读取整个流
readStream(reader);
5.3) 可写流:一个简单的示例
- 一个简单的示例:
ts
const config = {
write(chunk, controller) {
// 写入数据
console.log(`Writing chunk: ${chunk}`);
},
close() {
console.log('Stream has been closed.');
},
abort(reason) {
console.error(`Stream error: ${reason}`);
},
}
// 创建一个可写流
const writableStream = new WritableStream(config);
// 获取流的写入控制器
const writer = writableStream.getWriter();
// 向流中写入数据块
writer.write('Hello, ');
writer.write('world!');
// 关闭流
writer.close();
使用构造函数创建一个写入流 writableStream, 其中接收三个配置文件 write/close/abort
三个对象,getWriter 函数 writer。writer 具有 write 方法,此方法能够传递字符输入写入数据到写入流。
5.4) 转换流:一个简单的给一个流上字符串添加 js 类型的注释
ts
const tconfig = {
transform: (chunk, controller) => {
const upperCaseChunk = chunk.toString() + '/* this is */';
controller.enqueue(upperCaseChunk);
};
}
const transformStream = new TransformStream(tconfig);
// 创建一个可写入的流
const writableStream = new WritableStream({
write(chunk) {
console.log(`Received: ${chunk}`);
}
});
// 将转换流和可写流连接起来
transformStream.readable.pipeTo(writableStream);
// 向转换流中写入数据
const writer = transformStream.writable.getWriter();
writer.write("Hello, ");
writer.write("world!");
writer.close();
5.5) 字节长度策略:ByteLengthQueuingStrategy
ts
// 创建一个队列策略,每个数据块的字节长度为其 UTF-8 编码长度
const queuingStrategy = new ByteLengthQueuingStrategy({
highWaterMark: 1024 // 设置记为 1 KB
});
// 创建可写入流,并传入队列策略
const writableStream = new WritableStream({
async write(chunk) {
console.log(`Received: ${chunk}`);
}
}, queuingStrategy);
5.6) 内容长度策略:CountQueuingStrategy
ts
const queueingStrategy = new CountQueuingStrategy({ highWaterMark: 1 });
const writableStream = new WritableStream(
{},
queueingStrategy,
);
const size = queueingStrategy.size();
六、 Blob 二进制
6.1) 结构图
6.2) Blob 构造函数
ts
new Blob(array)
new Blob(array, options)
//
const text = "从 字符串 到 Blob";
const blob = new Blob([text], { type: 'text/plain' });
// 从 Uint8Array 到 Blob
const imageData = new Uint8Array([255, 0, 0, 255]); // 红色像素
const imageBlob = new Blob([imageData], { type: 'image/png' });
// 还可以是:TypedArray、DataView、DataView 以及它们的混合类型等等...
6.3) Blog 示例: 在浏览中下载一个使用保存 blog 数据文件
ts
const text = "这是一个保存 Blob 数据的示例文本。";
const blob = new Blob([text], { type: "text/plain" });
// 创建一个链接以下载 Blob 对象
const downloadLink = document.createElement("a");
downloadLink.href = window.URL.createObjectURL(blob);
downloadLink.download = "example.txt";
// 模拟点击链接以下载文件
downloadLink.click();
// 释放 Blob 对象的 URL
window.URL.revokeObjectURL(downloadLink.href);
七、WebSocket
7.1) 结构图
7.2) 协议
- 非加密:
ws://
- 加密:
wss://
7.3) 示例和基本 API 使用
ts
const socket = new WebSocket('your_socket_addr');
// open-message-close-error 四种不同的事件
socket.addEventListener('open', (event) => {
console.log('WebSocket连接已打开');
});
socket.addEventListener('message', (event) => {
console.log('接收到消息:', event.data);
});
socket.addEventListener('close', (event) => {
if (event.wasClean) {
console.log('连接已正常关闭,状态码:', event.code);
} else {
console.error('连接异常关闭');
}
});
socket.addEventListener('error', (error) => {
console.error('WebSocket错误:', error);
});
// 发送数据
socket.send('这是一条消息');
// 关闭 socket
socket.close();
八、解码和编码
8.1) 结构图
8.2) atob 和 btoa: 处理 base64
ts
const encodedData = btoa("handle base64"); // encode a string
const decodedData = atob(encodedData); // decode the string
const encodedData = btoa("handle base64"); // encode a string
const decodedData = atob(encodedData); // decode the string
atob("this") // '¶\x18¬'
btoa('¶\x18¬') // 'this'
8.3) TextDecoder 和 TextEncoder
可在不同的编码之间进行转换:
ts
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const text = "在不同的编码直接进行转换";
const encodedData = encoder.encode(text);
const decodedText = decoder.decode(encodedData);
console.log(decodedText)
// 在不同的编码直接进行转换
🦀默认支持 utf-8 字符编码。
九、JSON
9.1) 结构图
9.2) 序列化与反序列化的数据类型
对象、数组、字符串、数字、布尔值和 null
。其中不包含 函数
,这也是 json 不能完全深拷贝的特点。
9.3) 最简单的示例
ts
// 序列化对象为 JSON 字符串
const data = { name: "小明", age: 3 };
const jsonString = JSON.stringify(data);
// 反序列化 JSON 字符串为对象
const parsedData = JSON.parse(jsonString);
9.3) 第二个和第三个参数
- 第二个参数
ts
const data = {
name: "小明",
age: 3,
sex: "male"
};
const filteredData = JSON.stringify(data, (key, value) => {
if (key === "age") {
return undefined; // 过滤掉 age 属性
}
return value;
});
console.log(filteredData);
// 输出:{"name":"小明","sex": "male"}
- 第三个参数
ts
const data = { name: "JSON", age: 3 };
const prettyJSON = JSON.stringify(data, null, 2);
console.log(prettyJSON);
/*
{
"name": "JSON",
"age": 3
}
*/
小结
本小结主要讲解 fetch 为代表的新一代 Web API(当然也有一些老的 API) 以及在 Bun 运行时中的使用,前端学习标准 Web API 方向一定是对的,并且目前 Node.js、Deno 和 Bun 新的 JS 运行时都在跟进新的 Web API,同时如果你对 Bun 感兴趣,这可能对你有很大的帮助。希望这篇文章能够帮助读者朋友,更多内容可关注 公棕号 进二开物,每天不定时更新更多内容...