Node.js: 文件系统基础

最后更新:2026-08-26

Charlie 是一名后端工程师,负责维护公司日志分析平台。每天凌晨,系统需要处理约 50,000 行的服务器日志文件。最初他用 fs.readFileSync 逐个读取日志,结果整个程序在读取期间完全卡住,其他请求全部超时。改用 fs.readFile 异步读取后,程序在等待磁盘 I/O 的间隙仍能响应其他请求,整体吞吐量提升了 10 倍。这次经历让他深刻理解了 Node.js 文件系统 API 的同步与异步差异。

1. 你将学到



2. fs 模块概览

fs 模块是 Node.js 内置的文件系统操作模块,提供文件读写、目录管理、权限检查等能力。每种操作通常提供三种风格:同步、异步回调、异步 Promise。

特性 同步方法 异步回调方法 fs.promises 方法
命名特征 xxxSync 后缀 无后缀 fs.promises.xxx
返回值 直接返回结果 undefined,通过回调获取 返回 Promise
阻塞事件循环
错误处理 try/catch 回调第一个参数 .catch() / try-catch
推荐场景 启动时加载配置 兼容旧代码 新项目首选
JAVASCRIPT
const fs = require('fs');

// 同步
const data = fs.readFileSync('config.json', 'utf8');

// 异步回调
fs.readFile('config.json', 'utf8', (err, data) => {
  if (err) throw err;
  console.log(data);
});

// Promise
const fsPromises = require('fs/promises');
fsPromises.readFile('config.json', 'utf8')
  .then(data => console.log(data))
  .catch(err => console.error(err));


3. 同步 vs 异步执行时序

同步方法会阻塞事件循环,直到文件操作完成才继续执行后续代码。异步方法则立即返回,文件操作完成后通过回调或 Promise 通知结果。

100%
sequenceDiagram
    participant Main as 主线程
    participant FS_Sync as 同步读取
    participant FS_Async as 异步读取
    participant Disk as 磁盘 I/O

    Note over Main,Disk: 同步执行流程
    Main->>FS_Sync: readFileSync('log.txt')
    FS_Sync->>Disk: 读取文件(阻塞等待)
    Disk-->>FS_Sync: 返回数据
    FS_Sync-->>Main: 继续执行后续代码
    Note right of Main: 其他请求全部等待

    Note over Main,Disk: 异步执行流程
    Main->>FS_Async: readFile('log.txt', callback)
    FS_Async->>Disk: 提交读取请求
    FS_Async-->>Main: 立即返回,继续执行
    Note right of Main: 可处理其他请求
    Disk-->>FS_Async: I/O 完成
    FS_Async-->>Main: 执行回调函数

▶ 示例:同步读取阻塞整个程序

JAVASCRIPT
const fs = require('fs');

console.log('开始读取...');
const data = fs.readFileSync('big-log.txt', 'utf8');
console.log('读取完成,行数:', data.split('\n').length);
console.log('这行必须等读取完才执行');
▶ 试一试
TEXT 📖 仅展示
开始读取...
读取完成,行数:50000
这行必须等读取完才执行

▶ 示例:异步读取不阻塞事件循环

JAVASCRIPT
const fs = require('fs');

console.log('开始读取...');
fs.readFile('big-log.txt', 'utf8', (err, data) => {
  if (err) throw err;
  console.log('读取完成,行数:', data.split('\n').length);
});
console.log('这行立即执行,无需等待读取');
▶ 试一试
TEXT 📖 仅展示
开始读取...
这行立即执行,无需等待读取
读取完成,行数:50000


4. 文件读写操作

方法 参数 用途
fs.readFile(path, encoding, callback) 路径, 编码, 回调 异步读取整个文件
fs.readFileSync(path, encoding) 路径, 编码 同步读取整个文件
fs.writeFile(path, data, encoding, callback) 路径, 数据, 编码, 回调 异步写入(覆盖)
fs.writeFileSync(path, data, encoding) 路径, 数据, 编码 同步写入(覆盖)
fs.appendFile(path, data, encoding, callback) 路径, 数据, 编码, 回调 异步追加内容
fs.appendFileSync(path, data, encoding) 路径, 数据, 编码 同步追加内容
fs.unlink(path, callback) 路径, 回调 异步删除文件
fs.unlinkSync(path) 路径 同步删除文件

▶ 示例:写入与追加文件

JAVASCRIPT
const fs = require('fs');

fs.writeFile('output.txt', '第一行内容\n', 'utf8', (err) => {
  if (err) throw err;
  console.log('写入完成');

  fs.appendFile('output.txt', '追加的第二行\n', 'utf8', (err) => {
    if (err) throw err;
    console.log('追加完成');

    fs.readFile('output.txt', 'utf8', (err, data) => {
      if (err) throw err;
      console.log('文件内容:\n', data);
    });
  });
});
▶ 试一试
TEXT 📖 仅展示
写入完成
追加完成
文件内容:
 第一行内容
追加的第二行

▶ 示例:使用 fs.promises 避免回调嵌套

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

async function writeAndRead() {
  try {
    await fs.writeFile('output.txt', '第一行内容\n', 'utf8');
    console.log('写入完成');
    await fs.appendFile('output.txt', '追加的第二行\n', 'utf8');
    console.log('追加完成');
    const data = await fs.readFile('output.txt', 'utf8');
    console.log('文件内容:\n', data);
  } catch (err) {
    console.error('操作失败:', err.message);
  }
}

writeAndRead();
▶ 试一试

▶ 示例:删除文件

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

async function deleteFile() {
  try {
    await fs.unlink('output.txt');
    console.log('文件已删除');
  } catch (err) {
    console.error('删除失败:', err.message);
  }
}

deleteFile();
▶ 试一试

5. 目录操作

方法 参数 用途
fs.mkdir(path, options, callback) 路径, {recursive}, 回调 创建目录
fs.readdir(path, options, callback) 路径, {withFileTypes}, 回调 列出目录内容
fs.stat(path, callback) 路径, 回调 获取文件/目录信息
fs.existsSync(path) 路径 同步判断路径是否存在

▶ 示例:创建目录与列出内容

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

async function dirOperations() {
  try {
    await fs.mkdir('logs', { recursive: true });
    console.log('目录创建成功');

    await fs.writeFile('logs/app.log', '2025-01-01 Server started\n', 'utf8');
    await fs.writeFile('logs/error.log', '2025-01-01 Connection timeout\n', 'utf8');

    const files = await fs.readdir('logs');
    console.log('目录内容:', files);
  } catch (err) {
    console.error('操作失败:', err.message);
  }
}

dirOperations();
▶ 试一试
TEXT 📖 仅展示
目录创建成功
目录内容: [ 'app.log', 'error.log' ]

▶ 示例:判断文件还是目录

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

async function checkType() {
  const stats = await fs.stat('logs');
  console.log('logs 是目录:', stats.isDirectory());
  console.log('logs 是文件:', stats.isFile());

  const fileStats = await fs.stat('logs/app.log');
  console.log('app.log 是文件:', fileStats.isFile());
  console.log('文件大小:', fileStats.size, 'bytes');
}

checkType();
▶ 试一试
TEXT 📖 仅展示
logs 是目录: true
logs 是文件: false
app.log 是文件: true
文件大小: 29 bytes


6. 错误优先回调与 Promise 对比

Node.js 文件系统 API 遵循"错误优先回调"约定:回调函数的第一个参数始终是错误对象,若无错误则为 nullfs.promises 则用标准 Promise 机制处理错误。

对比项 错误优先回调 fs.promises
函数签名 (err, data) => {} 返回 Promise<data>
错误判断 if (err) 检查 try/catch.catch()
嵌套问题 容易产生回调地狱 async/await 扁平化
典型场景 旧项目兼容 新项目推荐
引入方式 require('fs') require('fs/promises')

▶ 示例:两种错误处理方式对比

JAVASCRIPT
const fsCallback = require('fs');
const fsPromise = require('fs/promises');

// 错误优先回调
fsCallback.readFile('not-exist.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('回调方式 - 错误:', err.code);
    return;
  }
  console.log(data);
});

// Promise 方式
async function readWithPromise() {
  try {
    const data = await fsPromise.readFile('not-exist.txt', 'utf8');
    console.log(data);
  } catch (err) {
    console.error('Promise方式 - 错误:', err.code);
  }
}

readWithPromise();
▶ 试一试
TEXT 📖 仅展示
回调方式 - 错误: ENOENT
Promise方式 - 错误: ENOENT


7. 文件编码

编码 说明 适用场景 示例
'utf8' UTF-8 文本编码(默认) 文本文件读写 日志、配置、JSON
'base64' Base64 编码 图片传输、二进制转文本 图片内嵌、邮件附件
'binary''latin1' 原始字节 二进制文件低层操作 图片、压缩包处理
null 返回 Buffer 对象 需要操作原始字节 文件校验、流处理

▶ 示例:不同编码读取同一文件

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

async function readEncodings() {
  await fs.writeFile('sample.txt', 'Hello 世界', 'utf8');

  const utf8Data = await fs.readFile('sample.txt', 'utf8');
  console.log('UTF-8:', utf8Data);

  const base64Data = await fs.readFile('sample.txt', 'base64');
  console.log('Base64:', base64Data);

  const bufferData = await fs.readFile('sample.txt');
  console.log('Buffer:', bufferData);
  console.log('Buffer 十六进制:', bufferData.toString('hex'));
}

readEncodings();
▶ 试一试
TEXT 📖 仅展示
UTF-8: Hello 世界
Base64: SGVsbG8g5LiW55WM
Buffer: <Buffer 48 65 6c 6c 6f 20 e4 b8 96 e7 95 8c>
Buffer 十六进制: 48656c6c6f20e4b896e7958c


8. 综合示例:文件管理工具

构建一个完整的文件管理流程:创建目录 → 写入配置 → 读取解析 → 追加日志 → 列出内容。

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

async function fileManager() {
  const dir = 'project-data';
  const configPath = path.join(dir, 'config.json');
  const logPath = path.join(dir, 'app.log');

  try {
    // Step 1: 创建目录
    await fs.mkdir(dir, { recursive: true });
    console.log('✓ 目录已创建:', dir);

    // Step 2: 写入配置文件
    const config = {
      appName: 'LogAnalyzer',
      version: '1.0.0',
      maxLines: 50000,
      encoding: 'utf8'
    };
    await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
    console.log('✓ 配置文件已写入:', configPath);

    // Step 3: 读取并解析配置
    const raw = await fs.readFile(configPath, 'utf8');
    const parsed = JSON.parse(raw);
    console.log('✓ 配置已加载:', parsed.appName, 'v' + parsed.version);

    // Step 4: 追加日志
    const timestamp = new Date().toISOString();
    await fs.appendFile(logPath, `[${timestamp}] Service started\n`, 'utf8');
    await fs.appendFile(logPath, `[${timestamp}] Config loaded: ${parsed.maxLines} lines\n`, 'utf8');
    console.log('✓ 日志已追加:', logPath);

    // Step 5: 列出目录内容
    const entries = await fs.readdir(dir, { withFileTypes: true });
    console.log('✓ 目录内容:');
    for (const entry of entries) {
      const type = entry.isDirectory() ? '[DIR]' : '[FILE]';
      const stats = await fs.stat(path.join(dir, entry.name));
      console.log(`  ${type} ${entry.name} (${stats.size} bytes)`);
    }
  } catch (err) {
    console.error('✗ 操作失败:', err.message);
  }
}

fileManager();
TEXT 📖 仅展示
✓ 目录已创建: project-data
✓ 配置文件已写入: project-data/config.json
✓ 配置已加载: LogAnalyzer v1.0.0
✓ 日志已追加: project-data/app.log
✓ 目录内容:
  [FILE] app.log (106 bytes)
  [FILE] config.json (98 bytes)

❓ 常见问题

Q 什么时候应该使用同步方法?
A 仅在应用启动阶段加载配置文件等一次性操作中使用,运行时绝不使用同步方法,否则会阻塞事件循环。
Q 为什么回调的第一个参数是 err?
A 这是 Node.js 错误优先回调(Error-first Callback)约定,强制开发者先检查错误再处理数据,避免忽略异常。
Q fs.promises 和 fs 有什么区别?
A fs.promises(或 require('fs/promises'))提供相同功能但返回 Promise,可用 async/await 替代回调嵌套;fs 使用回调风格,两者功能完全一致。
Q 如何判断路径是文件还是目录?
A 使用 fs.stat(path) 获取 stats 对象,调用 stats.isFile() 判断是否为文件,stats.isDirectory() 判断是否为目录。
Q readFile 会把整个文件加载到内存吗?
A 是的,readFile 将文件全部读入内存。处理大文件应使用 fs.createReadStream 流式读取,避免内存溢出。
Q fs.mkdir 的 recursive 选项有什么用?
A 设置 { recursive: true } 可以一次性创建多级嵌套目录,类似 mkdir -p,若目录已存在也不会报错。

📖 小节


📝 作业

  1. 完成本课所有代码示例,确保每个示例都能正确运行
  2. 修改综合示例,添加自己的扩展功能
  3. 查阅官方文档,找出本课未涉及的1-2个API并编写测试代码
  4. 思考:在实际项目中,你会如何应用本课学到的知识?
  5. 尝试将本课知识与前面课程的内容结合,构建一个小项目
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏