3、Node.js核心模块之fs:文件系统操作全攻略

说实话,fs模块是我在Node.js项目里打交道最多的模块之一。你想想看,只要是后端应用,基本都离不开文件读写——日志记录、配置文件加载、文件上传处理,哪样都绕不开它。今天我就带你把这个模块彻底搞明白。

3.1 fs模块概述

fs,全称File System,是Node.js内置的核心模块。它提供了操作文件的全部能力,包括读写、删除、重命名、权限修改等等。使用时只需要一行代码:

const fs = require('fs');

嗯,这里要注意,fs模块的所有方法都有两种形式:同步和异步。我个人习惯在服务器启动时用同步,运行时用异步。为什么?后面细说。

核心要点:fs模块的异步方法性能更好,不会阻塞事件循环;同步方法代码更直观,适合初始化场景。

3.2 读取文件(同步/异步)

读取文件是最基础的操作。我刚开始学Node.js时,就踩过同步读取的坑——文件大了直接卡住整个进程。

异步读取

const fs = require('fs');

// 异步读取
fs.readFile('./hello.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('读取失败:', err);
    return;
  }
  console.log('文件内容:', data);
});

console.log('这行会先执行');

异步读取不会阻塞后续代码。回调函数里的err参数一定要处理,我曾经因为漏掉这个判断,导致线上服务崩溃——文件不存在时直接报错退出。

同步读取

// 同步读取
try {
  const data = fs.readFileSync('./hello.txt', 'utf8');
  console.log('文件内容:', data);
} catch (err) {
  console.error('读取失败:', err);
}

console.log('这行会后执行');

同步方法用try/catch包裹是标准做法。我一般在项目启动时读取配置文件用同步,因为那时候还没开始处理请求,阻塞一下没关系。

我的建议:日常开发优先用异步方法。同步方法只在初始化、脚本工具等场景使用。

3.3 写入文件

写入文件有两种模式:覆盖写入和追加写入。先看覆盖写入:

// 异步写入(覆盖)
fs.writeFile('./note.txt', 'Hello Node.js', 'utf8', (err) => {
  if (err) {
    console.error('写入失败:', err);
    return;
  }
  console.log('写入成功');
});

// 同步写入(覆盖)
fs.writeFileSync('./note.txt', 'Hello Node.js', 'utf8');

writeFile默认会覆盖原有内容。如果你不想丢失旧数据,那就用追加模式。

注意:writeFile如果文件不存在,会自动创建。但如果目录不存在,会报错。我曾经因为这个bug排查了半天——目录路径写错了。

3.4 追加文件

追加文件用appendFile,说白了就是在文件末尾加内容:

// 异步追加
fs.appendFile('./log.txt', '新的一行日志\n', 'utf8', (err) => {
  if (err) {
    console.error('追加失败:', err);
    return;
  }
  console.log('追加成功');
});

// 同步追加
fs.appendFileSync('./log.txt', '新的一行日志\n', 'utf8');

这个在写日志文件时特别有用。我做过一个日志系统,每秒要追加几百条记录,用appendFile完全没问题。

3.5 删除文件

删除文件用unlink。名字有点怪,但记住就行:

// 异步删除
fs.unlink('./temp.txt', (err) => {
  if (err) {
    console.error('删除失败:', err);
    return;
  }
  console.log('删除成功');
});

// 同步删除
fs.unlinkSync('./temp.txt');

删除不存在的文件会报错。所以删除前最好先检查文件是否存在:

if (fs.existsSync('./temp.txt')) {
  fs.unlinkSync('./temp.txt');
}

3.6 创建目录

创建目录用mkdir。这里有个坑——默认只能创建一级目录:

// 创建单级目录
fs.mkdir('./data', (err) => {
  if (err) throw err;
  console.log('目录创建成功');
});

// 创建多级目录(递归)
fs.mkdir('./data/logs/2024', { recursive: true }, (err) => {
  if (err) throw err;
  console.log('多级目录创建成功');
});

recursive: true这个参数很重要。我早期写代码时不知道这个参数,只能一层一层手动创建,代码又臭又长。

3.7 读取目录

读取目录用readdir,返回目录下的所有文件和子目录:

// 异步读取目录
fs.readdir('./data', (err, files) => {
  if (err) {
    console.error('读取目录失败:', err);
    return;
  }
  console.log('目录内容:', files);
});

// 同步读取目录
const files = fs.readdirSync('./data');
console.log('目录内容:', files);

返回的是文件名数组。如果你需要区分文件和目录,可以结合stat方法:

const files = fs.readdirSync('./data');
files.forEach(file => {
  const stat = fs.statSync(`./data/${file}`);
  if (stat.isFile()) {
    console.log(`${file} 是文件`);
  } else if (stat.isDirectory()) {
    console.log(`${file} 是目录`);
  }
});

3.8 文件状态信息

stat方法能获取文件的详细信息,比如大小、修改时间、权限等:

fs.stat('./hello.txt', (err, stats) => {
  if (err) {
    console.error('获取状态失败:', err);
    return;
  }
  console.log('文件大小:', stats.size, '字节');
  console.log('是否是文件:', stats.isFile());
  console.log('是否是目录:', stats.isDirectory());
  console.log('创建时间:', stats.birthtime);
  console.log('修改时间:', stats.mtime);
});

常用的属性我整理了一下:

属性/方法 说明 类型
size 文件大小(字节) number
isFile() 判断是否为文件 boolean
isDirectory() 判断是否为目录 boolean
birthtime 文件创建时间 Date
mtime 文件最后修改时间 Date
mode 文件权限(八进制) number
实用技巧:我经常用stat来检查文件是否被修改过。比如监控配置文件变化,一旦mtime变了就重新加载配置。

3.9 综合示例:文件管理小工具

最后,我写一个综合示例,把今天学的知识点串起来。这个工具可以统计目录下所有文件的总大小:

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

function getDirSize(dirPath) {
  let totalSize = 0;
  
  try {
    const files = fs.readdirSync(dirPath);
    
    files.forEach(file => {
      const fullPath = path.join(dirPath, file);
      const stats = fs.statSync(fullPath);
      
      if (stats.isFile()) {
        totalSize += stats.size;
        console.log(`文件: ${file} - ${stats.size} 字节`);
      } else if (stats.isDirectory()) {
        console.log(`目录: ${file}`);
        totalSize += getDirSize(fullPath); // 递归
      }
    });
  } catch (err) {
    console.error('读取目录失败:', err);
  }
  
  return totalSize;
}

const size = getDirSize('./data');
console.log(`总大小: ${size} 字节`);

这个例子用到了readdir、stat、递归,还有path模块拼接路径。你想想看,是不是把今天学的都用上了?

避坑指南:我曾经在递归遍历目录时忘记处理符号链接,结果遇到循环链接导致栈溢出。建议生产环境用第三方库如fs-extra,它处理了各种边界情况。

好了,fs模块的核心操作就这些。记住一个原则:生产环境优先用异步,脚本工具可以用同步。下一章我们聊聊流(Stream),那才是处理大文件的利器。