[Bun 系列]:了解这些 Web API,助力你开发 Bun 运行时应用(一)

1,723 阅读6分钟

一、简介

Bun 实现的 Web APIs.png

由于 Bun 是服务端运行时,DOM 等相关的 Web API 与其无关,无需实现。以下是 Web API 在 Bun 运行时中的实现,如果你对这些 API 还不是很熟悉,那么这篇文章对你应该很有用!

本文更加适合:对 Bun 感兴趣和对 Web 标准感兴趣的同学。

二、HTTP 相关

在 Bun 中使用 Fetch 标注实现 HTTP 服务,这点与 Node.js 服务器不同,靠近 web 标准是很好的选择,开发人员熟悉 Web 标准,就很快能上手 Bun 的服务器开发的内容。

2.1) 结构图

Bun HTTP.png

2.2) 最简单的示例

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 请求头

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, 用于请求与取消:

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事件。
  • 使用超时函数
await fetch("your_url", { signal: AbortSignal.timeout(100) })

三、URLs 相关

3.1) 结构图

路径相关 URLs.png

3.1) 最简单的示例

Bun.serve({
  fetch(req) {
    const url = new URL(req.url); // 得到一个标准的 URL 对象
    return new Response(url);
  },
});

说明:目标是获取 urlpathname。在 req 中,获取 req.url, 借此实例化 URL 后可以访问 pathname

3.2) 获取查询参数

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) 结构图

Web Workers.png

4.2) Bun 中 worker使用示例:模拟一个耗时的任务

  • 定义 s1.ts 没有错误 Bun 中可以指定使用 ts 文件定义 worker:
// 监听主线程发送的消息
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;
}
  • 定义主进程
const worker = new Worker('./s1.ts')

// 监听 s1 发来的信息
worker.addEventListener('message', (message) => {
  console.log("message", message)
})

// 2s 后向 s1 发送信息
setTimeout(() => {
  worker.postMessage('start');
}, 2000)
  • 使用 bun 运行:
bun run index.ts // 运行
  • 输出结果:
message MessageEvent {
  type: "message",
  data: 49999995000000
}

说明:本示例主要使用了 Worker 和 global.postMessage, 此处是 Bun 运行时 self 改成 global 即可。同时在 worker 传递数据的时候自动进行 structuredClone 操作,数据不会共享引用。

4.3) MessagePort

  • portMessage 的两种用法:
postMessage(message)
postMessage(message, transfer) // transfer 转移所有权 (被转移所有权之后,发送上下文不可用,只有接收的才可用)
  • 定义主进程
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

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)
});
  • 运行并得到结果
receivedDataFromMain messageChannel port 2 发出
receivedDataFromWorker Web Worker 发出!

五、 Stream 流

5.1) 结构图

Streams.png

5.2) 可读流:从 fetch 开始并模拟 ChatGPT 的行为

  • 一个简单的示例: 模拟 ChatGPT 输出流:
// 创建配置对象
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) 可写流:一个简单的示例

  • 一个简单的示例:
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 类型的注释

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

// 创建一个队列策略,每个数据块的字节长度为其 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

const queueingStrategy = new CountQueuingStrategy({ highWaterMark: 1 });

const writableStream = new WritableStream(
  {},
  queueingStrategy,
);

const size = queueingStrategy.size();

六、 Blob 二进制

6.1) 结构图

Blob.png

6.2) Blob 构造函数

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 数据文件

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) 结构图

WebSocket.png

7.2) 协议

  • 非加密:ws://
  • 加密:wss://

7.3) 示例和基本 API 使用

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) 结构图

Encoding and decoding.png

8.2) atob 和 btoa: 处理 base64

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

可在不同的编码之间进行转换:

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) 结构图

JSON.png

9.2) 序列化与反序列化的数据类型

对象、数组、字符串、数字、布尔值和 null。其中不包含 函数,这也是 json 不能完全深拷贝的特点。

9.3) 最简单的示例

// 序列化对象为 JSON 字符串
const data = { name: "小明", age: 3 };
const jsonString = JSON.stringify(data);

// 反序列化 JSON 字符串为对象
const parsedData = JSON.parse(jsonString);

9.3) 第二个和第三个参数

  • 第二个参数
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"}
  • 第三个参数
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 感兴趣,这可能对你有很大的帮助。希望这篇文章能够帮助读者朋友,更多内容可关注 公棕号 进二开物,每天不定时更新更多内容...