Menu

Node.js fs.readFile:读取文件(同步、异步、Promise)

在 Node.js 中用 fs.readFile(回调)、fs.readFileSync 或 fs/promises 的 await readFile 读取文件。传入 'utf8' 得到字符串,文件不存在时处理 ENOENT。

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

fs.readFile(path, 'utf8', callback) 读取整个文件,并把内容以字符串形式传给回调。fs.readFileSync(path, 'utf8') 会等读取完成后直接返回字符串,而 fs/promises 中的 await readFile(path, 'utf8') 能做到同样的事,又不会阻塞程序。省略 'utf8',你得到的就不是文本,而是一个装着原始字节的 Buffer。

readFile called 会先打印出来:readFile 只是开始读取,等内容准备好后才运行回调。删掉 'utf8', 再运行一次:回调收到的是 <Buffer 42 75 79 20 6d 69 6c 6b 0a 43 61 6c 6c 20 41 64 61>,也就是文件的原始字节。

readFile、readFileSync 与 fs/promises 的区别

三者读取的是同一个文件。区别在于你如何拿到结果,以及程序的其他部分是否需要等待。

同步那一行最先打印,因为在它返回之前,其他代码都不会运行。另外两个在主代码之后完成,谁的读取先完成,谁就先打印。

readFileSyncreadFile(回调)fs/promises 的 readFile
返回值文件内容undefined一个结果为文件内容的 Promise
是否阻塞程序是否否
错误直接抛出,用 try/catch回调的第一个参数Promise 被拒绝,配合 await 用 try/catch
适用场景启动代码和脚本旧的回调式代码新代码

在启动时读取配置或在短小的脚本中,用 readFileSync。在服务器中,同步读取会在磁盘工作时卡住其他所有请求,所以那里要用 Promise 版本。该 API 的其余内容见 fs/promises 页面。

处理文件不存在(ENOENT)

文件不存在时,错误带有 code: 'ENOENT'(“error, no entry”,即没有这个条目)。检查 err.code,就能把文件不存在和权限问题等其他失败区分开。

第一次调用找不到文件,于是回退到 default settings;第二次读到了 dark mode。重新抛出其他错误,可以让真正的问题暴露出来,而不是悄悄地用默认值顶替。

fs.existsSync:为什么直接尝试读取更好

fs.existsSync(path) 返回 true 或 false,很容易让人想在读取前先检查一下:

if (fs.existsSync('data.txt')) {
    const text = fs.readFileSync('data.txt', 'utf8');
}

这样做用了两步,其实一步就够;而且文件可能在两步之间被删除,读取依然可能抛出错误。像上面那样直接尝试读取,并处理 ENOENT。如果你只需要知道某个东西是否存在、并不打算打开它,比如决定要不要创建一个文件夹,用 existsSync 就没问题。

读取 JSON 文件

先读取文本,再用 JSON.parse 解析。有语法错误的文件会抛出 SyntaxError,所以把它也放在同一个 try 里。

它会打印 3001 和 boolean:解析之后,值都有了各自的 JSON 类型。require('./config.json') 也能用,但它会缓存结果,所以即使文件变了,第二次 require 返回的仍是旧对象。

逐行读取文件

对于能放进内存的文件,整个读出来,再按换行符拆分。最后一个换行符会留下一个空字符串,把它过滤掉。

拆分得到 4 项,最后一项是空的,所以循环打印出三行带编号的内容。在 Windows 上写的文件用 \r\n 结束一行;按 /\r?\n/ 拆分,两种情况都能处理。

几 GB 的日志放不进内存。改用流来读取,这样每次只在内存中保留一行:

const fs = require('fs');
const readline = require('readline');

async function main() {
    const rl = readline.createInterface({
        input: fs.createReadStream('big.log'),
        crlfDelay: Infinity,
    });
    for await (const line of rl) {
        console.log(line);
    }
}
main();

常见错误

在回调之外使用结果。 const text = fs.readFile(...) 是 undefined。文件内容只存在于回调内部,或者 await 之后。

忘记指定编码。 不传 'utf8' 会得到一个 Buffer,data.split('\n') 会抛出 data.split is not a function。只要你想要文本,就传入编码。

相对路径找错了文件夹。 'data.txt' 是在启动 node 的地方查找的。对于和脚本放在一起的文件,使用 path.join(__dirname, 'data.txt')。

常见问题

为什么 fs.readFile 返回 undefined?

fs.readFile 不会返回文件内容。它只是开始读取,稍后再用 (err, data) 调用你的回调。要么在回调里使用数据,要么调用 readFileSync,要么 await 它的 Promise 版本。

Node 在哪里查找 'data.txt' 这样的相对路径?

在当前工作目录,也就是你运行 node 时所在的文件夹,而不是脚本所在的文件夹。要读取和脚本放在一起的文件,用 path.join(__dirname, 'data.txt') 拼出路径。

fs.readFile 能从 URL 读取文件吗?

不能,它只能从文件系统读取。要通过 HTTP 获取文件,请使用 fetch,然后 await res.text()。

readFile 能处理多大的文件?

它会把整个文件加载进内存,所以适合配置文件、模板和小型数据文件。对于几百 MB 的文件,用 fs.createReadStream 分块读取。

Coddy 编程语言插图

和 Coddy 一起学编程

开始