说说对Node中的fs模块的理解?有哪些常用方法
问题解析
fs模块是Node.js与文件系统交互的核心模块。面试官通过此题考察对文件操作API的熟悉程度,以及对同步/异步操作、文件描述符、权限等概念的理解。在实际开发中,文件操作是常见需求,正确使用fs模块至关重要。
核心概念
fs模块概述
fs(File System)模块提供了与文件系统交互的API,可以执行文件的读取、写入、删除、权限修改等操作。Node.js的fs模块设计遵循POSIX标准,同时针对JavaScript的异步特性做了优化。
操作方式分类
┌─────────────────────────────────────────────────────┐
│ fs 操作方式 │
├──────────────────┬──────────────────────────────────┤
│ 异步方式 │ 同步方式 │
├──────────────────┼──────────────────────────────────┤
│ 回调风格 │ 阻塞执行,完成后返回 │
│ readFile(path, cb)│ readFileSync(path) │
├──────────────────┼──────────────────────────────────┤
│ Promise风格 │ 适用于初始化配置读取 │
│ fs.promises │ 不适用于并发请求处理 │
│ .readFile(path) │ │
├──────────────────┼──────────────────────────────────┤
│ 流式操作 │ │
│ createReadStream │ │
└──────────────────┴──────────────────────────────────┘
核心概念
1. 文件权限位(mode)
// Unix风格权限表示
// 权限分为三组:所有者(user)、组(group)、其他(other)
// 每组权限:读(r=4)、写(w=2)、执行(x=1)
const fs = require('fs');
// 创建文件,权限设置为 rw-r--r-- (644)
// 6 = 4+2 (所有者读写)
// 4 = 4 (组只读)
// 4 = 4 (其他只读)
fs.writeFileSync('file.txt', 'content', { mode: 0o644 });
// 常用权限组合
const permissions = {
'rw-------': 0o600, // 仅所有者可读写
'rw-r--r--': 0o644, // 所有者可读写,其他只读
'rwxr-xr-x': 0o755, // 所有者可执行,其他可读执行
'rwxrwxrwx': 0o777 // 所有人可读写执行
};
2. 文件标识位(flag)
// flag控制文件的打开方式
const flags = {
'r': '只读,文件必须存在',
'r+': '读写,文件必须存在',
'w': '只写,文件不存在则创建,存在则清空',
'w+': '读写,文件不存在则创建,存在则清空',
'a': '追加写入,文件不存在则创建',
'a+': '追加读写,文件不存在则创建',
'wx': '只写,文件必须不存在(原子创建)',
'ax': '追加,文件必须不存在'
};
// 示例
fs.writeFile('file.txt', 'data', { flag: 'wx' }, (err) => {
if (err && err.code === 'EEXIST') {
console.log('文件已存在,不覆盖');
}
});
3. 文件描述符(fd)
// 文件描述符是操作系统分配给打开文件的非负整数
// 0 = stdin, 1 = stdout, 2 = stderr
const fs = require('fs');
// 打开文件获取描述符
fs.open('file.txt', 'r', (err, fd) => {
if (err) throw err;
// 使用描述符读取
const buffer = Buffer.alloc(1024);
fs.read(fd, buffer, 0, 1024, 0, (err, bytesRead) => {
if (err) throw err;
console.log(buffer.toString('utf8', 0, bytesRead));
// 必须关闭描述符
fs.close(fd, (err) => {
if (err) throw err;
});
});
});
详细解答
常用方法详解
1. 读取文件
const fs = require('fs');
const fsPromises = require('fs').promises;
// ========== 异步读取(回调风格)==========
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error('读取失败:', err);
return;
}
console.log('文件内容:', data);
});
// ========== 异步读取(Promise风格)==========
async function readFileAsync() {
try {
const data = await fsPromises.readFile('file.txt', 'utf8');
console.log('文件内容:', data);
} catch (err) {
console.error('读取失败:', err);
}
}
// ========== 同步读取 ==========
try {
const data = fs.readFileSync('file.txt', 'utf8');
console.log('文件内容:', data);
} catch (err) {
console.error('读取失败:', err);
}
// ========== 流式读取(大文件)==========
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('读取完成');
});
2. 写入文件
const fs = require('fs');
// ========== 覆盖写入 ==========
fs.writeFile('file.txt', 'Hello World', 'utf8', (err) => {
if (err) throw err;
console.log('写入成功');
});
// 带选项的写入
fs.writeFile('file.txt', 'content', {
encoding: 'utf8',
mode: 0o644,
flag: 'w'
}, callback);
// ========== 追加写入 ==========
fs.appendFile('log.txt', `${new Date().toISOString()} - 日志\n`, (err) => {
if (err) throw err;
});
// ========== 同步写入 ==========
fs.writeFileSync('file.txt', 'content');
// ========== 流式写入 ==========
const writeStream = fs.createWriteStream('output.txt');
writeStream.write('第一行\n');
writeStream.write('第二行\n');
writeStream.end('结束');
3. 文件复制
const fs = require('fs');
// ========== 简单复制 ==========
fs.copyFile('source.txt', 'dest.txt', (err) => {
if (err) throw err;
console.log('复制成功');
});
// 复制模式
// fs.constants.COPYFILE_EXCL: 目标存在则失败
// fs.constants.COPYFILE_FICLONE: 尝试创建写时复制链接
// fs.constants.COPYFILE_FICLONE_FORCE: 强制创建写时复制链接
fs.copyFile('source.txt', 'dest.txt', fs.constants.COPYFILE_EXCL, callback);
// ========== 大文件复制(流式)==========
function copyLargeFile(source, dest) {
return new Promise((resolve, reject) => {
const readStream = fs.createReadStream(source);
const writeStream = fs.createWriteStream(dest);
readStream.on('error', reject);
writeStream.on('error', reject);
writeStream.on('finish', resolve);
readStream.pipe(writeStream);
});
}
4. 目录操作
const fs = require('fs');
const path = require('path');
// ========== 创建目录 ==========
// 创建单层目录
fs.mkdir('newdir', (err) => {
if (err && err.code !== 'EEXIST') throw err;
});
// 递归创建多级目录
fs.mkdir('parent/child/grandchild', { recursive: true }, (err) => {
if (err) throw err;
});
// ========== 读取目录 ==========
fs.readdir('directory', { withFileTypes: true }, (err, entries) => {
if (err) throw err;
entries.forEach((entry) => {
if (entry.isDirectory()) {
console.log(`[目录] ${entry.name}`);
} else if (entry.isFile()) {
console.log(`[文件] ${entry.name}`);
}
});
});
// ========== 删除目录 ==========
// 删除空目录
fs.rmdir('empty-dir', (err) => {
if (err) throw err;
});
// 递归删除目录及其内容(Node 14+)
fs.rm('directory', { recursive: true, force: true }, (err) => {
if (err) throw err;
});
5. 文件信息
const fs = require('fs');
// ========== 获取文件状态 ==========
fs.stat('file.txt', (err, stats) => {
if (err) throw err;
console.log({
是文件: stats.isFile(),
是目录: stats.isDirectory(),
是符号链接: stats.isSymbolicLink(),
大小: stats.size, // 字节
创建时间: stats.birthtime,
修改时间: stats.mtime,
访问时间: stats.atime,
权限模式: stats.mode
});
});
// ========== 检查文件可访问性 ==========
fs.access('file.txt', fs.constants.R_OK | fs.constants.W_OK, (err) => {
if (err) {
console.log('文件不存在或无权限');
} else {
console.log('文件可读可写');
}
});
// ========== 监视文件变化 ==========
const watcher = fs.watch('file.txt', (eventType, filename) => {
console.log(`事件类型: ${eventType}`);
if (filename) {
console.log(`文件名: ${filename}`);
}
});
// 停止监视
watcher.close();
6. 文件操作(增删改)
const fs = require('fs');
// ========== 重命名/移动 ==========
fs.rename('old-name.txt', 'new-name.txt', (err) => {
if (err) throw err;
});
// 跨文件系统移动(复制+删除)
fs.rename('source.txt', '/different-fs/dest.txt', (err) => {
if (err && err.code === 'EXDEV') {
// 跨文件系统,需要手动复制删除
copyAndDelete('source.txt', '/different-fs/dest.txt');
}
});
// ========== 删除文件 ==========
fs.unlink('file.txt', (err) => {
if (err) throw err;
});
// ========== 截断文件 ==========
fs.truncate('file.txt', 100, (err) => {
if (err) throw err;
// 文件被截断为100字节
});
// ========== 修改权限 ==========
fs.chmod('file.txt', 0o755, (err) => {
if (err) throw err;
});
// ========== 修改所有者 ==========
fs.chown('file.txt', uid, gid, (err) => {
if (err) throw err;
});
深入理解
同步 vs 异步的选择
const fs = require('fs');
// ========== 何时使用同步方法 ==========
// 1. 程序启动时的配置文件读取
const config = JSON.parse(fs.readFileSync('./config.json', 'utf8'));
// 2. CLI工具的初始化
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
// 3. 测试代码中的准备/清理工作
beforeEach(() => {
fs.mkdirSync('./test-temp', { recursive: true });
});
afterEach(() => {
fs.rmSync('./test-temp', { recursive: true, force: true });
});
// ========== 何时使用异步方法 ==========
// 1. Web服务器的请求处理
const http = require('http');
http.createServer((req, res) => {
// 正确:使用异步
fs.readFile('file.txt', (err, data) => {
res.end(data);
});
// 错误:使用同步会阻塞其他请求
// const data = fs.readFileSync('file.txt');
// res.end(data);
});
// 2. 大量文件操作
async function processFiles(files) {
// 使用Promise.all并发处理
const results = await Promise.all(
files.map(file => fs.promises.readFile(file))
);
return results;
}
文件描述符的高级使用
const fs = require('fs');
// 使用文件描述符进行细粒度控制
class FileHandler {
constructor(filename) {
this.filename = filename;
this.fd = null;
}
open(flags = 'r', mode = 0o666) {
return new Promise((resolve, reject) => {
fs.open(this.filename, flags, mode, (err, fd) => {
if (err) reject(err);
else {
this.fd = fd;
resolve(fd);
}
});
});
}
read(buffer, offset, length, position) {
return new Promise((resolve, reject) => {
fs.read(this.fd, buffer, offset, length, position, (err, bytesRead) => {
if (err) reject(err);
else resolve(bytesRead);
});
});
}
write(buffer, offset, length, position) {
return new Promise((resolve, reject) => {
fs.write(this.fd, buffer, offset, length, position, (err, bytesWritten) => {
if (err) reject(err);
else resolve(bytesWritten);
});
});
}
close() {
return new Promise((resolve, reject) => {
fs.close(this.fd, (err) => {
if (err) reject(err);
else {
this.fd = null;
resolve();
}
});
});
}
}
// 使用示例:分块读取大文件
async function readInChunks(filename, chunkSize = 1024) {
const handler = new FileHandler(filename);
await handler.open('r');
const buffer = Buffer.alloc(chunkSize);
let position = 0;
let bytesRead;
do {
bytesRead = await handler.read(buffer, 0, chunkSize, position);
if (bytesRead > 0) {
console.log('读取块:', buffer.toString('utf8', 0, bytesRead));
position += bytesRead;
}
} while (bytesRead === chunkSize);
await handler.close();
}
文件系统的高级模式
const fs = require('fs');
const path = require('path');
// ========== 递归遍历目录 ==========
async function* walkDir(dir) {
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
yield* walkDir(fullPath); // 递归
} else {
yield fullPath;
}
}
}
// 使用
(async () => {
for await (const file of walkDir('./src')) {
console.log(file);
}
})();
// ========== 原子文件写入 ==========
// 先写入临时文件,再重命名,保证原子性
async function atomicWrite(filePath, data) {
const tempPath = `${filePath}.tmp`;
await fs.promises.writeFile(tempPath, data);
await fs.promises.rename(tempPath, filePath);
}
// ========== 文件锁(简单实现)==========
class FileLock {
constructor(lockFile) {
this.lockFile = lockFile;
}
async acquire() {
try {
// O_EXCL 保证原子创建
await fs.promises.open(this.lockFile, 'wx');
return true;
} catch (err) {
if (err.code === 'EEXIST') {
return false; // 锁已被占用
}
throw err;
}
}
async release() {
await fs.promises.unlink(this.lockFile);
}
}
最佳实践
1. 错误处理
const fs = require('fs');
// 区分不同类型的错误
fs.readFile('config.json', (err, data) => {
if (err) {
switch (err.code) {
case 'ENOENT':
console.error('文件不存在');
break;
case 'EACCES':
console.error('权限不足');
break;
case 'EISDIR':
console.error('路径是目录而非文件');
break;
default:
console.error('读取失败:', err);
}
return;
}
// 处理数据
});
2. 资源管理
const fs = require('fs');
// 使用try-finally确保资源释放
function withFile(filename, flags, callback) {
let fd;
try {
fd = fs.openSync(filename, flags);
return callback(fd);
} finally {
if (fd !== undefined) {
fs.closeSync(fd);
}
}
}
// 现代方式:使用Promise和finally
async function processFile(filename) {
let fd;
try {
fd = await fs.promises.open(filename, 'r');
const content = await fd.readFile('utf8');
return content;
} finally {
await fd?.close();
}
}
3. 路径处理
const fs = require('fs');
const path = require('path');
// 始终使用path模块处理路径
// 错误:字符串拼接
const wrongPath = __dirname + '/files/' + filename;
// 正确:使用path.join
const correctPath = path.join(__dirname, 'files', filename);
// 处理路径遍历攻击
function safeJoin(base, target) {
const targetPath = path.join(base, target);
if (!targetPath.startsWith(base)) {
throw new Error('路径遍历攻击检测');
}
return targetPath;
}
4. 大文件处理
const fs = require('fs');
const crypto = require('crypto');
// 计算大文件hash(流式处理)
function hashFile(filename) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256');
const stream = fs.createReadStream(filename);
stream.on('error', reject);
stream.on('data', (chunk) => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex')));
});
}
// 大文件复制带进度
function copyWithProgress(source, dest) {
return new Promise((resolve, reject) => {
const stats = fs.statSync(source);
const totalSize = stats.size;
let copiedSize = 0;
const readStream = fs.createReadStream(source);
const writeStream = fs.createWriteStream(dest);
readStream.on('data', (chunk) => {
copiedSize += chunk.length;
const progress = (copiedSize / totalSize * 100).toFixed(2);
process.stdout.write(`\r进度: ${progress}%`);
});
readStream.on('error', reject);
writeStream.on('error', reject);
writeStream.on('finish', () => {
console.log('\n复制完成');
resolve();
});
readStream.pipe(writeStream);
});
}
面试要点
- 操作方式:清楚同步和异步的区别,知道何时使用哪种
- 核心概念:理解mode、flag、fd的含义和用法
- 性能考虑:大文件使用流式处理,避免阻塞事件循环
- 错误处理:能区分ENOENT、EACCES等常见错误码
- 安全实践:防范路径遍历攻击,正确处理用户输入的路径
常见追问
-
Q: 同步方法和异步方法有什么区别?
- A: 同步方法阻塞事件循环直到完成,适用于启动配置;异步方法不阻塞,适用于运行时请求处理
-
Q: 如何安全地处理用户上传的文件路径?
- A: 使用path.join和路径验证,防止目录遍历攻击
-
Q: 读取10GB的文件应该用什么方法?
- A: 使用createReadStream流式读取,避免一次性载入内存