强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Node.js 开发指南 / 第 10 章 · 文件系统

第 10 章 · 文件系统

10.1 fs 模块概览

Node.js 的 fs(File System)模块提供了与文件系统交互的 API,支持同步和异步两种风格。

const fs = require('fs');
const fsPromises = require('fs/promises');
const path = require('path');

API 风格对比

风格示例特点
同步(Sync)fs.readFileSync()阻塞事件循环
回调fs.readFile(path, cb)非阻塞,错误优先回调
PromisefsPromises.readFile(path)非阻塞,async/await 友好
// 同步
try {
  const data = fs.readFileSync('/etc/hosts', 'utf8');
  console.log(data);
} catch (err) {
  console.error(err);
}

// 回调
fs.readFile('/etc/hosts', 'utf8', (err, data) => {
  if (err) return console.error(err);
  console.log(data);
});

// Promise(推荐)
async function readFile() {
  try {
    const data = await fsPromises.readFile('/etc/hosts', 'utf8');
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}

10.2 文件读取

const fs = require('fs/promises');

// 读取整个文件(小文件推荐)
const content = await fs.readFile('config.json', 'utf8');
const config = JSON.parse(content);

// 读取二进制文件
const buffer = await fs.readFile('image.png');
console.log(buffer.length); // 字节数

// 读取文件信息
const stat = await fs.stat('package.json');
console.log(stat.size);       // 文件大小(字节)
console.log(stat.mtime);      // 最后修改时间
console.log(stat.isFile());   // true
console.log(stat.isDirectory()); // false
console.log(stat.mode);       // 文件权限

// 检查文件是否存在
async function fileExists(filePath) {
  try {
    await fs.access(filePath, fs.constants.F_OK);
    return true;
  } catch {
    return false;
  }
}

// 更好的方式(Node.js 14+)
const { access } = require('fs/promises');
async function fileExistsV2(filePath) {
  try {
    await access(filePath);
    return true;
  } catch {
    return false;
  }
}

流式读取大文件

const { createReadStream } = require('fs');
const { createInterface } = require('readline');

// 逐行读取(内存友好)
async function processLargeFile(filePath) {
  const rl = createInterface({
    input: createReadStream(filePath, { encoding: 'utf8' }),
    crlfDelay: Infinity,
  });

  let lineCount = 0;
  for await (const line of rl) {
    lineCount++;
    // 处理每一行
  }
  return lineCount;
}

10.3 文件写入

const fs = require('fs/promises');

// 写入文件(覆盖)
await fs.writeFile('output.txt', 'Hello, World!', 'utf8');

// 追加写入
await fs.appendFile('log.txt', `${new Date().toISOString()} - 新日志\n`);

// 写入 JSON
const config = { host: 'localhost', port: 3000 };
await fs.writeFile('config.json', JSON.stringify(config, null, 2), 'utf8');

// 写入时自动创建目录
async function writeFileWithDirs(filePath, content) {
  const dir = path.dirname(filePath);
  await fs.mkdir(dir, { recursive: true });
  await fs.writeFile(filePath, content, 'utf8');
}

// 以流的方式写入大文件
const { createWriteStream } = require('fs');
const writable = createWriteStream('output.txt');
for (let i = 0; i < 1000000; i++) {
  writable.write(`行 ${i}\n`);
}
writable.end();

10.4 文件操作

const fs = require('fs/promises');

// 复制文件
await fs.copyFile('source.txt', 'dest.txt');

// 重命名/移动文件
await fs.rename('old-name.txt', 'new-name.txt');
await fs.rename('file.txt', 'archive/file.txt'); // 移动

// 删除文件
await fs.unlink('temp.txt');

// 创建硬链接
await fs.link('original.txt', 'hard-link.txt');

// 创建符号链接
await fs.symlink('/path/to/original', 'link-name');

// 修改文件权限
await fs.chmod('script.sh', 0o755);

// 修改文件时间
const now = new Date();
await fs.utimes('file.txt', now, now);

// 截断文件(保留前 N 字节)
await fs.truncate('file.txt', 100); // 保留前 100 字节

10.5 目录操作

const fs = require('fs/promises');

// 创建目录
await fs.mkdir('new-dir');
await fs.mkdir('path/to/deep/dir', { recursive: true }); // 递归创建

// 读取目录
const files = await fs.readdir('.');
console.log(files); // ['file1.txt', 'dir1', ...]

// 读取目录(带文件类型信息)
const entries = await fs.readdir('.', { withFileTypes: true });
for (const entry of entries) {
  console.log(`${entry.name} - ${entry.isDirectory() ? '目录' : '文件'}`);
}

// 递归遍历目录
async function walkDir(dir) {
  const entries = await fs.readdir(dir, { withFileTypes: true });
  const results = [];
  
  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      results.push(...(await walkDir(fullPath)));
    } else {
      results.push(fullPath);
    }
  }
  return results;
}

const allFiles = await walkDir('src');
console.log(allFiles);

// 删除目录
await fs.rmdir('empty-dir');               // 只能删除空目录
await fs.rm('dir-with-files', { recursive: true }); // 递归删除

// 创建临时目录
const os = require('os');
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'myapp-'));
console.log(tmpDir); // /tmp/myapp-abc123

// 读取目录大小
async function dirSize(dir) {
  const entries = await fs.readdir(dir, { withFileTypes: true });
  let size = 0;
  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      size += await dirSize(fullPath);
    } else {
      const stat = await fs.stat(fullPath);
      size += stat.size;
    }
  }
  return size;
}

10.6 文件监听

const fs = require('fs');

// fs.watch(推荐,基于操作系统事件)
const watcher = fs.watch('src', { recursive: true }, (eventType, filename) => {
  console.log(`事件: ${eventType}, 文件: ${filename}`);
});

// 关闭监听
watcher.close();

// fs.watchFile(基于轮询,跨平台兼容但效率低)
fs.watchFile('config.json', { interval: 1000 }, (curr, prev) => {
  console.log(`文件大小: ${prev.size}${curr.size}`);
  console.log(`修改时间: ${prev.mtime}${curr.mtime}`);
});

fs.unwatchFile('config.json');

// 使用 chokidar(第三方库,更稳定)
// npm install chokidar
const chokidar = require('chokidar');

const watcher2 = chokidar.watch('src', {
  ignored: /node_modules/,
  persistent: true,
  ignoreInitial: true,
});

watcher2
  .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}`));

10.7 path 模块

const path = require('path');

// 基本路径操作
console.log(path.basename('/home/user/file.txt'));      // 'file.txt'
console.log(path.basename('/home/user/file.txt', '.txt')); // 'file'
console.log(path.dirname('/home/user/file.txt'));       // '/home/user'
console.log(path.extname('file.txt'));                  // '.txt'
console.log(path.extname('file.tar.gz'));               // '.gz'

// 路径拼接
console.log(path.join('/home', 'user', 'file.txt'));
// '/home/user/file.txt'(规范化路径)

// 绝对路径解析
console.log(path.resolve('file.txt'));
// '/home/user/project/file.txt'(基于 cwd)

console.log(path.resolve('/a', '/b', 'c'));
// '/b/c'(绝对路径会重置)

// 相对路径
console.log(path.relative('/home/user/project', '/home/user/docs'));
// '../docs'

// 路径解析
const parsed = path.parse('/home/user/file.txt');
console.log(parsed);
// { root: '/', dir: '/home/user', base: 'file.txt', ext: '.txt', name: 'file' }

// 格式化路径
console.log(path.format(parsed));
// '/home/user/file.txt'

// 规范化路径
console.log(path.normalize('/home/user/../user/./file.txt'));
// '/home/user/file.txt'

// 跨平台路径分隔符
console.log(path.sep);       // '/' (Unix) 或 '\\' (Windows)
console.log(path.delimiter); // ':' (Unix) 或 ';' (Windows)

跨平台路径处理

// ⚠️ 不要手动拼接路径
// const bad = dir + '/' + file;  // Windows 上会有问题

// ✅ 使用 path.join
const good = path.join(dir, file);

// 使用 path.posix 或 path.win32 强制特定平台
console.log(path.posix.join('a', 'b'));  // 'a/b'
console.log(path.win32.join('a', 'b'));  // 'a\\b'

// URL 路径处理(推荐用于 URL)
const { fileURLToPath, pathToFileURL } = require('node:url');

// ESM 中获取 __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

10.8 实用工具函数

const fs = require('fs/promises');
const path = require('path');

// 确保目录存在
async function ensureDir(dir) {
  await fs.mkdir(dir, { recursive: true });
}

// 安全读取 JSON
async function readJSON(filePath) {
  try {
    const content = await fs.readFile(filePath, 'utf8');
    return JSON.parse(content);
  } catch (err) {
    if (err.code === 'ENOENT') return null;
    throw err;
  }
}

// 安全写入 JSON(先写临时文件再重命名,防止写入中断导致文件损坏)
async function writeJSON(filePath, data) {
  const tmpPath = filePath + '.tmp';
  await fs.writeFile(tmpPath, JSON.stringify(data, null, 2), 'utf8');
  await fs.rename(tmpPath, filePath);
}

// 查找文件
async function findFiles(dir, pattern) {
  const entries = await fs.readdir(dir, { withFileTypes: true });
  const results = [];
  
  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name);
    if (entry.name === 'node_modules' || entry.name === '.git') continue;
    
    if (entry.isDirectory()) {
      results.push(...(await findFiles(fullPath, pattern)));
    } else if (pattern.test(entry.name)) {
      results.push(fullPath);
    }
  }
  return results;
}

// 用法:查找所有 .js 文件
const jsFiles = await findFiles('.', /\.js$/);

注意事项

⚠️ 生产环境避免同步 APIreadFileSync 等会阻塞事件循环,在服务器中应使用异步版本。

⚠️ 大文件用流readFile 会将整个文件加载到内存,大文件应使用 createReadStream

⚠️ 路径用 path.join:不要手动拼接路径字符串,path.join 自动处理分隔符和规范化。

⚠️ 文件监听的平台差异fs.watch 在不同操作系统上行为可能不一致,生产环境推荐 chokidar

⚠️ 并发写入安全:多个进程同时写入同一文件可能导致数据损坏,使用文件锁或临时文件 + 重命名。

业务场景

  1. 配置管理:读取 JSON/YAML 配置文件,支持热更新
  2. 日志收集:流式读取大量日志文件进行分析
  3. 文件上传/下载:处理用户上传的文件
  4. 静态资源服务:为 Web 应用提供静态文件
  5. 脚本工具:批量处理文件(重命名、转换、压缩)

扩展阅读


上一章第 9 章 · Buffer 与二进制数据 下一章第 11 章 · HTTP 服务与客户端 — 构建 HTTP 服务器、客户端、中间件和路由。