Bun技术评估 - 09 File

概述

本文是笔者的系列博文 《Bun技术评估》 中的第九篇。

本文主要探讨的内容,是在Bun中,如何处理文件。

文件操作,是任何一个后端应用开发系统,其实也包括前端系统的一个核心功能,是非常重要和实用的。当然nodejs也内置了fs模块,来处理方面的问题。Bun的策略,是将文件操作的功能,直接集成在全局对象Bun中,内置提供了文件API(以下简称为bun file),并且改进了一些应用的方式,使开发操作和使用更方便。

在Bun开发技术文档中,这部分的内容被称为file-io,其链接如下:

bun.sh/docs/api/fi...

基本形式

bun file应用的基本形式非常简单,就是Bun.file(),read(),write(), delete()。

  • file

file()方法,可以用于打开一个文件,操作结果是一个文件对象(描述符)。

  • read

在创建了文件对象之后,bun file为其提供了多种带有格式转换的read方法,可以以不同的形式来读取文件的内容,这个特性对于开发非常灵活和方便。

  • write

文件打开之后,就可以向文件中写入内容。bun file提供的写入功能也非常强大和多样。

  • delete

就是从文件系统中删除文件,也是通过文件对象进行操作。

上述基本形式的操作,示例代码如下:

js 复制代码
// 连接或者打开文件
const foo = Bun.file("foo.txt"); // relative to cwd

// 检查文件状态
const exists = await foo.exists(); 

foo.size; // number of bytes
foo.type; // MIME type


// 读取内容

await foo.text(); // contents as a string
await foo.stream(); // contents as ReadableStream
await foo.arrayBuffer(); // contents as ArrayBuffer
await foo.bytes(); // contents as Uint8Array

// 读取前设置类型,方便正确处理和解码
const notreal = Bun.file("notreal.json", { type: "application/json" });

// 删除文件
await Bun.file("logs.json").delete();

Bun file实现的文件写操作,有点复杂多样,也比较重要,笔者认为需要在独立的章节中专门进行探讨。

写文件

bun file写文件的基本形式是:

Bun.write(destination, data): Promise

按照其技术文档的说法,这个方法是一个可以将所有类型的负载写入磁盘的多功能的工具。

方法的第一个参数,是写入的目标,它可能包括以下几种形式:

  • 字符串: 文件系统的位置(写入文件名)
  • URL: 可以使用 file:// 来描述文件位置
  • BunFile: bun file文件对象示例

第二个参数是需要写入的数据和信息,可以是如下类型:

  • string,字符串
  • Blob
  • BunFile
  • ArrayBuffer or SharedArrayBuffer
  • TypedArray (Uint8Array, et. al.)
  • Response

下面是一些场景和示例:

js 复制代码
// 写入文本文件    
const data = `It was the best of times, it was the worst of times.`;
await Bun.write("output.txt", data);

// 二进制模式    
const encoder = new TextEncoder();
const data = encoder.encode("datadatadata"); // Uint8Array
await Bun.write("output.txt", data);
    
// 文件复制,会自动创建目标文件
const input = Bun.file("input.txt");
const output = Bun.file("output.txt"); // doesn't exist yet!
await Bun.write(output, input);
    
//  写入标准输出,如控制台    
const input = Bun.file("input.txt");
await Bun.write(Bun.stdout, input);

// http响应写入文件
const response = await fetch("https://bun.sh");
await Bun.write("index.html", response);
    

笔者感觉这套bun file的设计和实现很不错,它跳脱出了传统操作系统中对于文件处理的传统方式,更像是网络时代的新模式,比如格式转换,支持标准响应等等,和网络访问的API是一致的。

另外,笔者注意到,好像这里没有提供一般文件API提供的close方法,但愿是bun可以帮助自动处理,因为文件句柄没有及时正确关闭的话,会出现一些操作系统层面的问题和风险。

FileSink

除了标准的文件写入之外,bun file提供了一个原生的增量写入API,叫FileSink(文件沉没??),其实可能就是优化的增量写入。这个设计,在某些应用场景中非常有用,比如日志记录(数据库WAL?)和文件合并,它可以持续不断的进行写入操作,并且可以通过缓存和批量操作,来保证操作性能和效率。

下面是它的用法:

js 复制代码
// 目标和写入器
const file = Bun.file("output.txt");
const writer = file.writer();

// 持续写入操作,其实可能是缓存    
writer.write("it was the best of times\n");
writer.write("it was the worst of times\n");   
    
// 写磁盘    
writer.flush(); // write buffer to disk

// 控制水位,用于自动flush
const writer = file.writer({ highWaterMark: 1024 * 1024 }); // 1MB
    
// 暂停和恢复    
writer.unref();
// to "re-ref" it later
writer.ref(); 
    
// 关闭写入器    
writer.end();    

文件夹和fs模块

虽然bun file在文件操作方面已经基本完善,但还有有少数功能包括文件夹等操作没有完全实现。在这种情况下,bun的策略是提供了一个类似于node:fs的模块实现。我们来简单了解一下:

js 复制代码
import { readdir, mkdir } from "fs/promises";

// 列出当前文件夹所有文件
const files1 = await readdir(import.meta.dir);

// 递归列出子文件夹内容
const files2 = await readdir("../", { recursive: true });

// 创建文件夹,可选递归
await mkdir("path/to/dir", { recursive: true });

bun f.ts
[
  ".env", ".git", ".gitignore", ".vscode", "b58.js", "b58a.js", "brtree.js", "bstree.js", 
  "wslog.js"
]
[
  ".env", ".git", ".gitignore", ".vscode", "b58.js", "b58a.js", "brtree.js", "bstree.js",
  "wslog.js", "lib\\cp1305.js", "lib\\gtop.js", "lib\\jscha20.js", ....
]

应用场景

前面我们已经了解到bun file的基本原理和操作方式。下面简单的来看一下它们是被如何应用到一些实际和常用的业务场景当中的:

  • 静态文件服务(临时性的)
js 复制代码
Bun.serve({
    routes: {
        // Per-HTTP method handlers
        "/public": {
        GET: async req => new Response(await Bun.file("./public.html").bytes(), 
            { headers: { "Content-Type": "text/html",}}),
    ...
  • 远程下载文件
js 复制代码
const response = await fetch(
    "https://github.com/oven-sh/bun/releases/latest/download/bun-linux-x64.zip");
    
await Bun.write("bun-latest.zip", response);
    
  • 文件上传服务
js 复制代码
Bun.serve({
  async fetch(req) {
    const url = new URL(req.url);

    if (req.method === "POST" && url.pathname === "/upload") {
      const formData = await req.formData();
      const file = formData.get("file");

      if (file instanceof File) {
        const buffer = await file.arrayBuffer();
        const filename = file.name;

        // 保存到服务器本地
        await Bun.write(`./uploads/${filename}`, Buffer.from(buffer));

        return new Response("✅ 文件上传成功");
      } else {
        return new Response("❌ 没有找到上传文件", { status: 400 });
      }
    }   
  • http响应流
js 复制代码
const file = Bun.file("./video.mp4");
return new Response(file.stream(), {
  headers: {
    "Content-Type": "video/mp4",
    "Content-Length": (await file.size).toString(),
  },
});
    

从这些例子,我们应该可以体会和感受到,bun file提供的API在很多Web应用中的使用更加简洁方便。

操作系统指令

bun file功能模块,主要的设计目标,还是真的文件相关数据的处理。实际上,所有的操作系统,都具备完备的文件、目录和文件系统的操作功能。所有要进行更通用的文件操作,可能在某些情况下使用操作系统提供的文件操作命令,可能更加简单有效。

如果有这方面的需求,可以考虑使用bun的shell运行功能,来执行一个操作系统指令来完成工作。

简单的示例代码如下:

js 复制代码
     
    let cmdList;
    if (process.platform.startsWith("win")) {
        cmdList = [ "cmd","/c", " cd /d c:/temp && del *.jpg"];
    } else {
        cmdList = [ "sh","-c", " cd /opt/uploads/ && rm *.jpg"];
    }

    const proc = Bun.spawn(cmdList, { stdout: "pipe", stderr: "pipe" });

    const text = await new Response(proc.stdout).text();
    console.log("输出:\n",text);

    const err = await new Response(proc.stderr).text();
    if (err) console.error("错误输出:\n", err);   
    

这个示例可以看到利用操作系统文件指令来操作文件的一些特点:

  • 需要spawn来包裹和调用系统命令

  • 系统操作指令,可能根据不同系统而有差异,特别是windows,有很多不便的地方

  • 输入输出处理比较复杂

  • 操作系统和文件系统级别的调用,理论上有更好的性能

  • 操作和实现可能确实更方便

性能

关于性能,一直是bun的优势和注重的地方。刚好笔者看到技术文档里面就有相关的内容,就不自己做测试了,直接引用这个测试过程和结果,并简单解读分析一下:

  • 这个测试的操作很简单,就是使用串流的方式,读取一个文件,发送到标准输出
  • 测试使用了三种实现方式,包括bun file,node fs,和系统命令cat
  • 使用sha256检查输出,确认三者的操作结果是一致的
  • 测试操作多轮,消除热启动和随机干扰
  • 三者分别耗时为 50ms,500ms和120ms,差异还是很明显
  • 比较意外的是,bun的性能比cat稍好
  • 仔细观察,相比cat,bun还是耗费更多的用户态时间,但系统态真的比cat还好

这个测试结果,可能可以给我们一个提示,就是在某些情况下,可能使用bun进行文件操作,比系统级的命令,可能更有效率,而且更好控制。如果稳定性可以得到验证,同时你的应用系统中,有大量的文件操作,无疑相对而言,你应当选择bun file。

在技术文档中,bun还简单解释了一下它在文件操作方面的优化设计。在当前平台上,bun会尽可能的调用效率最高,性能最好的系统级别操作,如下图所示:

类定义

最后来了解一下file的类结构和定义:

js 复制代码
    
interface Bun {
  stdin: BunFile;
  stdout: BunFile;
  stderr: BunFile;

  file(path: string | number | URL, options?: { type?: string }): BunFile;

  write(
    destination: string | number | BunFile | URL,
    input:
      | string
      | Blob
      | ArrayBuffer
      | SharedArrayBuffer
      | TypedArray
      | Response,
  ): Promise<number>;
}

interface BunFile {
  readonly size: number;
  readonly type: string;

  text(): Promise<string>;
  stream(): ReadableStream;
  arrayBuffer(): Promise<ArrayBuffer>;
  json(): Promise<any>;
  writer(params: { highWaterMark?: number }): FileSink;
  exists(): Promise<boolean>;
}

export interface FileSink {
  write(
    chunk: string | ArrayBufferView | ArrayBuffer | SharedArrayBuffer,
  ): number;
  flush(): number | Promise<number>;
  end(error?: Error): number | Promise<number>;
  start(options?: { highWaterMark?: number }): void;
  ref(): void;
  unref(): void;
} 
    

简单解读一下:

  • 有关于file的方法,Bun只有file和write
  • fiiesink 竟然是单独设计的接口
  • Bunfile, 只有size, type两个属性,和一个exists判断方法,没有进一步的stat?
  • stuin, stuout, stderr 同样实现了Bun file接口

小结

作为系列文章中的第九篇,本文探讨了bun内置的文件处理模块,包括file方法,读取和写入操作的一般形式和流程。后续还探讨了fs module,并简单展示和分析了bun官方的性能测试和比较过程。

相关推荐
Kookoos7 分钟前
ABP VNext + MongoDB 数据存储:多模型支持与 NoSQL 扩展
后端·mongodb·c#·.net·abp vnext
加瓦点灯18 分钟前
深挖 JVM 关闭钩子与 Signal 机制:优雅停机背后的秘密
后端
Kier24 分钟前
🚀 前端实战:优雅地实现一个通用Blob文件下载方法
前端·javascript·axios
前端Hardy24 分钟前
从生活场景学透 JavaScript 原型与原型链
前端·javascript
程序小武33 分钟前
Python面向对象编程-类方法与静态方法
后端
GalaxyPokemon43 分钟前
RPC - Response模块
java·前端·javascript
加瓦点灯1 小时前
通过 Netty 的 Pipeline 学习责任链设计模式
后端
加密社1 小时前
Solana 开发实战:Rust 客户端调用链上程序全流程
开发语言·后端·rust
YGGP1 小时前
吃透 Golang 基础:Goroutine
后端·golang