返回首页

说说对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);
  });
}

面试要点

  1. 操作方式:清楚同步和异步的区别,知道何时使用哪种
  2. 核心概念:理解mode、flag、fd的含义和用法
  3. 性能考虑:大文件使用流式处理,避免阻塞事件循环
  4. 错误处理:能区分ENOENT、EACCES等常见错误码
  5. 安全实践:防范路径遍历攻击,正确处理用户输入的路径

常见追问

  • Q: 同步方法和异步方法有什么区别?

    • A: 同步方法阻塞事件循环直到完成,适用于启动配置;异步方法不阻塞,适用于运行时请求处理
  • Q: 如何安全地处理用户上传的文件路径?

    • A: 使用path.join和路径验证,防止目录遍历攻击
  • Q: 读取10GB的文件应该用什么方法?

    • A: 使用createReadStream流式读取,避免一次性载入内存