首页 / Node.js 教程 / fs 读写文件

Node.js 教程

fs 读写文件

本教程共 76 篇 · 第 18 篇 · 更新于 2026-07-25 · 约 5 分钟阅读

Node.jsfs文件操作promises读写

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 / readFileflag 选项。

标志含义
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);