04 fs 回调、Promise 与同步 API
1. 三种风格
回调:
import { readFile } from "node:fs";
readFile("package.json", "utf8", (error, content) => {
if (error !== null) {
console.error(error);
return;
}
console.log(content);
});
Promise:
import { readFile } from "node:fs/promises";
const content = await readFile("package.json", "utf8");
同步:
import { readFileSync } from "node:fs";
const content = readFileSync("package.json", "utf8");
2. 项目中的选择
请求处理和后台任务优先 Promise/Stream。同步 API 会阻塞当前 Node.js 事件循环:读取尚未结束时,这个进程不能继续处理其他 JavaScript 请求。
同步 API 可谨慎用于程序启动阶段的小型、可信配置,但仍要处理异常。不要在 Express 路由中使用 readFileSync() 读取上传文件。
3. Buffer 与字符串
不传编码时:
const bytes: Buffer = await readFile(filePath);
传入 UTF-8 时:
const text: string = await readFile(filePath, "utf8");
ZIP、图片、PDF 是二进制数据,不应该先转成 UTF-8 字符串。转换可能产生替换字符并破坏原始字节。
4. 错误处理
async function loadText(filePath: string): Promise<string> {
try {
return await readFile(filePath, "utf8");
} catch (error: unknown) {
if (
typeof error === "object" &&
error !== null &&
"code" in error &&
error.code === "ENOENT"
) {
throw new Error("指定文本不存在", { cause: error });
}
throw error;
}
}
不要捕获后返回空字符串:
catch {
return "";
}
它会把“文件不存在”伪装成“空文件”。
5. readFile() 的边界
readFile() 会尝试把完整文件读入内存,适合有明确大小上限的小文本。例如单个 Markdown 已限制为 2 MiB。它不适合数 GB 的 FASTQ、BAM 或 ZIP。
读取前 stat() 可以提供早期拒绝,但文件可能在 stat() 后变化,因此仍需限制上游上传大小、处理并发和实际读取错误。
实操
编写三个版本读取同一文件,并在读取期间用另一个 HTTP 请求观察同步版本的阻塞。然后读取 ZIP:一次以 Buffer,一次错误地使用 UTF-8,对比长度和是否还能被 ZIP 工具识别。
练习题
- 什么场景可以接受同步读取?
- 为什么
Buffer.toString("utf8")不适合任意二进制文件? readFile()已经异步,为什么仍可能撑爆内存?- 为什么不能把所有读取错误都转换成空字符串?