fs 读写文件
本教程共 76 篇 · 第 18 篇 · 更新于 2026-07-25 · 约 5 分钟阅读
18. fs 读写文件
本节目标:用 fs/promises 读写文件,同步与异步对比,以及常见文件操作。
Node.js 跟文件系统打交道,离不开 fs 模块。读写配置、记录日志、处理用户上传——这些活儿每天都在做。本章带你把最常用的文件操作跑一遍,重点是 fs/promises 的异步写法,同步方法只作对比,让你知道什么时候该用、什么时候不该用。
18.1 三种 API 风格
fs 模块提供三套接口:
fs/promises(推荐):返回 Promise,配合async/await代码最清爽。- 回调风格:
fs.readFile(path, callback),Node 早期的经典写法。 - 同步方法:
fs.readFileSync(path),名字里带Sync,会阻塞事件循环。
import { readFile } from 'node:fs/promises';
// 现代写法:fs/promises + async/await
async function readConfig() {
try {
const data = await readFile('config.json', 'utf8');
return JSON.parse(data);
} catch (err) {
console.error('读取失败:', err.message);
}
}
同步方法只在启动时读配置文件、写死数据这种场景下才用。服务器运行期间大量调用 *Sync,会把事件循环卡住,请求就进不来了。
18.2 读取文件
读取文本文件
import { readFile } from 'node:fs/promises';
const content = await readFile('readme.txt', 'utf8');
console.log(content);
utf8 这个编码参数很重要。不加的话,readFile 返回的是 Buffer,适合读图片、PDF 这类二进制文件。
import { readFile } from 'node:fs/promises';
// 读二进制文件
const imageBuf = await readFile('avatar.png');
console.log(imageBuf.length); // 文件大小,单位字节
用回调风格读取
维护老项目时你可能还会见到:
import { readFile } from 'node:fs';
readFile('readme.txt', 'utf8', (err, data) => {
if (err) {
console.error('出错了:', err);
return;
}
console.log(data);
});
回调的第一个参数永远是错误对象,遵循「错误优先」约定。新项目直接上 fs/promises,不用再写这种嵌套。
同步读取(谨慎使用)
import { readFileSync } from 'node:fs';
const content = readFileSync('readme.txt', 'utf8');
简单直接,但记住:它在读文件期间,整个线程都在等硬盘转完。
18.3 写入文件
覆盖写入
import { writeFile } from 'node:fs/promises';
await writeFile('output.txt', 'Hello, Node.js!\n', 'utf8');
writeFile 默认覆盖原有内容。想保留旧数据,看下面的追加写法。
追加内容
import { appendFile } from 'node:fs/promises';
const logLine = `[${new Date().toISOString()}] 服务启动\n`;
await appendFile('app.log', logLine, 'utf8');
日志文件就是这么攒出来的。注意这里没有自动换行,需要你自己在字符串里加上 \n。
写入 JSON
import { writeFile } from 'node:fs/promises';
const config = { port: 3000, env: 'production' };
await writeFile('config.json', JSON.stringify(config, null, 2), 'utf8');
JSON.stringify 的第三个参数 2 表示缩进两个空格,生成的文件人类可读。
18.4 文件打开标志
有时候你需要更精细的控制,比如「只在文件不存在时才创建」。这时可以用 open 拿到文件描述符,或者用 writeFile / readFile 的 flag 选项。
| 标志 | 含义 |
|---|---|
r | 只读,文件必须存在 |
r+ | 读写,文件必须存在 |
w | 只写,不存在则创建,存在则截断 |
w+ | 读写,不存在则创建,存在则截断 |
a | 追加写入,不存在则创建 |
a+ | 追加读写,不存在则创建 |
wx | 只写,必须新建,存在则报错 |
import { writeFile } from 'node:fs/promises';
// 如果 file.txt 已存在,这行会抛错
await writeFile('file.txt', 'data', { flag: 'wx' });
wx 在做原子写、防止覆盖时很有用。
18.5 文件描述符操作
需要反复读写同一个大文件时,用 fs.open 拿到文件描述符(fd)更高效,不用每次都重新定位。
import { open } from 'node:fs/promises';
let fh;
try {
fh = await open('data.txt', 'a+');
await fh.write('追加一行\n');
// 回到文件开头读取
await fh.read({ buffer: Buffer.alloc(20), position: 0 });
} finally {
await fh?.close();
}
Warning文件描述符是有限资源,用完必须
close()。上面用了try...finally,确保即使抛错也能释放。
18.6 删除与重命名
import { unlink, rename } from 'node:fs/promises';
// 删除文件
await unlink('temp.txt');
// 重命名(也可用于移动文件)
await rename('old.txt', 'new.txt');
rename 在同个磁盘分区内是原子操作,但跨分区移动文件会失败,需要改用「复制 + 删除」兜底。
18.7 检查文件状态
import { stat, access } from 'node:fs/promises';
import { constants } from 'node:fs';
// 获取文件元数据
const info = await stat('report.pdf');
console.log(info.size); // 文件大小(字节)
console.log(info.isFile()); // true
console.log(info.isDirectory()); // false
console.log(info.mtime); // 最后修改时间
// 检查文件是否存在且可读
await access('report.pdf', constants.R_OK);
access 不返回具体信息,只在权限不足或文件不存在时抛错。注意别用它来做「存在性检查后再读写」——这是典型的竞态条件。直接读写,错了再处理异常,反而更可靠。
18.8 实战:安全的配置文件读写
把本节内容串起来,写一个小工具:读取 JSON 配置,不存在就用默认值创建。
import { readFile, writeFile } from 'node:fs/promises';
const DEFAULT_CONFIG = { port: 3000, host: '127.0.0.1' };
async function loadConfig(path) {
try {
const raw = await readFile(path, 'utf8');
return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
} catch (err) {
if (err.code === 'ENOENT') {
// 文件不存在,写一份默认配置
await writeFile(path, JSON.stringify(DEFAULT_CONFIG, null, 2));
return DEFAULT_CONFIG;
}
throw err;
}
}
const config = await loadConfig('config.json');
console.log(config);