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

一、简介

由于 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);
  },
});

说明:目标是获取 urlpathname。在 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 感兴趣,这可能对你有很大的帮助。希望这篇文章能够帮助读者朋友,更多内容可关注 公棕号 进二开物,每天不定时更新更多内容...

相关推荐
微信-since811924 分钟前
[ruby on rails] 安装docker
后端·docker·ruby on rails
Myli_ing6 分钟前
考研倒计时-配色+1
前端·javascript·考研
余道各努力,千里自同风9 分钟前
前端 vue 如何区分开发环境
前端·javascript·vue.js
软件小伟18 分钟前
Vue3+element-plus 实现中英文切换(Vue-i18n组件的使用)
前端·javascript·vue.js
醉の虾39 分钟前
Vue3 使用v-for 渲染列表数据后更新
前端·javascript·vue.js
张小小大智慧1 小时前
TypeScript 的发展与基本语法
前端·javascript·typescript
hummhumm1 小时前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j
asleep7011 小时前
第8章利用CSS制作导航菜单
前端·css
hummhumm1 小时前
第 28 章 - Go语言 Web 开发入门
java·开发语言·前端·python·sql·golang·前端框架
幼儿园的小霸王2 小时前
通过socket设置版本更新提示
前端·vue.js·webpack·typescript·前端框架·anti-design-vue