跳到主要内容

文件系统

Node.js 的 fs 模块提供了文件系统操作能力,包括文件读写、目录管理、文件监听等功能。

fs 模块概述

fs 模块同时提供同步和异步两种 API:

导入方式

// CommonJS
const fs = require('node:fs');
const { readFile, writeFile } = require('node:fs');
const fsPromises = require('node:fs/promises');

// ES Modules
import fs from 'node:fs';
import { readFile, writeFile } from 'node:fs';
import fsPromises from 'node:fs/promises';

文件读取

异步读取

const fs = require('node:fs');

// 回调方式
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.error('读取失败:', err);
return;
}
console.log('文件内容:', data);
});

// Promise 方式
const fsPromises = require('node:fs/promises');

async function readFile() {
try {
const data = await fsPromises.readFile('example.txt', 'utf8');
console.log('文件内容:', data);
} catch (err) {
console.error('读取失败:', err);
}
}

readFile();

同步读取

const fs = require('node:fs');

try {
const data = fs.readFileSync('example.txt', 'utf8');
console.log('文件内容:', data);
} catch (err) {
console.error('读取失败:', err);
}

读取选项

// 读取为 Buffer(默认)
const buffer = fs.readFileSync('image.png');
console.log(buffer); // <Buffer 89 50 4e 47 ...>

// 指定编码读取
const text = fs.readFileSync('example.txt', 'utf8');

// 使用选项对象
const data = fs.readFileSync('example.txt', {
encoding: 'utf8',
flag: 'r' // 读取模式
});

文件写入

异步写入

const fsPromises = require('node:fs/promises');

async function writeFile() {
try {
await fsPromises.writeFile('output.txt', 'Hello, Node.js!', 'utf8');
console.log('写入成功');
} catch (err) {
console.error('写入失败:', err);
}
}

writeFile();

写入模式

const fs = require('node:fs');

// 覆盖写入(默认)
fs.writeFileSync('output.txt', '第一行');

// 追加写入
fs.writeFileSync('output.txt', '\n第二行', { flag: 'a' });

// 或使用 appendFile
fs.appendFileSync('output.txt', '\n第三行');

写入选项

fs.writeFileSync('output.txt', 'content', {
encoding: 'utf8',
mode: 0o644, // 文件权限
flag: 'w', // 写入模式
});

// 常用 flag:
// 'r' - 读取(文件必须存在)
// 'r+' - 读写(文件必须存在)
// 'w' - 写入(覆盖或创建)
// 'w+' - 读写(覆盖或创建)
// 'a' - 追加(文件不存在则创建)
// 'a+' - 读取追加(文件不存在则创建)
// 'x' - 排他创建(文件已存在则失败)

文件操作

复制文件

const fs = require('node:fs');

// 同步复制
fs.copyFileSync('source.txt', 'destination.txt');

// 异步复制
fs.copyFile('source.txt', 'destination.txt', (err) => {
if (err) throw err;
console.log('复制成功');
});

// Promise 方式
await fs.promises.copyFile('source.txt', 'destination.txt');

移动/重命名文件

const fs = require('node:fs');

// 重命名
fs.renameSync('old-name.txt', 'new-name.txt');

// 移动文件
fs.renameSync('file.txt', 'new-dir/file.txt');

// 异步方式
await fs.promises.rename('old.txt', 'new.txt');

删除文件

const fs = require('node:fs');

// 删除文件
fs.unlinkSync('file-to-delete.txt');

// 异步删除
await fs.promises.unlink('file-to-delete.txt');

// Node.js 14.14+ 使用 rm
await fs.promises.rm('file.txt');
await fs.promises.rm('directory', { recursive: true, force: true });

检查文件是否存在

const fs = require('node:fs');

// 已弃用的方式(不推荐)
fs.existsSync('file.txt');

// 推荐方式:检查可访问性
fs.accessSync('file.txt', fs.constants.R_OK);

// Promise 方式
try {
await fs.promises.access('file.txt', fs.constants.R_OK);
console.log('文件存在且可读');
} catch {
console.log('文件不存在或不可读');
}

// 检查文件状态
const stats = await fs.promises.stat('file.txt');
console.log('是文件:', stats.isFile());
console.log('是目录:', stats.isDirectory());

目录操作

创建目录

const fs = require('node:fs');

// 创建单层目录
fs.mkdirSync('new-dir');

// 递归创建目录
fs.mkdirSync('deep/nested/dir', { recursive: true });

// 异步方式
await fs.promises.mkdir('new-dir', { recursive: true });

读取目录

const fs = require('node:fs');

// 读取目录内容
const files = fs.readdirSync('my-directory');
console.log(files); // ['file1.txt', 'file2.txt', 'subdir']

// 包含文件类型
const entries = fs.readdirSync('my-directory', { withFileTypes: true });
entries.forEach(entry => {
console.log(`${entry.name} - ${entry.isFile() ? '文件' : '目录'}`);
});

// 异步方式
const files = await fs.promises.readdir('my-directory');

删除目录

const fs = require('node:fs');

// 删除空目录
fs.rmdirSync('empty-dir');

// 递归删除(Node.js 14.14+)
fs.rmSync('directory', { recursive: true, force: true });

// 异步方式
await fs.promises.rm('directory', { recursive: true, force: true });

文件信息

获取文件状态

const fs = require('node:fs');

const stats = fs.statSync('example.txt');

console.log('文件大小:', stats.size, '字节');
console.log('是文件:', stats.isFile());
console.log('是目录:', stats.isDirectory());
console.log('创建时间:', stats.birthtime);
console.log('修改时间:', stats.mtime);
console.log('访问时间:', stats.atime);
console.log('权限:', stats.mode.toString(8));

// 异步方式
const stats = await fs.promises.stat('example.txt');

Stats 对象属性

属性说明
size文件大小(字节)
isFile()是否为文件
isDirectory()是否为目录
isSymbolicLink()是否为符号链接
birthtime创建时间
mtime内容修改时间
atime访问时间
ctime状态修改时间
mode文件权限
uid所有者用户 ID
gid所有者组 ID

流式操作

对于大文件,使用流可以避免内存问题。

读取流

const fs = require('node:fs');

// 创建读取流
const readStream = fs.createReadStream('large-file.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);
});

写入流

const fs = require('node:fs');

// 创建写入流
const writeStream = fs.createWriteStream('output.txt');

// 写入数据
writeStream.write('第一行\n');
writeStream.write('第二行\n');

// 结束写入
writeStream.end('最后一行');

// 监听完成事件
writeStream.on('finish', () => {
console.log('写入完成');
});

管道操作

const fs = require('node:fs');
const zlib = require('node:zlib');

// 复制大文件
fs.createReadStream('source.txt')
.pipe(fs.createWriteStream('destination.txt'))
.on('finish', () => {
console.log('复制完成');
});

// 压缩文件
fs.createReadStream('source.txt')
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('source.txt.gz'))
.on('finish', () => {
console.log('压缩完成');
});

文件监听

watchFile

监听文件变化:

const fs = require('node:fs');

// 监听文件变化
fs.watchFile('example.txt', (curr, prev) => {
console.log('文件发生变化');
console.log('当前修改时间:', curr.mtime);
console.log('之前修改时间:', prev.mtime);
});

// 设置监听间隔
fs.watchFile('example.txt', { interval: 1000 }, (curr, prev) => {
console.log('文件被修改');
});

// 取消监听
fs.unwatchFile('example.txt');

watch

更高效的监听方式:

const fs = require('node:fs');

// 监听目录或文件
const watcher = fs.watch('./', (eventType, filename) => {
console.log(`事件类型: ${eventType}`);
console.log(`文件名: ${filename}`);
});

// 关闭监听
watcher.close();

// 使用异步迭代器(Node.js 18+)
async function watchDirectory() {
for await (const event of fs.promises.watch('./')) {
console.log(event);
}
}

路径处理

使用 path 模块处理路径:

const path = require('node:path');

// 路径拼接
const fullPath = path.join('/home', 'user', 'documents', 'file.txt');
console.log(fullPath); // '/home/user/documents/file.txt'

// 获取文件名
console.log(path.basename('/home/user/file.txt')); // 'file.txt'
console.log(path.basename('/home/user/file.txt', '.txt')); // 'file'

// 获取目录名
console.log(path.dirname('/home/user/file.txt')); // '/home/user'

// 获取扩展名
console.log(path.extname('file.txt')); // '.txt'
console.log(path.extname('file.min.js')); // '.js'

// 解析路径
console.log(path.parse('/home/user/file.txt'));
// {
// root: '/',
// dir: '/home/user',
// base: 'file.txt',
// ext: '.txt',
// name: 'file'
// }

// 格式化路径
console.log(path.format({
dir: '/home/user',
base: 'file.txt'
})); // '/home/user/file.txt'

// 绝对路径
console.log(path.resolve('file.txt'));
console.log(path.resolve('/home', 'user', 'file.txt')); // '/home/user/file.txt'

// 相对路径
console.log(path.relative('/home/user', '/home/guest')); // '../guest'

// 规范化路径
console.log(path.normalize('/home/../user/./documents')); // '/user/documents'

实用示例

递归遍历目录

const fs = require('node:fs/promises');
const path = require('node:path');

async function walkDirectory(dir) {
const files = [];
const entries = await fs.readdir(dir, { withFileTypes: true });

for (const entry of entries) {
const fullPath = path.join(dir, entry.name);

if (entry.isDirectory()) {
const subFiles = await walkDirectory(fullPath);
files.push(...subFiles);
} else {
files.push(fullPath);
}
}

return files;
}

// 使用
walkDirectory('./src').then(files => {
console.log('所有文件:', files);
});

复制目录

const fs = require('node:fs/promises');
const path = require('node:path');

async function copyDirectory(src, dest) {
await fs.mkdir(dest, { recursive: true });
const entries = await fs.readdir(src, { withFileTypes: true });

for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);

if (entry.isDirectory()) {
await copyDirectory(srcPath, destPath);
} else {
await fs.copyFile(srcPath, destPath);
}
}
}

// 使用
copyDirectory('./src', './backup');

按行读取文件

const fs = require('node:fs');
const readline = require('node:readline');

async function readLines(filePath) {
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});

for await (const line of rl) {
console.log(line);
}
}

readLines('data.txt');

小结

本章我们学习了:

  1. 文件读写:同步和异步方式、编码处理
  2. 文件操作:复制、移动、删除、检查存在
  3. 目录操作:创建、读取、删除
  4. 文件信息:获取文件状态和属性
  5. 流式操作:处理大文件
  6. 文件监听:监听文件和目录变化
  7. 路径处理:path 模块的使用

练习

  1. 实现一个递归复制目录的函数
  2. 使用流处理大文件,统计文件行数
  3. 创建一个文件监听器,当文件变化时自动重启服务
  4. 实现一个简单的文件搜索功能,在目录中搜索包含特定文本的文件