3、核心模块 - fs 文件系统:读取文件(同步/异步)、写入文件、文件流(stream)、监听文件变化、实战:日志文件生成器
好,咱们今天来啃 Node.js 里最常用的一个核心模块——fs(文件系统)。
说实话,后端开发里,文件操作几乎是躲不开的。无论是读取配置文件、写入日志、处理上传的图片,还是做数据持久化,都得跟文件打交道。Node.js 的 fs 模块提供了非常丰富的 API,但用不好也容易踩坑。
我个人习惯把文件操作分成两类:一次性操作和流式操作。咱们一个一个来看。
3.1 读取文件:同步 vs 异步
读取文件,最基础的操作。Node.js 提供了两种方式:同步和异步。
3.1.1 异步读取(推荐)
异步是 Node.js 的基因。用 fs.readFile,不会阻塞事件循环。
const fs = require('fs');
// 异步读取
fs.readFile('./data.txt', 'utf8', (err, data) => {
if (err) {
console.error('读取失败:', err);
return;
}
console.log('文件内容:', data);
});
console.log('这行会先执行,因为 readFile 是异步的');
嗯,这里要注意:回调函数的第一个参数永远是错误对象。这是 Node.js 的约定俗成,我建议你养成习惯,每次写回调都先处理 err。
3.1.2 同步读取(慎用)
用 fs.readFileSync,代码看起来更「线性」,但会阻塞线程。
try {
const data = fs.readFileSync('./data.txt', 'utf8');
console.log('文件内容:', data);
} catch (err) {
console.error('读取失败:', err);
}
console.log('这行会等上面执行完才执行');
什么时候用同步? 我个人只在程序启动时读取配置文件时用同步。因为那时候还没开始处理请求,阻塞一下无所谓。其他时候,一律用异步。
3.2 写入文件
写入文件同样有同步和异步之分。核心 API 是 fs.writeFile 和 fs.writeFileSync。
3.2.1 异步写入
const content = 'Hello, Node.js!';
fs.writeFile('./output.txt', content, 'utf8', (err) => {
if (err) {
console.error('写入失败:', err);
return;
}
console.log('文件写入成功');
});
默认情况下,writeFile 会覆盖文件内容。如果你想追加内容,用 fs.appendFile。
fs.appendFile('./log.txt', '新的一行日志\n', (err) => {
if (err) console.error('追加失败:', err);
});
3.2.2 写入选项
writeFile 的第三个参数可以传一个 options 对象,常用的有:
| 选项 | 说明 | 默认值 |
|---|---|---|
encoding |
编码格式 | 'utf8' |
mode |
文件权限 | 0o666 |
flag |
文件系统标志 | 'w'(写入) |
flag 的值很有意思:
'w'- 写入,覆盖已有内容'a'- 追加,不覆盖'wx'- 写入,但文件已存在时报错
flag: 'a' 配合 appendFile,这样每次写入都是追加,不会丢失历史数据。
3.3 文件流(Stream)
好,接下来是重点——文件流。为什么需要流?
你想想看,如果用 readFile 读取一个 2GB 的文件,内存直接爆了。流式处理的核心思想是:分块读取,边读边处理。
3.3.1 创建可读流
const readStream = fs.createReadStream('./bigfile.txt', {
encoding: 'utf8',
highWaterMark: 64 * 1024 // 每次读取 64KB
});
readStream.on('data', (chunk) => {
console.log('收到一块数据,大小:', chunk.length);
});
readStream.on('end', () => {
console.log('文件读取完毕');
});
readStream.on('error', (err) => {
console.error('读取出错:', err);
});
highWaterMark 这个参数很关键。它控制每次读取的「块」大小。默认是 64KB,你可以根据场景调整。我处理大日志文件时,一般设成 1MB,平衡内存和 I/O 次数。
3.3.2 可写流
const writeStream = fs.createWriteStream('./output.txt');
writeStream.write('第一行\n');
writeStream.write('第二行\n');
writeStream.end('结束\n');
writeStream.on('finish', () => {
console.log('写入完成');
});
3.3.3 pipe:最优雅的流式传输
pipe 方法可以把可读流直接「管道」到可写流。说白了,就是自动读取、自动写入,还带背压控制。
const readStream = fs.createReadStream('./source.txt');
const writeStream = fs.createWriteStream('./dest.txt');
readStream.pipe(writeStream);
readStream.on('end', () => {
console.log('文件复制完成');
});
一行 pipe,搞定文件复制。我记得第一次用 pipe 时,感觉这 API 设计得太优雅了。
3.4 监听文件变化
有时候我们需要监控文件的变化,比如配置文件改了自动重载、日志文件新增了实时读取。Node.js 提供了 fs.watch 和 fs.watchFile。
3.4.1 fs.watch(推荐)
fs.watch('./config.json', (eventType, filename) => {
console.log(`文件 ${filename} 发生了 ${eventType} 事件`);
// eventType 可能是 'rename' 或 'change'
});
这个 API 底层依赖操作系统的文件通知机制,效率很高。但有个坑:在 macOS 上,rename 事件可能触发多次。我曾经被这个坑过,后来加了防抖处理。
3.4.2 fs.watchFile(轮询)
fs.watchFile('./config.json', {
interval: 1000 // 每秒检查一次
}, (curr, prev) => {
console.log('文件修改时间:', curr.mtime);
});
watchFile 是轮询方式,不推荐用于生产环境。除非你监控的是网络文件系统(NFS),那种场景下 watch 可能不工作。
3.5 实战:日志文件生成器
理论讲完了,咱们来写个实战项目。需求很简单:
- 每秒生成一条日志,格式为
[时间戳] [级别] 消息 - 日志写入到
app.log文件 - 当日志文件超过 1MB 时,自动归档(重命名为
app-日期.log),然后新建文件继续写
const fs = require('fs');
const path = require('path');
const LOG_FILE = './app.log';
const MAX_SIZE = 1 * 1024 * 1024; // 1MB
function getTimestamp() {
return new Date().toISOString();
}
function archiveLog() {
const dateStr = new Date().toISOString().slice(0, 10);
const archiveName = `app-${dateStr}.log`;
fs.renameSync(LOG_FILE, archiveName);
console.log(`日志已归档: ${archiveName}`);
}
function writeLog(level, message) {
const logLine = `[${getTimestamp()}] [${level}] ${message}\n`;
// 检查文件大小
try {
const stats = fs.statSync(LOG_FILE);
if (stats.size >= MAX_SIZE) {
archiveLog();
}
} catch (err) {
// 文件不存在,忽略错误
}
// 追加写入
fs.appendFile(LOG_FILE, logLine, (err) => {
if (err) console.error('写入日志失败:', err);
});
}
// 模拟日志生成
setInterval(() => {
const levels = ['INFO', 'WARN', 'ERROR'];
const level = levels[Math.floor(Math.random() * levels.length)];
const messages = [
'用户登录成功',
'数据库查询耗时 150ms',
'内存使用率 78%',
'请求超时,重试中',
'缓存命中率 92%'
];
const message = messages[Math.floor(Math.random() * messages.length)];
writeLog(level, message);
}, 1000);
console.log('日志生成器已启动,按 Ctrl+C 停止');
这个生成器用到了:
fs.appendFile异步追加写入fs.statSync同步检查文件大小(这里用同步没问题,因为只在写入前检查一次)fs.renameSync重命名文件实现归档
好了,fs 模块的核心内容就这些。总结一下:
- 小文件用 readFile/writeFile,大文件用流
- 生产环境避免同步操作
- 文件监控注意跨平台兼容性
- 日志系统要考虑归档和轮转
下一章,咱们聊聊网络编程,看看 Node.js 怎么处理 TCP 和 HTTP 请求。
公众号:蓝海资料掘金营,微信deep3321