Node.js: 文件系统基础
最后更新:2026-08-26
Charlie 是一名后端工程师,负责维护公司日志分析平台。每天凌晨,系统需要处理约 50,000 行的服务器日志文件。最初他用 fs.readFileSync 逐个读取日志,结果整个程序在读取期间完全卡住,其他请求全部超时。改用 fs.readFile 异步读取后,程序在等待磁盘 I/O 的间隙仍能响应其他请求,整体吞吐量提升了 10 倍。这次经历让他深刻理解了 Node.js 文件系统 API 的同步与异步差异。
1. 你将学到
- 使用
fs.readFile/fs.writeFile/fs.appendFile/fs.unlink进行文件操作 - 使用
fs.mkdir/fs.readdir/fs.stat/fs.existsSync进行目录操作 - 区分同步方法与异步方法的执行差异
- 理解错误优先回调
(err, data)模式 - 使用
fs.promisesAPI 以 Promise 方式操作文件 - 选择合适的文件编码(utf8 / base64 / binary)
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 通知结果。
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 遵循"错误优先回调"约定:回调函数的第一个参数始终是错误对象,若无错误则为 null。fs.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,若目录已存在也不会报错。📖 小节
- 你将学到的核心概念与使用方法
- fs 模块概览的核心概念与使用方法
- 同步 vs 异步执行时序的核心概念与使用方法
- 文件读写操作的核心概念与使用方法
- 目录操作的核心概念与使用方法
- 错误优先回调与 Promise 对比的核心概念与使用方法
- 文件编码的核心概念与使用方法
- 综合示例:文件管理工具的核心概念与使用方法
📝 作业
- 完成本课所有代码示例,确保每个示例都能正确运行
- 修改综合示例,添加自己的扩展功能
- 查阅官方文档,找出本课未涉及的1-2个API并编写测试代码
- 思考:在实际项目中,你会如何应用本课学到的知识?
- 尝试将本课知识与前面课程的内容结合,构建一个小项目