Node.js fs 模块
前面我们学会了用 Node.js npm 入门 装依赖,但程序还没有真正碰过硬盘。这一篇解决核心问题,怎么用 JavaScript 读写磁盘上的文件,fs 是 File System 的缩写,是 Node.js 内置的文件系统模块,不需要安装。
同步读取与异步读取
fs 模块几乎每个方法都有两个版本,同步版本和异步版本,这是新手最容易搞混的地方。
同步版本 readFileSync
const fs = require('fs');
// 同步读取,会阻塞主线程,直到文件读完才执行下一行
const data = fs.readFileSync('hello.txt', 'utf8');
console.log(data);
readFileSync 名字里的 Sync 就是同步的意思。程序执行到这里会停下来等文件读完,然后才继续往下走。代码写起来简单,一行拿结果。
异步版本 readFile
const fs = require('fs');
// 异步读取,不会阻塞主线程,文件读完回调才执行
fs.readFile('hello.txt', 'utf8', (err, data) => {
if (err) {
console.error('读取失败', err.message);
return;
}
console.log(data);
});
console.log('这里先执行');
异步版本多了一个回调函数参数,文件读完后回调才会被调用。回调风格是后面 Node.js 回调与回调地狱 那篇的主角,这里先认识它。
同步为什么会阻塞
Node.js 是单线程的,所有代码都在一条主线程上跑。同步读取时主线程干等文件读完,期间什么都做不了。想象一下读取一个 2GB 的日志文件,主线程可能要卡住好几秒,服务器在这几秒内无法响应任何请求,这就是阻塞的危害。
写服务器程序时要用异步版本。写一次性脚本时用同步版本也可以接受,代码更短更好读。
异步的两种风格
回调风格 fs.readFile 的结果从回调参数 (err, data) 里拿,第一个参数永远是错误对象,没有错误时是 null。Node.js 10 开始还提供 fs.promises,返回 Promise 而不是回调,可以用 async/await 写,代码像同步一样直白。
const fs = require('fs').promises;
async function main() {
const data = await fs.readFile('hello.txt', 'utf8');
console.log(data);
}
main().catch(err => console.error('读取失败', err.message));
两种风格功能完全一样。这篇的示例主要用 fs.promises,写起来更清晰。
文件操作
writeFile 写入与 appendFile 追加
writeFile 会覆盖已有内容,文件不存在时自动创建。appendFile 在末尾追加,不会覆盖。
const fs = require('fs').promises;
async function main() {
// writeFile 覆盖写
await fs.writeFile('note.txt', '第一行内容');
console.log(await fs.readFile('note.txt', 'utf8'));
await fs.writeFile('note.txt', '第二行内容');
console.log(await fs.readFile('note.txt', 'utf8'));
// appendFile 追加
await fs.appendFile('log.txt', '第一条日志\n');
await fs.appendFile('log.txt', '第二条日志\n');
console.log(await fs.readFile('log.txt', 'utf8'));
}
main().catch(err => console.error('失败', err.message));
rename 重命名与 unlink 删除
const fs = require('fs').promises;
async function main() {
await fs.writeFile('old.txt', '内容');
// rename 重命名
await fs.rename('old.txt', 'new.txt');
console.log('重命名完成');
// unlink 删除
await fs.unlink('new.txt');
console.log('文件已删除');
}
main().catch(err => console.error('失败', err.message));
目录操作
mkdir 创建目录与 readdir 读取目录
不加 recursive: true 时,父目录不存在会直接报错。readdir 返回文件名数组,默认只有名字,要拿详细信息得配合 fs.stat。
const fs = require('fs').promises;
async function main() {
// recursive 为 true 时,中间层级不存在也会一并创建
await fs.mkdir('data/2026/08', { recursive: true });
console.log('目录创建完成');
// readdir 返回文件名数组
const files = await fs.readdir('data');
console.log(files);
}
main().catch(err => console.error('失败', err.message));
rm 删除目录
const fs = require('fs').promises;
async function main() {
// recursive 为 true 时,目录非空也会递归删除
await fs.rm('data', { recursive: true });
console.log('目录已删除');
}
main().catch(err => console.error('失败', err.message));
删除非空目录必须加 recursive: true,否则会报错。这个操作不可恢复,删除前要确认路径写对了。
文件信息 fs.stat
fs.stat 返回文件或目录的详细信息,包括大小、修改时间、类型等。
const fs = require('fs').promises;
async function main() {
const info = await fs.stat('hello.txt');
console.log('大小', info.size, '字节');
console.log('是文件', info.isFile());
console.log('是目录', info.isDirectory());
console.log('修改时间', new Date(info.mtimeMs));
}
main().catch(err => console.error('失败', err.message));
常用字段有 size 文件大小字节数,mtimeMs 修改时间的毫秒时间戳,isFile() 和 isDirectory() 判断类型。
编码说明
上面的示例都传了 'utf8'。不传的话,读出来的是 Buffer 二进制数据。
const fs = require('fs').promises;
async function main() {
const buf = await fs.readFile('hello.txt');
console.log(buf);
// 输出 <Buffer 68 65 6c ...>,是一堆十六进制字节
}
main().catch(err => console.error('失败', err.message));
传 'utf8' 得到的是可以直接用的字符串,不传得到 Buffer 原始字节。Buffer 是处理二进制数据的核心工具,后面的 Node.js buffer 模块 会专门讲解。
常见坑
第一,同步方法名都带 Sync 后缀,readFileSync、writeFileSync,不带 Sync 的是异步版本,名字写错或混用是高频错误。第二,writeFile 是覆盖写不是追加,追加要用 appendFile。第三,mkdir 和 rm 处理多层目录或非空目录必须加 recursive: true。第四,相对路径是相对当前工作目录的,不是相对代码文件,换个目录运行脚本路径就错了,路径问题下一篇专门解决。
实践
程序一,大小写转换
读 a.txt 内容,转成大写后写入 b.txt。
// upper.js
const fs = require('fs').promises;
async function main() {
const data = await fs.readFile('a.txt', 'utf8');
const upper = data.toUpperCase();
await fs.writeFile('b.txt', upper);
console.log('转换完成,共', upper.length, '个字符');
}
main().catch(err => console.error('失败', err.message));
准备 a.txt 并运行。
echo "hello node.js" > a.txt
node upper.js
运行后 b.txt 里是 HELLO NODE.JS。
程序二,目录文件清单
列出指定目录下所有文件的名字和大小。
// list.js
const fs = require('fs').promises;
const path = require('path');
async function main(dir) {
const names = await fs.readdir(dir);
for (const name of names) {
const info = await fs.stat(path.join(dir, name));
console.log(name, info.isFile() ? '文件' : '目录', info.size, '字节');
}
}
const dir = process.argv[2] || '.';
main(dir).catch(err => console.error('失败', err.message));
运行。
node list.js .
node list.js source
第一个参数是要列出的目录,不传默认当前目录。这里用到了 path.join 拼接路径,fs.stat 拿大小,路径拼接的原理和跨平台写法,见 Node.js path 模块。