在 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
)。
思路:
获取当前日期,生成文件名;
用异步追加写入(
fs.writeFile
+flag: 'a'
);封装成日志函数,方便重复调用。
代码:
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
里的配置,修改后再写回去。
思路:
同步读取 JSON 文件(小文件,同步更简单);
解析成 JS 对象,修改属性;
把对象转成字符串,同步写入(覆盖原文件)。
代码:
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
事件,把这些细节吃透,才能在实际开发中灵活处理各种文件操作需求~
如果看完还没完全吃透,建议找个小项目练手:比如写个批量修改文件夹下所有文本文件内容的脚本,或者做个简易的“本地笔记应用”(读取、写入、追加笔记),用代码验证这些知识点,实践后理解会更深刻~
网友评论文明上网理性发言 已有0人参与
发表评论: