我们今天拥有的 JavaScript API 比十年前要好得多。考虑一下XMLHttpRequest
到fetch()
的转变:开发人员的体验明显更好,使我们能够编写更简洁的功能代码来完成同样的事情。异步编程 Promise 的引入允许这种改变,以及一系列使 JavaScript 更容易编写的其他改变。然而,有一个领域几乎没有创新:服务器端 JavaScript 运行时中的文件系统 API。
Node.js:当今文件系统 API 的起源
Node.js 最初于 2009 年发布, fs
模块也随之诞生。 fs
模块是围绕 Linux 的核心实用程序构建的,其中许多方法反映了其 Linux 灵感,例如rmdir
、 mkdir
和stat
。为此,Node.js 成功创建了一个低级文件系统 API,可以处理开发人员希望在命令行上完成的任何事情。不幸的是,创新就到此为止了。
Node.js 文件系统 API 的最大变化是引入了fs/promises
,将整个实用程序从基于回调的方法转移到基于 Promise 的方法。较小的增量更改包括实现网络流并确保读者也实现异步迭代器。 API 仍然使用专有的Buffer
类来读取二进制数据。 (尽管Buffer
现在是Uint8Array
的子类,但仍然存在不兼容性1导致使用Buffer
出现问题。)
即使是 Ryan Dhal 的 Node.js 继承者 Deno,也没有在文件系统 API 的现代化方面做太多工作。它主要遵循与 Node.js 中的fs
模块相同的模式,尽管它使用Uint8Array
,而 Node.js 使用Buffer
并在多个地方使用异步迭代器。除此之外,它仍然是 Node.js 中采用的相同的低级 API 方法。
只有 Bun,服务器端 JavaScript 运行时生态系统的最新成员,甚至尝试使用Bun.file()
2来现代化文件系统 API,其灵感来自fetch()
。虽然我对如何处理文件的重新思考表示赞赏,但当您处理多个文件时,为您想要处理的每个文件创建一个新对象可能会很麻烦(并且在处理数千个文件时会出现很大的性能下降)。除此之外,Bun 希望您使用 Node.js fs
模块进行其他操作。
现代文件系统 API 是什么样子的?
在花费数年时间与 Node.js fs
模块进行斗争并维护 ESLint 后,我问自己,现代文件系统 API 会是什么样子?以下是我想到的一些事情:
- 常见情况很容易。至少 80% 的时间,我要么读取文件,要么写入文件,或者检查文件是否存在。差不多就这样了。然而,这些操作充满了危险,因为我需要检查各种事情以避免错误或记住其他属性(即
{ encoding: "utf8" }
)。 - 错误很少见。我对
fs
模块最大的抱怨是它抛出错误的频率。对不存在的文件调用fs.stat()
会引发错误,这意味着您实际上需要将每个调用包装在try-catch
中。为什么?对于大多数应用程序来说,丢失文件并不是不可恢复的错误。 - 行动将是可观察的。在测试文件系统操作时,我真的只是想要一种方法来验证我期望发生的事情是否确实发生了。我不想与其他一些实用程序建立间谍网络,这些实用程序可能会也可能不会改变我正在观察的方法的实际行为。
- 嘲笑会很容易。我总是对模拟文件系统操作有多么困难感到惊讶。我最终使用了像
proxyquire
这样的东西,否则需要设置一个需要一段时间才能正确的模拟迷宫。这是文件系统操作的常见要求,令人惊讶的是没有解决方案。
考虑到这些想法,我继续设计 fsx。
FSX 基础知识
fsx 库3是我关于现代高级文件系统 API 应该是什么样子的所有想法的结晶。此时,它专注于支持最常见的文件系统操作,同时留下较少使用的操作(例如chmod
)。 (我并不是说将来不会添加这些操作,但对我来说,重点关注最常见的情况来开始然后以与初始方法相同的深思熟虑的方式构建更多功能非常重要。)
使用 fsx 运行时包
首先,fsx API 在三个运行时包中可用。这些包都包含相同的功能,但绑定到不同的底层 API。这些软件包是:
-
fsx-node
– fsx API 的 Node.js 绑定 fsx-deno
– fsx API 的 Deno 绑定fsx-memory
– 适用于任何运行时(包括 Web 浏览器)的内存中实现
因此,首先,您将使用最适合您的用例的运行时包。出于本文的目的,我将重点关注fsx-node
,但所有运行时包上都存在相同的 API。所有运行时包都会导出一个fsx
单例,您可以以类似于fs
的方式使用它。
import { fsx } from "node-fsx" ;
使用 fsx 读取文件
使用返回所需特定数据类型的方法读取文件:
-
jsx.text(filePath)
读取给定的文件并返回一个字符串。 -
jsx.json(filePath)
读取给定文件并返回 JSON 值。 -
jsx.arrayBuffer(filePath)
读取给定文件并返回ArrayBuffer
。
这里有些例子:
// read plain text const text = await fsx. text ( "/path/to/file.txt" ); // read JSON const json = await fsx. json ( "/path/to/file.json" ); // read bytes const bytes = await fsx. arrayBuffer ( "/path/to/file.png" );
如果文件不存在,每个方法都会返回undefined
而不是抛出错误。这意味着您可以使用if
语句而不是try-catch
,并且可以选择使用 nullish 合并运算符来指定默认值,如下所示:
// read plain text const text = await fsx. text ( "/path/to/file.txt" ) ?? "default value" ; // read JSON const json = await fsx. json ( "/path/to/file.json" ) ?? {}; // read bytes const bytes = await fsx. arrayBuffer ( "/path/to/file.png" ) ?? new ArrayBuffer ( 16 );
我觉得这种方法在 2024 年比不断担心不存在的文件出现错误更加 JavaScripty。
使用 fsx 写入文件
要写入文件,请调用fsx.write()
方法。该方法接受两个参数:
-
filePath:string
– 要写入的路径 value:string|ArrayBuffer
– 要写入文件的值
这是一个例子:
// write a string await fsx. write ( "/path/to/file.txt" , "Hello world!" ); const bytes = new TextEncoder (). encode ( "Hello world!" ).buffer; // write a buffer await fsx. write ( "/path/to/file.txt" , buffer);
作为额外的好处, fsx.write()
将自动创建任何尚不存在的目录。这是我经常遇到的另一个问题,我认为它应该在现代文件系统 API 中“正常工作”。
使用 fsx 检测文件
要确定文件是否存在,请使用fsx.isFile(filePath)
方法,如果给定文件存在则返回true
,否则返回false
。
if ( await fsx. isFile ( "/path/to/file.txt" )) { // handle the file }
与fs.stat()
不同,如果文件不存在,此方法只是返回false
,而不是抛出错误。与等效的fs.stat()
代码进行比较:
try { const stat = await fs. stat (filePath); return stat. isFile (); } catch (ex) { if (ex.code === "ENOENT" ) { return false ; } throw ex; }
删除文件和目录
fsx.delete()
方法接受单个参数,即要删除的路径,并且适用于文件和目录。
// delete a file await fsx. delete ( "/path/to/file.txt" ); // delete a directory await fsx. delete ( "/path/to" );
fsx.delete()
方法是故意激进的:即使目录不为空,它也会递归删除目录(实际上rmdir -r
)。
fsx 日志记录
fsx 的主要功能之一是,借助其内置的日志记录系统,可以轻松确定使用哪些参数调用了哪些方法。要在fsx
实例上启用日志记录,请调用logStart()
方法并传入日志名称。完成日志记录后,调用logEnd()
并传入相同的名称以检索日志条目数组。这是一个例子:
fsx. logStart ( "test1" ); const fileFound = await fsx. isFile ( "/path/to/file.txt" ); const logs = fsx. logEnd ( "test1" );
每个日志条目都是一个包含以下属性的对象:
-
timestamp
– 创建日志时的数字时间戳 type
– 描述日志类型的字符串data
– 与日志相关的附加数据
对于方法调用,日志条目的type
是"call"
, data
属性是一个包含以下内容的对象:
-
methodName
– 被调用的方法的名称 args
– 传递给方法的参数数组。
对于前面的示例, logs
将包含单个条目:
// example log entry { timestamp : 123456789 , type : "call" , data : { methodName : "isFile" , args : [ "/path/to/file.txt" ] } }
了解这一点后,您可以轻松地在测试中设置日志记录,然后检查调用了哪些方法,而无需使用第三方间谍库。
使用 fsx 实现
fsx 的设计是在fsx-core
包中包含抽象的核心功能。每个运行时包都通过封装在名为impl 的对象中的文件系统操作的运行时特定实现来扩展该功能。每个运行时包实际上导出三样东西:
-
fsx
单例 - 一个构造函数,可让您创建另一个
fsx
实例(例如fsx-node
中的NodeFsx
) - 一个构造函数,可让您为运行时包创建 impl 实例(例如
node-fsx
中的NodeFsxImpl
)
这使您可以只使用您想要的功能。
fsx 中的基本 impl 和活动 impl
每个fsx
实例都是使用一个基本 impl创建的,该 impl 定义了fsx
对象在生产中的行为方式。活动 impl是在任何给定时间使用的 impl,它可能是也可能不是基本 impl。您可以通过调用fsx.setImpl()
来更改活动的 impl。例如:
import { fsx } from "fsx-node" ; fsx. setImpl ({ json () { throw Error ( "This operation is not supported" ); } }) // somewhere else await fsx. json ( "/path/to/file.json" ); // throws error
在此示例中,基本实现被替换为自定义实现,该自定义实现在调用fsx.json()
方法时会引发错误。这使得您可以轻松地模拟测试方法,而不必担心它可能如何影响整个包含的fsx
对象。
交换 impls 以进行测试
假设您有一个名为readConfigFile()
的函数,它使用node-fsx
中的fsx
单例来读取名为config.json
的文件。当需要测试该函数时,您真的不希望它实际访问文件系统。您可以换出fsx
的 impl,并将其替换为fsx-memory
提供的内存中文件系统实现,如下所示:
import { fsx } from "fsx-node" ; import { MemoryFsxImpl } from "fsx-memory" ; import { readConfigFile } from "../src/example.js" ; import assert from "node:assert" ; describe ( "readConfigFile()" , () => { beforeEach (() => { fsx. setImpl ( new MemoryFsxImpl ()); }); afterEach (() => { fsx. resetImpl (); }); it ( "should read config file" , async () => { await fsx. write ( "config.json" , JSON . stringify ({ found: true }); const result = await readConfigFile (); assert. isTrue (result.found); }); });
这就是使用 fsx 在内存中模拟整个文件系统是多么容易。您不必像模块加载器拦截那样担心导入所有测试模块的顺序,也不需要经历包含模拟库的过程以确保一切正常。您只需更换测试的 impl,然后再重置它。通过这种方式,您可以以更高性能且不易出错的方式测试文件系统操作。
命名注意事项
不幸的是,在我发布 fsx 的时候,亚马逊发布了一款名为 FSx 4的产品。如果这个库获得任何关注,我可能会重命名它,欢迎提出建议5 。
结论和希望得到的反馈
很长一段时间以来,我们一直在 JavaScript 运行时中处理同样笨重的低级文件系统 API。 fsx 库是我重新构想现代文件系统 API 的尝试,如果我们花一些时间关注最常见的情况并改进 JavaScript 语言今天提供的功能,那么它会是什么样子。通过从头开始重新思考,我认为 fsx 提供了一种更愉快的文件系统体验。
基础库只关注我最常使用的方法,但我确实计划在我理解和思考用例时添加更多方法。您今天就可以尝试6并欢迎提供反馈7 。我很想知道你的想法!
脚注
原文: https://humanwhocodes.com/blog/2024/01/fsx-modern-filesystem-api-javascript/