×

先搞懂同步与异步,Node.js 文件操作的两种逻辑

作者:Terry2025.07.01来源:Web前端之家浏览:46评论:0
关键词:js文件操作

在 Node.js 开发里,文件读写是绕不开的基础操作——不管是做后端服务处理配置文件,还是写脚本批量处理数据,甚至做工具类项目生成日志,都得和文件读写打交道,可 Node.js 到底怎么实现文件的读取和写入?不同场景下选哪种方式更高效?今天就从基础逻辑到实战案例,把这些问题拆明白。

Node.js 里操作文件主要靠内置的 fs 模块(File System 的缩写),而文件操作分**同步**和**异步**两种逻辑,理解它们的区别是选对方法的前提。

先说同步操作,比如读取文件用 fs.readFileSync(),写入用 fs.writeFileSync(),这类方法的特点是“阻塞式执行”——代码运行到这里会停下来,等文件操作完成后再继续往下走,好处是逻辑简单,适合写脚本、处理小文件(比如配置文件),不用操心异步的回调或 Promise;但缺点也很明显,如果文件很大或者操作很耗时,整个程序会被“卡主”,没法处理其他请求,在服务端项目里容易拖慢性能。

再看异步操作,对应的方法是 fs.readFile()fs.writeFile() 这类不带 Sync 后缀的,它们的执行逻辑是“非阻塞”:发起文件操作后,Node.js 会继续处理其他任务,等文件操作完成后,再通过回调函数或者 Promise 通知结果,这种方式特别适合服务端项目(Express 写的接口服务),能同时处理多个请求,避免因为文件操作拖慢整体响应速度,不过异步代码的逻辑嵌套(比如回调地狱)或者 Promise 链式调用,对新手来说需要适应。

读取文件:三种常用方式的区别与场景

知道了同步异步的逻辑,具体到“读取文件”,Node.js 提供了不同的实现方式,各自对应不同的场景。

同步读取:简单直接,适合小文件

如果是写个自动化脚本,或者处理几十 KB 以内的配置文件,同步读取足够简单,代码逻辑像这样:

const fs = require('fs');
try {
  // 读取文件内容,第二个参数指定编码(比如utf8),否则返回Buffer
  const content = fs.readFileSync('./test.txt', 'utf8');
  console.log('文件内容:', content);
} catch (err) {
  console.error('读取失败:', err);
}

这里要注意错误处理——同步方法执行出错会抛出异常,所以要用 try...catch 包裹,这种方式的优势是代码流程线性,新手容易理解;但如果文件太大(比如几百 MB),同步读取会一次性把内容加载到内存,可能导致内存溢出,这时候就得换其他方式。

异步读取(回调版):非阻塞,服务端常用

在 Node.js 早期,异步操作主要靠回调函数实现,读取文件的代码大概长这样:

const fs = require('fs');
fs.readFile('./test.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('读取失败:', err);
    return;
  }
  console.log('文件内容:', data);
});
// 注意:这里的代码会在readFile发起后立即执行,不用等文件读取完成
console.log('发起读取请求后,继续做其他事...');

回调函数里第一个参数是错误对象 err(成功时为 null),第二个是文件内容 data,这种方式的好处是不阻塞后续代码,但如果多个异步操作嵌套(比如读文件后要写文件,写文件后还要读另一个),就会出现“回调地狱”(一层套一层的回调),代码可读性变差。

异步读取(Promise 版 + 流式读取):解决回调痛点,还能处理大文件

为了让异步代码更优雅,Node.js 提供了 util.promisify 工具,能把回调风格的方法转成 Promise 风格,对于大文件(比如几个 GB 的日志),流式读取(Stream)更高效——它会把文件分成小块,逐段读取到内存,避免一次性加载超大内容导致内存爆炸。

先看 Promise 版的改造:

const fs = require('fs');
const util = require('util');
// 把readFile转成返回Promise的函数
const readFilePromise = util.promisify(fs.readFile);
readFilePromise('./test.txt', 'utf8')
  .then(content => {
    console.log('文件内容:', content);
  })
  .catch(err => {
    console.error('读取失败:', err);
  });

这种方式用 then/catch 处理异步结果,比回调更清爽。

再看流式读取的场景(比如处理 1GB 以上的大文件):

const fs = require('fs');
// 创建可读流
const readStream = fs.createReadStream('./bigFile.txt', {
  encoding: 'utf8', // 指定编码,否则流里是Buffer
  highWaterMark: 1024 * 1024, // 每次读取的字节数(1MB,可自定义)
});
readStream.on('data', (chunk) => {
  console.log('读取到一块内容:', chunk.length);
  // 这里可以处理每一块内容,比如逐行解析日志
});
readStream.on('end', () => {
  console.log('文件读取完毕');
});
readStream.on('error', (err) => {
  console.error('读取出错:', err);
});

流式读取的核心是“事件驱动”:当流里有数据时触发 data 事件,读完触发 end 事件,出错触发 error 事件,这种方式内存占用极低,哪怕文件几个 GB,也能稳定处理。

写入文件:覆盖、追加与流式写入的技巧

读取是“输入”,写入就是“输出”,Node.js 写入文件同样分同步、异步、流式三种思路,而且要注意覆盖追加的区别。

同步写入:快速生成小文件

和同步读取逻辑类似,用 fs.writeFileSync() 可以同步写入内容,比如生成一个简单的日志文件:

const fs = require('fs');
try {
  // 第二个参数是要写入的内容,第三个参数可选编码
  fs.writeFileSync('./log.txt', '这是一条日志\n', 'utf8');
  console.log('写入成功');
} catch (err) {
  console.error('写入失败:', err);
}

注意:writeFileSync 默认是覆盖写入——如果文件已经存在,旧内容会被清空,换成新内容,如果想“追加内容”而不是覆盖,得用异步方法配合 flag 参数(后面讲)。

异步写入(覆盖与追加):灵活控制文件内容

异步写入用 fs.writeFile(),默认也是覆盖,如果要追加内容,需要传 flag: 'a'(append 的缩写),代码示例:

覆盖写入

const fs = require('fs');
fs.writeFile('./log.txt', '覆盖式写入的新内容\n', 'utf8', (err) => {
  if (err) {
    console.error('写入失败:', err);
    return;
  }
  console.log('覆盖写入成功');
});

追加写入(比如往日志里加新内容):

fs.writeFile('./log.txt', '这是追加的内容\n', {
  encoding: 'utf8',
  flag: 'a' // a表示追加(append)
}, (err) => {
  if (err) {
    console.error('追加失败:', err);
    return;
  }
  console.log('追加成功');
});

除了 flag: 'a',还有其他常用 flag,w+(读写,不存在则创建)、r+(读写,文件必须存在)等,具体可参考 Node.js 官方文档中 fs.open() 的说明(不同 flag 对应不同的文件操作权限)。

流式写入:大文件分批写,避免内存爆炸

和流式读取对应,流式写入用 fs.createWriteStream(),适合处理大文件写入(比如把几个 GB 的数据分块写入),比如把读取的大文件内容,通过流的方式写入另一个文件(实现文件复制):

const fs = require('fs');
// 创建可读流(源文件)和可写流(目标文件)
const readStream = fs.createReadStream('./bigFile.txt');
const writeStream = fs.createWriteStream('./copyBigFile.txt');
// 管道:把可读流的数据直接“导”到可写流
readStream.pipe(writeStream);
// 监听事件
readStream.on('end', () => {
  console.log('源文件读取完毕,写入完成');
});
writeStream.on('error', (err) => {
  console.error('写入出错:', err);
});

pipe() 方法是流的“语法糖”,它自动帮我们处理了数据的流动:当可读流有数据时,自动写到可写流里,不用手动监听 data 事件再调用 write(),这种方式在处理大文件时,内存占用始终很低,性能比一次性读取再写入好太多。

实战:用 Node.js 文件操作解决真实场景问题

光看方法不够,得结合实际场景练手,下面举三个常见案例,看看文件读写怎么落地。

案例 1:搭建简易日志系统(追加写入 + 按日期拆分)

需求:每次程序运行时,把日志追加到当天的日志文件里(2024-09-12.log)。

思路:

  1. 获取当前日期,生成文件名;

  2. 用异步追加写入(fs.writeFile + flag: 'a');

  3. 封装成日志函数,方便重复调用。

代码:

const fs = require('fs');
const path = require('path');
function log(message) {
  // 获取当前日期,格式:YYYY-MM-DD
  const today = new Date().toISOString().slice(0, 10);
  const logFilePath = path.join(__dirname, `${today}.log`);
  const logContent = `[${new Date().toLocaleTimeString()}] ${message}\n`;
  fs.writeFile(logFilePath, logContent, {
    encoding: 'utf8',
    flag: 'a' // 追加模式
  }, (err) => {
    if (err) {
      console.error('日志写入失败:', err);
    }
  });
}
// 测试调用
log('用户登录成功');
log('订单创建完成');

这样每次调用 log(),日志会自动追加到当天的文件里,方便后续分析。

案例 2:配置文件读写(JSON 格式)

需求:读取 config.json 里的配置,修改后再写回去。

思路:

  1. 同步读取 JSON 文件(小文件,同步更简单);

  2. 解析成 JS 对象,修改属性;

  3. 把对象转成字符串,同步写入(覆盖原文件)。

代码:

const fs = require('fs');
// 读取配置
function readConfig() {
  try {
    const content = fs.readFileSync('./config.json', 'utf8');
    return JSON.parse(content);
  } catch (err) {
    console.error('读取配置失败:', err);
    return {}; // 返回空对象兜底
  }
}
// 写入配置
function writeConfig(config) {
  try {
    const content = JSON.stringify(config, null, 2); // 格式化缩进2个空格
    fs.writeFileSync('./config.json', content, 'utf8');
    console.log('配置写入成功');
  } catch (err) {
    console.error('写入配置失败:', err);
  }
}
// 测试:读取 -> 修改 -> 写入
const config = readConfig();
config.theme = 'dark'; // 修改主题为深色
config.language = 'zh-CN'; // 修改语言为中文
writeConfig(config);

这种方式适合处理项目的配置文件,比如前端构建工具的 vue.config.js 换成 JSON 格式的话,就可以用类似逻辑读写。

案例 3:大文件复制工具(流式操作)

需求:把一个几个 GB 的视频文件,复制到另一个目录,要求高效且内存占用低。

思路:用可读流 + 可写流 + pipe 管道,实现边读边写。

代码:

const fs = require('fs');
const path = require('path');
function copyLargeFile(sourcePath, targetPath) {
  const readStream = fs.createReadStream(sourcePath);
  const writeStream = fs.createWriteStream(targetPath);
  // 监听进度(可选,给用户反馈)
  let copiedSize = 0;
  readStream.on('data', (chunk) => {
    copiedSize += chunk.length;
    console.log(`已复制 ${(copiedSize / 1024 / 1024).toFixed(2)} MB`);
  });
  // 管道连接
  readStream.pipe(writeStream);
  // 完成回调
  writeStream.on('finish', () => {
    console.log('大文件复制完成!');
  });
  // 错误处理
  readStream.on('error', (err) => {
    console.error('读取源文件出错:', err);
    writeStream.end(); // 终止写入
  });
  writeStream.on('error', (err) => {
    console.error('写入目标文件出错:', err);
    readStream.destroy(); // 销毁可读流
  });
}
// 测试:复制大视频文件
const source = path.join(__dirname, 'bigVideo.mp4');
const target = path.join(__dirname, 'copyBigVideo.mp4');
copyLargeFile(source, target);

这种流式复制的方式,哪怕文件有 10GB,内存占用也只会维持在几百 KB 左右(取决于 highWaterMark 的设置),比同步读取再写入(一次性加载 10GB 到内存)要安全得多。

选对方法,效率翻倍

Node.js 的文件读写看似简单,实际要根据文件大小项目场景(脚本还是服务端)、性能要求来选方法:

  • 小文件、脚本工具 → 优先同步操作(代码简单,逻辑清晰);

  • 服务端项目、高并发场景 → 优先异步操作(回调/Promise/Async/Await),避免阻塞事件循环;

  • 大文件(几百 MB 以上) → 必须用流式操作(可读流/可写流),防止内存溢出。

不管哪种方式,错误处理都很重要——同步用 try...catch,异步用回调的 err 参数、Promise 的 catch,流用 error 事件,把这些细节吃透,才能在实际开发中灵活处理各种文件操作需求~

如果看完还没完全吃透,建议找个小项目练手:比如写个批量修改文件夹下所有文本文件内容的脚本,或者做个简易的“本地笔记应用”(读取、写入、追加笔记),用代码验证这些知识点,实践后理解会更深刻~

您的支持是我们创作的动力!
温馨提示:本文作者系Terry ,经Web前端之家编辑修改或补充,转载请注明出处和本文链接:
https://jiangweishan.com/article/node.js.html

网友评论文明上网理性发言 已有0人参与

发表评论: