Skip to content

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. 文件操作最佳实践

问题:文件操作有哪些最佳实践?

答案

最佳实践

  1. 使用 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, () => {
      // ...
    });
  }
});
  1. 流式处理大文件
javascript
// 大文件使用流
const stream = fs.createReadStream('large-file.txt');
stream.pipe(process.stdout);

// 避免一次性读取大文件
const data = await fs.readFile('huge-file.zip');  // 内存溢出风险
  1. 原子写入
javascript
async function atomicWrite(filePath, data) {
  const tmpFile = `${filePath}.tmp`;
  await fs.writeFile(tmpFile, data);
  await fs.rename(tmpFile, filePath);  // 原子操作
}
  1. 路径安全
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;
}
  1. 资源释放
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();
  }
}
  1. 并发控制
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')))
);