人人都会AI编程

文件读写、目录操作、文件监听、权限管理

更新时间:2026-07-11

在 Node.js 开发中,与文件系统打交道是最基础也最高频的操作之一。无论是读取配置、存储日志、处理上传文件,还是生成临时数据,fs 模块都是第一入口。Node.js 提供了三套编程接口:同步 API回调式异步 API 以及 Promise 异步 API。实际开发中,强烈推荐优先使用 fs/promises,避免陷入回调嵌套,也更契合 async/await 的代码风格。

文件读写:从简单读取到流式处理

最简单的文件读写方式是使用 fs.promises.readFilefs.promises.writeFile,它会将整个文件内容读入内存。对于配置、JSON 数据等小文件来说,这完全足够且非常便利:

const fs = require('fs/promises');
const path = require('path');

// 读取文本文件
const content = await fs.readFile(path.join(__dirname, 'config.json'), 'utf-8');
const config = JSON.parse(content);

// 写入文件(会覆盖原有内容)
await fs.writeFile(
  path.join(__dirname, 'output.txt'),
  'Hello Node.js',
  'utf-8'
);

// 追加内容到文件末尾
await fs.appendFile(
  path.join(__dirname, 'app.log'),
  `${new Date().toISOString()} - 服务启动\n`
);

当需要精细控制时,也可以使用文件描述符操作,例如在同一个文件上多次写入而不重复打开关闭:

const fs = require('fs/promises');

const fd = await fs.open('log.txt', 'a');
await fd.appendFile('第一行\n');
await fd.appendFile('第二行\n');
await fd.close();

重要提醒:对于动辄上百 MB 甚至 GB 的大文件,readFile 会一次性将全部数据载入内存,极易导致内存溢出。这时必须使用流(Stream) 式处理,它是 Node.js 处理大文件的惯用手段,我们将在第 7 章和第 8 章中详细展开。这里只给出一个用流复制文件的基础示例:

const fs = require('fs');

const readStream = fs.createReadStream('source.mp4');
const writeStream = fs.createWriteStream('dest.mp4');

readStream.pipe(writeStream);

writeStream.on('finish', () => {
  console.log('复制完成');
});

目录操作:创建、列举与删除

目录操作同样提供了同步、回调与 Promise 三种风格,推荐统一使用 fs/promises

const fs = require('fs/promises');

// 创建目录,recursive: true 会自动创建不存在的父级目录
await fs.mkdir('uploads/2024/avatars', { recursive: true });

// 列举目录内容
const files = await fs.readdir('uploads', { withFileTypes: true });
for (const dirent of files) {
  if (dirent.isDirectory()) {
    console.log('目录:', dirent.name);
  } else if (dirent.isFile()) {
    console.log('文件:', dirent.name);
  }
}

// 删除目录(recursive: true 会递归删除内部所有文件和子目录)
await fs.rm('temp-cache', { recursive: true, force: true });
// 或者使用 fs.rmdir,但 rm 更为统一

路径拼接务必使用 path.join(),它能自动处理不同操作系统下的路径分隔符(/\),避免硬编码导致的跨平台问题。

文件监听:捕捉变化的两种方式

Node.js 原生提供了两种文件监听机制:

  1. fs.watch:利用操作系统底层事件(如 inotify),性能较好,但不同平台行为有差异,有时会重复触发,且文件名不一定能准确捕获。
  2. fs.watchFile:通过轮询文件状态(mtime、size),比较稳定但效率较低,不适合高频变动的文件。
// 使用 fs.watch(推荐首选)
const fs = require('fs');
const watcher = fs.watch('uploads', (eventType, filename) => {
  console.log(`事件类型: ${eventType}`);
  if (filename) {
    console.log(`变化的文件: ${filename}`);
  }
});

// 关闭监听器
// watcher.close();

// 使用 fs.watchFile(轮询方式,不推荐)
fs.watchFile('config.json', { interval: 500 }, (curr, prev) => {
  console.log('文件被修改,新大小:', curr.size);
});

真实项目中的选择:原生 API 存在较多兼容性和细节问题(如 macOS 下的递归监听受限),实际开发中绝大多数项目会直接使用社区久经考验的库 chokidar。它提供了统一且丰富的 API,能正确处理各种边界情况:

npm install chokidar
const chokidar = require('chokidar');

const watcher = chokidar.watch('src', {
  ignored: /(^|[\/\\])\../, // 忽略隐藏文件和目录
  persistent: true
});

watcher
  .on('add', path => console.log(`文件 ${path} 已添加`))
  .on('change', path => console.log(`文件 ${path} 已更改`))
  .on('unlink', path => console.log(`文件 ${path} 已删除`));

在 Webpack、Vite 等工具的底层,你都能看到 chokidar 的身影。就可靠性而言,它几乎已成为 Node.js 文件监听的“事实标准”。

权限管理:检查、修改与安全原则

文件权限在服务端安全中至关重要,尤其是在处理上传文件或公开目录时。Node.js 通过 fs.accessfs.chmodfs.chown 提供了完整的权限控制能力。

const fs = require('fs/promises');

// 检查当前进程是否有读取权限
try {
  await fs.access('secret.key', fs.constants.R_OK);
  console.log('有读权限');
} catch {
  console.log('无读权限');
}

// 修改文件或目录的权限模式(使用数字掩码)
// 0o600: 只有所有者可读写
// 0o644: 所有者可读写,同组和其他用户只读
// 0o755: 所有者可读写执行,同组和其他用户可读执行
await fs.chmod('script.sh', 0o755);

// 修改文件所有者(需要足够权限,通常 root 下可用)
// await fs.chown('file.txt', uid, gid);

权限数字快速参考

  • 0o400:所有者只读
  • 0o600:所有者读写
  • 0o644:所有者读写,其他人只读
  • 0o755:所有者读写执行,其他人读执行
  • 0o777:所有人读写执行(极其危险,避免使用)

fs.access 除了读取权限外,还可以检查 R_OK(读)、W_OK(写)、X_OK(执行)和 F_OK(文件是否存在)。但它在分布式环境中存在时间窗口竞争问题,通常建议直接尝试操作并捕获异常,而不是先检查再操作。

最佳实践

  • 上传目录务必设置为 0o750,禁止其他用户访问。
  • 配置文件(如数据库密码)应设为 0o600,仅运行进程的用户可读。
  • 避免以 root 用户运行 Node.js 进程,应创建低权限用户(如 node),并确保应用目录归属于该用户。
  • 使用 Docker 等容器环境时,需同步配好容器内用户的 UID/GID,否则可能出现权限不足或文件泄露风险。

通过合理运用这些文件系统 API,你能在保证安全的前提下,灵活地管理服务器上的各类数据。后续章节会进一步讨论大文件流式处理、临时文件清理以及在生产环境中的权限兜底策略。