跳转到内容

最佳实践与常见问题

同步方法使用原则

适用场景

js
// 启动初始化阶段(阻塞不影响业务)
const config = JSON.parse(fs.readFileSync('config.json'));

// CLI 工具脚本(执行顺序明确)
fs.writeFileSync('output.log', processReport());

危险场景

js
// HTTP 请求处理中(会阻塞所有并发请求)
app.get('/data', () => {
  const data = fs.readFileSync('large.csv'); // ❌
});

路径处理陷阱

跨平台问题

js
// 错误:硬编码路径分隔符
const badPath = 'src\\config'; // Windows 专属

// 正确:动态路径处理
const goodPath = path.join('src', 'config', 'app.ini');

路径安全防护

js
// 过滤用户输入路径
function sanitizePath(userInput) {
  const basePath = path.resolve('uploads');
  const fullPath = path.join(basePath, userInput);
  if (!fullPath.startsWith(basePath)) {
    throw new Error('非法路径访问');
  }
  return fullPath;
}

回调地狱解决方案

Promise 包装技巧

js
const readFile = (file) => new Promise((resolve, reject) => {
  fs.readFile(file, (err, data) => {
    err ? reject(err) : resolve(data);
  });
});

// 使用 async/await 简化
async function processFiles() {
  const [a, b] = await Promise.all([
    readFile('a.txt'),
    readFile('b.txt')
  ]);
}

内存管理策略

大文件处理对比

js
// 危险:一次性读取(内存峰值 2GB)
fs.readFile('huge.iso', (err, data) => {});

// 安全:流式处理(内存稳定)
fs.createReadStream('huge.iso')
  .pipe(fs.createWriteStream('copy.iso'));

缓存优化方案

js
const fileCache = new Map();

async function getFileWithCache(path) {
  if (fileCache.has(path)) {
    return fileCache.get(path);
  }
  const data = await fs.promises.readFile(path);
  fileCache.set(path, data);
  return data;
}

⚠️ 常见性能问题

重复文件状态检查

js
// 错误:循环中频繁调用 stat
setInterval(() => {
  fs.stat('file', () => { ... }); // 高 I/O 开销
}, 100);

// 优化:使用 watch 或合理缓存

小文件流式滥用

js
// 不必要流处理(增加复杂度)
createReadStream('1kb.txt').pipe(process.stdout); 

// 更优方案:直接 readFile

不当的并发控制

js
// 同时打开数千个文件描述符
files.forEach(file => fs.promises.open(file)); // 导致 EMFILE 错误

// 解决方案:使用 p-limit 控制并发

✅ 工程化实践

配置加载规范

js
// 使用 require 缓存机制
function reloadConfig() {
  delete require.cache[require.resolve('./config.json')];
  return require('./config.json');
}

日志滚动策略

js
const MAX_LOG_SIZE = 1024 * 1024 * 100; // 100MB

async function writeLog(message) {
  const stats = await fs.promises.stat('app.log');
  if (stats.size > MAX_LOG_SIZE) {
    await fs.promises.rename('app.log', `app.${Date.now()}.log`);
  }
  await fs.promises.appendFile('app.log', message);
}

原子文件操作

js
// 使用临时文件+重命名保证操作完整性
async function safeWrite(path, content) {
  const tempPath = `${path}.${process.pid}.tmp`;
  await fs.promises.writeFile(tempPath, content);
  await fs.promises.rename(tempPath, path);
}

高频问题解答

Q:为什么文件已存在却报 ENOENT?

  • 路径包含不存在的中间目录
  • 文件路径包含非法字符(如 NUL)
  • 父目录权限不足

Q:如何正确处理文件锁?

js
// 使用 flock(需要第三方库)
const { flock } = require('fs-ext');
const fd = await fs.promises.open('data.lock', 'w');
await flock(fd, 'ex'); // 排他锁

Q:如何监控整个目录树的变化?

  • 使用 chokidar 库(封装跨平台处理)
  • 避免递归使用 fs.watch
js
const chokidar = require('chokidar');
chokidar.watch('src', { ignoreInitial: true })
  .on('all', (event, path) => { ... });