六维教程

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));
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 后缀,readFileSyncwriteFileSync,不带 Sync 的是异步版本,名字写错或混用是高频错误。第二,writeFile 是覆盖写不是追加,追加要用 appendFile。第三,mkdirrm 处理多层目录或非空目录必须加 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 模块

上一篇
Node.js npm 入门
下一篇
Node.js path 模块