Appearance
Node.js 文件系统操作面试题
1. fs 模块的基本使用
问题:Node.js 的 fs 模块有哪些使用方式?
答案: fs 模块提供三种使用方式:
1. 同步方式(阻塞):
javascript
const fs = require('fs');
// 同步读取
try {
const data = fs.readFileSync('file.txt', 'utf8');
console.log(data);
} catch (err) {
console.error('读取失败:', err);
}
// 同步写入
fs.writeFileSync('file.txt', 'Hello World', 'utf8');
// 同步检查文件存在
if (fs.existsSync('file.txt')) {
console.log('文件存在');
}2. 回调方式(异步):
javascript
const fs = require('fs');
// 异步读取
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error('读取失败:', err);
return;
}
console.log(data);
});
// 异步写入
fs.writeFile('file.txt', 'Hello World', 'utf8', (err) => {
if (err) {
console.error('写入失败:', err);
return;
}
console.log('写入成功');
});3. Promise 方式(异步):
javascript
const fs = require('fs').promises;
// Promise 方式
async function fileOperations() {
try {
// 读取
const data = await fs.readFile('file.txt', 'utf8');
console.log(data);
// 写入
await fs.writeFile('file.txt', 'Hello World', 'utf8');
// 追加
await fs.appendFile('file.txt', '\nNew line', 'utf8');
// 删除
await fs.unlink('file.txt');
} catch (err) {
console.error('操作失败:', err);
}
}三种方式对比:
| 方式 | 优点 | 缺点 | 使用场景 |
|---|---|---|---|
| 同步 | 代码简单,顺序执行 | 阻塞事件循环 | 配置文件读取、启动时初始化 |
| 回调 | 非阻塞 | 回调地狱 | 简单异步操作 |
| Promise | 非阻塞,可读性好 | 需要 async/await | 复杂异步操作,现代代码 |
2. 文件读写操作
问题:Node.js 如何进行文件的读写操作?
答案:
读取文件:
javascript
const fs = require('fs').promises;
// 1. 完整读取(小文件)
const data = await fs.readFile('file.txt', 'utf8');
// 2. 读取为 Buffer(二进制文件)
const buffer = await fs.readFile('image.png');
// 3. 流式读取(大文件)
const stream = fs.createReadStream('large-file.txt', {
encoding: 'utf8',
highWaterMark: 64 * 1024 // 64KB 缓冲区
});
stream.on('data', chunk => {
console.log('收到数据块:', chunk.length);
});
stream.on('end', () => {
console.log('读取完成');
});
// 4. 指定位置读取
const fd = await fs.open('file.txt', 'r');
const buffer = Buffer.alloc(1024);
const { bytesRead } = await fd.read(buffer, 0, 1024, 100); // 从第 100 字节开始读取
await fd.close();写入文件:
javascript
const fs = require('fs').promises;
// 1. 完整写入(覆盖)
await fs.writeFile('file.txt', 'Hello World', 'utf8');
// 2. 追加写入
await fs.appendFile('file.txt', '\nNew line', 'utf8');
// 3. 流式写入(大文件)
const stream = fs.createWriteStream('large-file.txt');
stream.write('Line 1\n');
stream.write('Line 2\n');
stream.end('Line 3\n');
// 4. 指定位置写入
const fd = await fs.open('file.txt', 'w');
await fd.write('Hello', 0, 'utf8'); // 从开头写入
await fd.write('World', 5, 'utf8'); // 从第 5 字节写入
await fd.close();
// 5. 写入 Buffer
const buffer = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
await fs.writeFile('binary.bin', buffer);写入模式:
javascript
const fs = require('fs');
// r - 读取
// r+ - 读写
// w - 写入(创建或截断)
// w+ - 读写(创建或截断)
// a - 追加(创建或追加)
// a+ - 读取追加
// wx - 写入(仅创建,文件存在则失败)
// x - 排他创建
fs.open('file.txt', 'wx', (err, fd) => {
if (err) {
if (err.code === 'EEXIST') {
console.log('文件已存在');
}
return;
}
// 写入数据
fs.write(fd, 'data', (err) => {
fs.close(fd);
});
});3. 目录操作
问题:Node.js 如何进行目录操作?
答案:
javascript
const fs = require('fs').promises;
const path = require('path');
// 1. 创建目录
await fs.mkdir('new-directory');
await fs.mkdir('parent/child', { recursive: true }); // 递归创建
// 2. 读取目录内容
const files = await fs.readdir('directory');
console.log(files); // ['file1.txt', 'file2.txt', 'subdir']
// 带文件类型的目录读取
const entries = await fs.readdir('directory', { withFileTypes: true });
for (const entry of entries) {
console.log(entry.name, entry.isFile(), entry.isDirectory());
}
// 3. 删除目录
await fs.rmdir('empty-directory'); // 只能删除空目录
await fs.rm('directory', { recursive: true, force: true }); // 递归删除
// 4. 重命名/移动
await fs.rename('old-name', 'new-name');
await fs.rename('file.txt', 'moved/file.txt'); // 移动文件
// 5. 检查文件/目录状态
const stats = await fs.stat('file.txt');
console.log(stats.isFile()); // true
console.log(stats.isDirectory()); // false
console.log(stats.size); // 文件大小(字节)
console.log(stats.mtime); // 修改时间
console.log(stats.birthtime); // 创建时间
// 6. 检查是否存在
try {
await fs.access('file.txt', fs.constants.F_OK); // 检查存在
await fs.access('file.txt', fs.constants.R_OK); // 检查可读
await fs.access('file.txt', fs.constants.W_OK); // 检查可写
console.log('文件存在且可访问');
} catch {
console.log('文件不存在或无法访问');
}
// 7. 更改权限
await fs.chmod('file.txt', 0o644); // rw-r--r--
await fs.chown('file.txt', 1000, 1000); // 更改所有者和组
// 8. 创建临时目录
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'app-'));
console.log(tmpDir); // /tmp/app-abc123递归遍历目录:
javascript
async function walkDir(dir, callback) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
await walkDir(fullPath, callback);
} else {
await callback(fullPath);
}
}
}
// 使用
await walkDir('./src', (filePath) => {
console.log('文件:', filePath);
});4. 文件监控
问题:如何监控文件或目录的变化?
答案:
使用 fs.watch():
javascript
const fs = require('fs');
// 监控文件
const watcher = fs.watch('file.txt', (eventType, filename) => {
console.log(`事件类型: ${eventType}`);
if (filename) {
console.log(`文件名: ${filename}`);
}
});
// 停止监控
watcher.close();
// 监控目录
const dirWatcher = fs.watch('directory', { recursive: true }, (eventType, filename) => {
console.log(`${eventType}: ${filename}`);
});使用 fs.watchFile():
javascript
const fs = require('fs');
// 轮询方式监控(兼容性更好)
fs.watchFile('file.txt', { interval: 1000 }, (curr, prev) => {
console.log('当前状态:', curr);
console.log('之前状态:', prev);
if (curr.mtime !== prev.mtime) {
console.log('文件被修改');
}
if (curr.size !== prev.size) {
console.log('文件大小变化');
}
});
// 停止监控
fs.unwatchFile('file.txt');使用 chokidar(推荐):
javascript
const chokidar = require('chokidar');
// 初始化监控
const watcher = chokidar.watch('file-or-dir', {
ignored: /(^|[\/\\])\../, // 忽略隐藏文件
persistent: true,
ignoreInitial: false,
awaitWriteFinish: {
stabilityThreshold: 300,
pollInterval: 100
}
});
// 事件监听
watcher
.on('add', path => console.log(`文件添加: ${path}`))
.on('change', path => console.log(`文件修改: ${path}`))
.on('unlink', path => console.log(`文件删除: ${path}`))
.on('addDir', path => console.log(`目录添加: ${path}`))
.on('unlinkDir', path => console.log(`目录删除: ${path}`))
.on('error', error => console.log(`错误: ${error}`))
.on('ready', () => console.log('初始扫描完成'));
// 停止监控
await watcher.close();5. 文件流操作
问题:如何使用流进行文件操作?
答案:
读写流:
javascript
const fs = require('fs');
const path = require('path');
// 1. 复制文件(管道)
const source = fs.createReadStream('source.txt');
const destination = fs.createWriteStream('destination.txt');
source.pipe(destination);
destination.on('finish', () => {
console.log('复制完成');
});
// 2. 带进度监控的复制
const copyFileWithProgress = (src, dest) => {
return new Promise((resolve, reject) => {
const stats = fs.statSync(src);
const totalSize = stats.size;
let copiedSize = 0;
const readStream = fs.createReadStream(src);
const writeStream = fs.createWriteStream(dest);
readStream.on('data', (chunk) => {
copiedSize += chunk.length;
const progress = (copiedSize / totalSize * 100).toFixed(2);
console.log(`进度: ${progress}%`);
});
readStream.pipe(writeStream);
writeStream.on('finish', resolve);
writeStream.on('error', reject);
});
};
// 3. 大文件处理(逐行读取)
const readline = require('readline');
async function readLines(filename) {
const fileStream = fs.createReadStream(filename);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
for await (const line of rl) {
console.log(`行: ${line}`);
}
}
// 4. 压缩文件
const zlib = require('zlib');
const gzip = zlib.createGzip();
const input = fs.createReadStream('file.txt');
const output = fs.createWriteStream('file.txt.gz');
input.pipe(gzip).pipe(output);
// 5. 解压文件
const gunzip = zlib.createGunzip();
const compressed = fs.createReadStream('file.txt.gz');
const decompressed = fs.createWriteStream('file.txt');
compressed.pipe(gunzip).pipe(decompressed);Transform 流:
javascript
const { Transform } = require('stream');
// 自定义 Transform 流(转换大小写)
const upperCaseTransform = new Transform({
transform(chunk, encoding, callback) {
this.push(chunk.toString().toUpperCase());
callback();
}
});
fs.createReadStream('input.txt')
.pipe(upperCaseTransform)
.pipe(fs.createWriteStream('output.txt'));6. 路径操作
问题:Node.js 中如何处理文件路径?
答案:
path 模块:
javascript
const path = require('path');
// 1. 路径拼接
const fullPath = path.join('/foo', 'bar', 'baz'); // /foo/bar/baz
const resolvedPath = path.resolve('foo', 'bar'); // 绝对路径
// 2. 路径解析
path.parse('/home/user/file.txt');
// {
// root: '/',
// dir: '/home/user',
// base: 'file.txt',
// ext: '.txt',
// name: 'file'
// }
// 3. 获取路径各部分
path.dirname('/foo/bar/baz.txt'); // /foo/bar
path.basename('/foo/bar/baz.txt'); // baz.txt
path.extname('/foo/bar/baz.txt'); // .txt
// 4. 规范化路径
path.normalize('/foo/bar//baz/../qux'); // /foo/bar/qux
// 5. 相对路径
path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb');
// ../../impl/bbb
// 6. 平台相关
path.sep; // 路径分隔符(Windows: \, POSIX: /)
path.delimiter; // 环境变量分隔符(Windows: ;, POSIX: :)
// 7. 跨平台路径
path.posix.join('foo', 'bar'); // 使用 POSIX 风格
path.win32.join('foo', 'bar'); // 使用 Windows 风格
// 8. __dirname 和 __filename
console.log(__filename); // 当前文件绝对路径
console.log(__dirname); // 当前目录绝对路径
// 9. 安全的路径拼接
const userPath = '../etc/passwd'; // 恶意路径
const safePath = path.join(__dirname, 'uploads', path.basename(userPath));
// 结果: /project/uploads/passwd(安全)7. 文件上传处理
问题:如何处理文件上传?
答案:
使用 multer:
javascript
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
// 配置存储
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
}
});
// 文件过滤
const fileFilter = (req, file, cb) => {
if (file.mimetype === 'image/jpeg' || file.mimetype === 'image/png') {
cb(null, true);
} else {
cb(new Error('只支持 JPEG 和 PNG 格式'), false);
}
};
const upload = multer({
storage,
fileFilter,
limits: {
fileSize: 5 * 1024 * 1024 // 5MB
}
});
// 单文件上传
app.post('/upload', upload.single('avatar'), (req, res) => {
res.json({
message: '上传成功',
file: req.file
});
});
// 多文件上传
app.post('/upload-multiple', upload.array('photos', 5), (req, res) => {
res.json({
message: '上传成功',
files: req.files
});
});
// 多字段上传
const multiUpload = upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'gallery', maxCount: 8 }
]);
app.post('/upload-fields', multiUpload, (req, res) => {
res.json({
avatar: req.files['avatar'],
gallery: req.files['gallery']
});
});
// 内存存储(处理后再保存)
const memoryStorage = multer({ storage: multer.memoryStorage() });
app.post('/upload-process', memoryStorage.single('file'), async (req, res) => {
const buffer = req.file.buffer;
// 处理文件(如压缩、转换格式)
const processedBuffer = await processImage(buffer);
// 保存到磁盘或云存储
await fs.writeFile('processed.jpg', processedBuffer);
res.json({ message: '处理完成' });
});8. 临时文件和清理
问题:如何处理临时文件和清理?
答案:
javascript
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
// 1. 创建临时文件
async function createTempFile(content) {
const tmpDir = os.tmpdir();
const tmpFile = path.join(tmpDir, `tmp-${Date.now()}.txt`);
await fs.writeFile(tmpFile, content);
return {
path: tmpFile,
async cleanup() {
try {
await fs.unlink(tmpFile);
} catch (err) {
// 忽略删除错误
}
}
};
}
// 2. 使用 tmp 包
const tmp = require('tmp');
// 临时文件
tmp.file({ prefix: 'myapp-', postfix: '.txt' }, (err, path, fd, cleanupCallback) => {
if (err) throw err;
// 使用临时文件
fs.writeFile(path, 'data', () => {
// 清理
cleanupCallback();
});
});
// Promise 方式
const tmpFile = tmp.fileSync();
console.log(tmpFile.name);
tmpFile.removeCallback();
// 临时目录
const tmpDir = tmp.dirSync({ unsafeCleanup: true });
console.log(tmpDir.name);
tmpDir.removeCallback();
// 3. 自动清理类
class TempFileManager {
constructor() {
this.tempFiles = new Set();
// 进程退出时清理
process.on('exit', () => this.cleanupAll());
process.on('SIGINT', () => {
this.cleanupAll();
process.exit(0);
});
}
async create(content, ext = '.tmp') {
const tmpPath = path.join(os.tmpdir(), `app-${Date.now()}${ext}`);
await fs.writeFile(tmpPath, content);
this.tempFiles.add(tmpPath);
return tmpPath;
}
async cleanup(filePath) {
try {
await fs.unlink(filePath);
this.tempFiles.delete(filePath);
} catch (err) {
console.error('清理失败:', err);
}
}
async cleanupAll() {
for (const filePath of this.tempFiles) {
await this.cleanup(filePath);
}
}
}9. 文件操作错误处理
问题:文件操作中常见的错误有哪些?如何处理?
答案:
常见错误及处理:
javascript
const fs = require('fs').promises;
// 1. 文件不存在
async function safeReadFile(filePath) {
try {
return await fs.readFile(filePath, 'utf8');
} catch (err) {
if (err.code === 'ENOENT') {
console.log('文件不存在');
return null;
}
throw err;
}
}
// 2. 权限不足
async function safeWriteFile(filePath, data) {
try {
await fs.writeFile(filePath, data);
} catch (err) {
if (err.code === 'EACCES') {
console.log('权限不足,无法写入文件');
// 尝试更改权限
await fs.chmod(filePath, 0o644);
await fs.writeFile(filePath, data);
} else {
throw err;
}
}
}
// 3. 文件被占用
async function safeDeleteFile(filePath) {
try {
await fs.unlink(filePath);
} catch (err) {
if (err.code === 'EBUSY') {
console.log('文件被占用,稍后重试');
setTimeout(() => safeDeleteFile(filePath), 1000);
} else if (err.code === 'ENOENT') {
console.log('文件已不存在');
} else {
throw err;
}
}
}
// 4. 磁盘空间不足
async function checkDiskSpace(filePath, requiredSpace) {
const stats = await fs.statfs(path.dirname(filePath));
const availableSpace = stats.bavail * stats.bsize;
if (availableSpace < requiredSpace) {
throw new Error(`磁盘空间不足,需要 ${requiredSpace},可用 ${availableSpace}`);
}
}
// 5. 文件锁(防止并发写入)
const lockfile = require('proper-lockfile');
async function writeWithLock(filePath, data) {
let release;
try {
release = await lockfile.lock(filePath);
await fs.writeFile(filePath, data);
} finally {
if (release) {
await release();
}
}
}
// 6. 重试机制
async function withRetry(operation, maxRetries = 3, delay = 1000) {
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (err) {
if (i === maxRetries - 1) throw err;
if (err.code === 'EBUSY' || err.code === 'EAGAIN') {
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw err;
}
}
}
}10. 文件操作最佳实践
问题:文件操作有哪些最佳实践?
答案:
最佳实践:
- 使用 Promise API:
javascript
// 推荐
const fs = require('fs').promises;
const data = await fs.readFile('file.txt', 'utf8');
// 避免回调地狱
fs.readFile('file.txt', (err, data) => {
if (err) {
fs.writeFile('error.log', err.message, () => {
// ...
});
}
});- 流式处理大文件:
javascript
// 大文件使用流
const stream = fs.createReadStream('large-file.txt');
stream.pipe(process.stdout);
// 避免一次性读取大文件
const data = await fs.readFile('huge-file.zip'); // 内存溢出风险- 原子写入:
javascript
async function atomicWrite(filePath, data) {
const tmpFile = `${filePath}.tmp`;
await fs.writeFile(tmpFile, data);
await fs.rename(tmpFile, filePath); // 原子操作
}- 路径安全:
javascript
// 验证路径,防止目录遍历攻击
function sanitizePath(userInput, baseDir) {
const targetPath = path.join(baseDir, userInput);
const resolvedPath = path.resolve(targetPath);
const resolvedBase = path.resolve(baseDir);
if (!resolvedPath.startsWith(resolvedBase)) {
throw new Error('非法路径');
}
return resolvedPath;
}- 资源释放:
javascript
// 使用 try-finally 确保资源释放
let fd;
try {
fd = await fs.open('file.txt', 'r');
const buffer = Buffer.alloc(1024);
await fd.read(buffer, 0, 1024, 0);
} finally {
if (fd) {
await fd.close();
}
}- 并发控制:
javascript
const pLimit = require('p-limit');
const limit = pLimit(5); // 限制并发数为 5
const files = ['file1.txt', 'file2.txt', 'file3.txt'];
await Promise.all(
files.map(file => limit(() => fs.readFile(file, 'utf8')))
);