3、Node.js核心模块之fs:文件系统操作全攻略
说实话,fs模块是我在Node.js项目里打交道最多的模块之一。你想想看,只要是后端应用,基本都离不开文件读写——日志记录、配置文件加载、文件上传处理,哪样都绕不开它。今天我就带你把这个模块彻底搞明白。
3.1 fs模块概述
fs,全称File System,是Node.js内置的核心模块。它提供了操作文件的全部能力,包括读写、删除、重命名、权限修改等等。使用时只需要一行代码:
const fs = require('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默认会覆盖原有内容。如果你不想丢失旧数据,那就用追加模式。
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 |
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),那才是处理大文件的利器。