Menu

Node.js fs/promises:用 async/await 操作文件系统

fs/promises 是 Node fs 模块的 Promise 版本:readFile、writeFile、readdir、stat、mkdir、rm 和 rename 都返回可以 await 的 Promise,错误用 try/catch 捕获。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

fs/promises 是 Node fs 模块基于 Promise 的版本。它的函数(readFile、writeFile、readdir、stat、mkdir、rm、rename 等)不接收回调,而是返回一个 Promise,所以你可以 await 它们,并用 try/catch 捕获错误。用 require('fs/promises') 或 import fs from 'fs/promises' 导入它。

它会打印 [ 'write docs', 'ship it' ]:每个 await 都会等一个操作完成后才运行下一行,所以追加总是发生在第一次写入之后。在 ES 模块中,await 可以直接写在顶层;在 CommonJS 中,要把代码包在一个 async 函数里。

最常用的函数

函数名和回调 API 一样,只是去掉了回调。readFile 和 writeFile 页面详细介绍了这两个函数。

函数作用resolve 的值
readFile(path, 'utf8')读取整个文件文件内容
writeFile(path, data)创建或替换文件undefined
appendFile(path, data)追加到文件末尾undefined
readdir(path)列出文件夹内容名称数组
stat(path)大小、日期、是文件还是文件夹一个 Stats 对象
mkdir(path, { recursive: true })创建文件夹第一个被创建的文件夹,或 undefined
rename(from, to)移动或重命名undefined
rm(path, { recursive: true, force: true })删除文件或文件夹undefined

用 readdir 和 stat 列出文件夹

readdir 只返回名称。对每个名称调用 stat 可以得到它的大小,或区分文件和文件夹;也可以传入 { withFileTypes: true },直接得到已经知道自身类型的条目。

循环会打印 README.md 6 bytes、notes.txt 3 bytes 和 src folder。对不存在的路径,stat 会以 ENOENT reject,所以当你还需要文件详情时,它可以顺便用来快速检查文件是否存在。

重命名、移动和删除

rename 既能重命名文件,也能移动文件(在同一个磁盘内)。rm 用来删除;删除有内容的文件夹需要 recursive: true,而 force: true 能让它在路径已经不存在时不报错。

文件从 drafts 移到了 published,并换了新名字;rm 之后,blog 文件夹里只剩下 published。把第二个 rm 中的 force: true 删掉,它就会以 ENOENT reject。

用 try/catch 处理错误

操作失败时,它的 Promise 会 reject。配合 await,这个 rejection 会变成一个异常,你可以像捕获其他异常一样捕获它。错误的 code 会告诉你出了什么问题。

第一次调用找不到文件,返回 [];第二次打印 Ada。需要了解的错误码:ENOENT(文件不存在)、EEXIST(已经存在)、EISDIR(需要文件的地方是一个文件夹)、ENOTEMPTY(文件夹里还有内容)和 EACCES(没有权限)。

一个既没有 await 也没有 .catch() 的 Promise,失败时会变成未处理的 rejection,Node 会报错退出。每个调用都要 await,或者把它的 Promise 返回给会 await 它的调用者。

用 Promise.all 同时读取多个文件

在 for 循环中使用 await,文件会一个接一个地读取。如果这些读取互不依赖,就同时启动它们,再用 Promise.all 只等待一次:磁盘会同时处理它们。

Promise.all 会保持输入数组的顺序,所以无论哪个读取先完成,结果都是 [ 'alpha', 'beta', 'gamma' ],拼接后的长度是 14。把数组中的 'b.txt' 改成 'x.txt',整个 await 就会以 ENOENT reject。

如果你需要每个结果,即使某些读取失败也要,就用 Promise.allSettled;两者在 Promise 页面 中都有介绍。

常见错误

把两个模块搞混。 require('fs').readFile 需要一个回调;require('fs/promises').readFile 返回一个 Promise。不传回调就调用回调版本,会抛出 The "cb" argument must be of type function。

忘记写 await。 const text = fs.readFile(...) 得到的是一个待定的 Promise,text.split 会抛出 text.split is not a function。

并行写入同一个文件。 用 Promise.all 对同一个路径执行多次写入,可能让文件内容混在一起。这类操作要按顺序执行。

常见问题

fs.promises 和 fs/promises 有什么区别?

没有区别。require('fs').promises 和 require('fs/promises') 是同一个对象。如今通常用 fs/promises 这个路径(或 node:fs/promises)来导入它。

fs/promises 里有 existsSync 吗?

没有。Promise API 有 access,路径不存在时它会 reject,但常见的做法是直接调用你需要的操作,然后捕获 ENOENT。要做快速检查,普通 fs 模块里的 existsSync 依然可用。

fs/promises 比回调 API 慢吗?

因为 Promise 的开销会稍慢一点,但和磁盘耗时相比可以忽略不计。按可读性来选;大多数新代码用的都是 Promise API。

如何把回调式的 fs 函数变成 Promise?

很少需要这样做,因为 fs/promises 已经覆盖了所有文件函数。对于其他回调式 API,util.promisify(fn) 会返回一个返回 Promise 的版本。

Coddy 编程语言插图

和 Coddy 一起学编程

开始