Node.js fs 与 path 完全指南

从零到一掌握 Node.js 文件系统操作,小白也能看懂的实战教程

📌 核心原则

在开始之前,请务必记住这四条黄金法则:

1. 异步优先 :优先使用 fs.promises(不会阻塞程序运行)
2. 路径安全 :使用 path.* 组合路径,避免字符串拼接
3. 原子写入 :关键数据采用 "临时文件 + rename" 模式(下文会解释)
4. 安全校验:所有用户输入的路径必须做安全检查


💡 术语小课堂

在正式开始前,先了解几个常见术语:

🔹 什么是 API 签名(函数签名)?

就是函数的"使用说明书",告诉你:

  • 函数叫什么名字
  • 需要传什么参数
  • 会返回什么结果

例如:readFile(path, options)Promise<string> 意思是:调用 readFile 函数,传入路径和选项,会返回一个 Promise,最终得到字符串内容

🔹 什么是原子写入?

简单理解:要么全部成功,要么全部失败,不会出现"写了一半"的情况。

举例:你正在保存一个重要配置文件,突然断电了

  • ❌ 普通写入:文件可能只写了一半,变成乱码
  • ✅ 原子写入:要么保存成功,要么还是原来的文件,绝不会损坏

实现方法:先写到临时文件,成功后再一次性替换原文件(rename 操作是原子性的)

🔹 什么是目录穿越攻击?

黑客通过特殊路径(如 ../../etc/passwd)来访问不该访问的文件。

举例:你的网站允许用户下载 /uploads 目录下的文件

  • 用户正常请求:/uploads/avatar.png
  • 黑客恶意请求:/uploads/../../etc/passwd ❌ (试图访问系统密码文件)

防御方法:检查最终路径是否真的在允许的目录内

🔹 异步 vs 同步?

  • 异步:不等待任务完成,程序继续往下执行(推荐)
  • 同步:必须等待任务完成才能继续(会阻塞程序,不推荐)

举例:读取一个 100MB 的文件

  • 异步:发起读取请求后,程序可以继续处理其他事情(如响应用户点击)
  • 同步:程序卡住,等文件读完才能动(用户会觉得卡顿)

一、fs 模块 - 文件系统操作

1.1 API 基础用法

在动手实践前,先快速浏览一下 fs 模块提供了哪些工具:

📖 文件读写类

readFile(path, options) → 返回文件内容

js 复制代码
// 读取文本文件
const content = await fs.readFile('config.json', 'utf8');
  • 参数:文件路径 + 编码方式(如 'utf8'
  • 返回:文件内容(字符串或二进制数据)
  • 适用:小文件(< 10MB)

writeFile(path, data, options) → 写入文件

js 复制代码
// 写入文本文件
await fs.writeFile('log.txt', '日志内容', 'utf8');
  • 参数:文件路径 + 要写入的内容 + 编码方式
  • 说明:文件不存在会自动创建,存在则覆盖
  • 适用:小文件

appendFile(path, data, options) → 追加内容到文件末尾

js 复制代码
// 在文件末尾追加内容(不覆盖原内容)
await fs.appendFile('log.txt', '新的一行日志\n', 'utf8');
  • 适用:日志记录

📁 目录操作类

mkdir(path, options) → 创建目录

js 复制代码
// 创建多级目录(父目录不存在也会自动创建)
await fs.mkdir('data/cache/temp', { recursive: true });
  • 重要参数:recursive: true 可创建多级目录
  • 说明:目录已存在不会报错

readdir(path, options) → 读取目录内容

js 复制代码
// 获取目录下的所有文件和子目录
const files = await fs.readdir('data');  // 返回文件名数组

// 获取详细信息(包括文件类型)
const entries = await fs.readdir('data', { withFileTypes: true });
for (const entry of entries) {
  console.log(entry.name, entry.isFile() ? '文件' : '目录');
}

🔍 信息查询类

stat(path) → 获取文件详细信息

js 复制代码
const info = await fs.stat('file.txt');
console.log(info.size);        // 文件大小(字节)
console.log(info.isFile());    // 是否为文件
console.log(info.mtime);       // 最后修改时间

access(path, mode) → 检查文件是否存在/可读写

js 复制代码
import { constants } from 'node:fs';

// 检查文件是否可读写
await fs.access('file.txt', constants.R_OK | constants.W_OK);

🛠️ 文件操作类

copyFile(src, dest) → 拷贝文件

js 复制代码
await fs.copyFile('source.txt', 'backup.txt');

rename(oldPath, newPath) → 重命名或移动文件

js 复制代码
await fs.rename('old.txt', 'new.txt');  // 重命名
await fs.rename('temp/file.txt', 'data/file.txt');  // 移动

rm(path, options) → 删除文件或目录

js 复制代码
// 删除文件(不存在也不报错)
await fs.rm('file.txt', { force: true });

// 删除整个目录(小心使用!)
await fs.rm('temp', { recursive: true, force: true });
  • ⚠️ 危险操作recursive: true 会删除整个目录树

unlink(path) → 删除文件

js 复制代码
await fs.unlink('file.txt');  // 只能删除文件,不能删除目录

🌊 流式操作类(处理大文件)

createReadStream(path) → 创建读取流

js 复制代码
const stream = fs.createReadStream('video.mp4');
// 逐块读取,不会一次性加载到内存

createWriteStream(path) → 创建写入流

js 复制代码
const stream = fs.createWriteStream('output.txt');
stream.write('内容');

pipeline(...streams) → 连接多个流(最佳实践)

js 复制代码
import { pipeline } from 'node:stream/promises';

// 复制大文件
await pipeline(
  fs.createReadStream('large.dat'),
  fs.createWriteStream('large.dat.backup')
);

1.2 异步 vs 同步方法

Node.js 为每个文件操作都提供了两种版本

🟢 异步方法(推荐)

使用 fs.promises 或回调函数,不会阻塞程序:

js 复制代码
import { promises as fs } from 'node:fs';

// ✅ 推荐:使用 Promise 风格(现代写法)
const content = await fs.readFile('file.txt', 'utf8');
console.log('读取完成');
console.log('这行代码会等上面读取完成后执行');

// 在等待读取期间,程序可以处理其他任务(如响应用户操作)

优点

  • 不阻塞程序,性能好
  • 适合服务器环境(可同时处理多个请求)

🔴 同步方法(谨慎使用)

方法名以 Sync 结尾,会阻塞程序:

js 复制代码
import fs from 'node:fs';

// ❌ 不推荐:同步方法(会卡住程序)
const content = fs.readFileSync('file.txt', 'utf8');
console.log('读取完成');

// 在读取文件期间,程序完全卡住,无法做其他事情

缺点

  • 会阻塞事件循环,程序卡住
  • 在服务器环境中会严重影响性能

何时可以用?

  • 程序启动时读取配置文件(只执行一次)
  • 命令行工具的简单脚本(单线程,无并发)

示例:程序启动时读取配置

js 复制代码
import fs from 'node:fs';

// 程序启动时,可以使用同步读取
const config = JSON.parse(
  fs.readFileSync('./config.json', 'utf8')
);

// 但在请求处理函数中,必须用异步
app.get('/data', async (req, res) => {
  const data = await fs.promises.readFile('data.json', 'utf8'); // ✅
  res.send(data);
});

1.3 实战场景详解

掌握了基础 API 后,我们来看看在实际项目中如何应用。

场景 1:读取与写入配置文件

💡 场景描述

  • 程序启动时读取 config.json 配置
  • 用户修改设置后保存到配置文件
  • 写日志到 app.log 文件
js 复制代码
import { promises as fs } from 'node:fs';

// 读取配置文件
async function loadConfig() {
  const text = await fs.readFile('config.json', 'utf8');
  return JSON.parse(text);
}

// 保存配置
async function saveConfig(config) {
  const text = JSON.stringify(config, null, 2);  // 格式化为易读的 JSON
  await fs.writeFile('config.json', text, 'utf8');
}

// 记录日志(追加方式)
async function log(message) {
  const timestamp = new Date().toISOString();
  await fs.appendFile('app.log', `[${timestamp}] ${message}\n`, 'utf8');
}

// 使用示例
const config = await loadConfig();
config.theme = 'dark';
await saveConfig(config);
await log('配置已更新');

⚠️ 注意事项

  • 小文件(< 10MB)用 readFile/writeFile
  • 大文件会导致内存溢出(OOM),必须用流式处理(见后文)
  • 指定 'utf8' 编码会返回字符串,否则返回二进制 Buffer

场景 2:遍历目录并分类文件

💡 场景描述

  • 扫描 uploads 目录下的所有文件
  • 根据类型(图片、文档、其他)分别统计
js 复制代码
import { promises as fs } from 'node:fs';
import path from 'node:path';

async function classifyFiles(dirPath) {
  const stats = {
    images: [],
    documents: [],
    others: []
  };
  
  // withFileTypes: true 返回详细信息,性能更好
  const entries = await fs.readdir(dirPath, { withFileTypes: true });
  
  for (const entry of entries) {
    if (entry.isDirectory()) {
      continue;  // 跳过子目录
    }
    
    const ext = path.extname(entry.name).toLowerCase();
    const fullPath = path.join(dirPath, entry.name);
    
    if (['.jpg', '.png', '.gif'].includes(ext)) {
      stats.images.push(fullPath);
    } else if (['.pdf', '.docx', '.txt'].includes(ext)) {
      stats.documents.push(fullPath);
    } else {
      stats.others.push(fullPath);
    }
  }
  
  return stats;
}

// 使用示例
const result = await classifyFiles('uploads');
console.log(`找到 ${result.images.length} 张图片`);
console.log(`找到 ${result.documents.length} 个文档`);

💡 小技巧

  • withFileTypes: true 比先 readdirstat 性能高很多
  • path.extname() 可以获取文件扩展名

场景 3:递归创建目录

💡 场景描述

  • 脚手架工具初始化项目时创建目录结构
  • 确保日志目录存在后再写入日志
js 复制代码
import { promises as fs } from 'node:fs';

// 创建多级目录
async function ensureDir(dirPath) {
  await fs.mkdir(dirPath, { recursive: true });
}

// 使用示例
await ensureDir('project/src/components/Button');
await ensureDir('logs/2024/01');

// 现在可以放心写入文件了
await fs.writeFile('logs/2024/01/app.log', '日志内容');

⚠️ 重点

  • recursive: true 会自动创建父目录
  • 目录已存在不会报错

场景 4:备份与日志轮转

💡 场景描述

  • 每天零点将今天的日志文件重命名为 app-2024-01-05.log
  • 拷贝重要文件做备份
js 复制代码
import { promises as fs } from 'node:fs';

// 日志轮转:重命名今天的日志
async function rotateLog() {
  const today = new Date().toISOString().split('T')[0];  // '2024-01-05'
  await fs.rename('app.log', `app-${today}.log`);
  // 创建新的空日志文件
  await fs.writeFile('app.log', '');
}

// 备份文件
async function backupFile(source) {
  const backup = `${source}.backup`;
  await fs.copyFile(source, backup);
  console.log(`已备份到: ${backup}`);
}

// 使用示例
await rotateLog();
await backupFile('config.json');

场景 5:安全的原子写入(防止文件损坏)

💡 场景描述

  • 保存重要配置文件时,防止写到一半程序崩溃导致文件损坏

🔹 为什么需要原子写入?

普通写入的问题:

js 复制代码
// ❌ 危险:如果写到一半程序崩溃,文件会损坏
await fs.writeFile('config.json', largeContent);
// 假设写到这里突然断电 → config.json 只写了一半,变成乱码!

原子写入的解决方案:

js 复制代码
import { promises as fs } from 'node:fs';
import path from 'node:path';

/**
 * 原子写入:先写临时文件,成功后再替换
 */
async function atomicWrite(filePath, content) {
  // 1. 确保目录存在
  const dir = path.dirname(filePath);
  await fs.mkdir(dir, { recursive: true });
  
  // 2. 先写到临时文件(加时间戳避免冲突)
  const tmpFile = `${filePath}.tmp-${Date.now()}`;
  await fs.writeFile(tmpFile, content);
  
  // 3. 原子替换(rename 是原子操作)
  await fs.rename(tmpFile, filePath);
  // 这一步要么成功,要么失败,不会出现"替换了一半"的情况
}

// 使用示例
const config = { version: '2.0', theme: 'dark' };
await atomicWrite('config.json', JSON.stringify(config, null, 2));

✅ 原子写入的好处

  • 写入过程中即使断电/崩溃,原文件不会损坏
  • 要么看到旧版本,要么看到新版本,绝不会是"半截"内容

⚠️ 何时必须用

  • 配置文件
  • 数据库数据
  • 用户保存的文档

场景 6:流式处理大文件

💡 场景描述

  • 复制 1GB 的视频文件
  • 逐行读取 500MB 的日志文件并分析

🔹 为什么需要流式处理?

普通方式的问题:

js 复制代码
// ❌ 危险:读取 1GB 文件会占用 1GB 内存,导致程序崩溃
const content = await fs.readFile('video.mp4');  // OOM!
await fs.writeFile('video.mp4.backup', content);

流式处理的解决方案:

js 复制代码
import { createReadStream, createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';

// ✅ 正确:逐块读写,内存占用极低(只有几 MB)
async function copyLargeFile(source, dest) {
  await pipeline(
    createReadStream(source),
    createWriteStream(dest)
  );
}

// 带进度显示
async function copyWithProgress(source, dest) {
  let processedBytes = 0;
  const readStream = createReadStream(source);
  
  readStream.on('data', (chunk) => {
    processedBytes += chunk.length;
    const mb = (processedBytes / 1024 / 1024).toFixed(2);
    console.log(`已处理: ${mb} MB`);
  });
  
  await pipeline(readStream, createWriteStream(dest));
  console.log('✅ 复制完成');
}

// 使用示例
await copyLargeFile('video.mp4', 'backup.mp4');

⚠️ 关键规则

  • 文件 > 10MB 必须使用流
  • 必须使用 pipeline(自动处理错误和背压)
  • 禁止手写 readStream.on('data') + writeStream.write()

场景 7:批量处理文件(并发控制)

💡 场景描述

  • 压缩 1000 张图片
  • 复制 5 万个文件

🔹 为什么需要并发控制?

js 复制代码
// ❌ 危险:同时打开 1000 个文件会导致资源耗尽
const files = ['img1.jpg', 'img2.jpg', /* ...1000 个 */];
await Promise.all(files.map(file => compressImage(file)));  // 崩溃!

限制并发数的解决方案:

js 复制代码
/**
 * 并发池:限制同时运行的任务数
 */
async function concurrentPool(items, limit, worker) {
  const queue = [...items];
  let running = 0;
  let index = 0;
  
  return new Promise((resolve) => {
    function runNext() {
      if (index >= items.length && running === 0) {
        resolve();
        return;
      }
      
      while (running < limit && index < items.length) {
        const item = items[index++];
        running++;
        
        worker(item).finally(() => {
          running--;
          runNext();
        });
      }
    }
    
    runNext();
  });
}

// 使用示例:限制并发为 10
const images = ['img1.jpg', 'img2.jpg', /* ...1000 个 */];
await concurrentPool(images, 10, async (file) => {
  await compressImage(file);
  console.log(`已处理: ${file}`);
});

💡 推荐并发数

  • CPU 密集任务(图片压缩):CPU 核心数(如 4 或 8)
  • IO 密集任务(文件复制):10-50

场景 8:错误处理最佳实践

💡 场景描述

  • 文件不存在时给出友好提示
  • 权限不足时引导用户检查
js 复制代码
import { promises as fs } from 'node:fs';

async function safeReadFile(filePath) {
try {
    return await fs.readFile(filePath, 'utf8');
} catch (err) {
    // 根据错误码提供友好提示
    switch (err.code) {
      case 'ENOENT':
        console.error(`❌ 文件不存在: ${filePath}`);
        console.error('请检查文件路径是否正确');
        break;
      case 'EACCES':
      case 'EPERM':
        console.error(`❌ 权限不足: ${filePath}`);
        console.error('请检查文件权限或以管理员身份运行');
        break;
      case 'EISDIR':
        console.error(`❌ 这是一个目录,不是文件: ${filePath}`);
        break;
      default:
        console.error(`❌ 未知错误: ${err.message}`);
        console.error(err.stack);  // 开发环境保留堆栈
    }
    throw err;
  }
}

// 使用示例
try {
  const content = await safeReadFile('config.json');
  console.log(content);
} catch {
  console.log('读取失败,使用默认配置');
}

常见错误码

  • ENOENT:文件/目录不存在
  • EACCES / EPERM:权限不足
  • EEXIST:文件已存在
  • EISDIR:路径是目录,不是文件
  • ENOTDIR:路径是文件,不是目录

二、path 模块 - 路径处理

2.1 为什么需要 path 模块?

❌ 错误做法:字符串拼接

js 复制代码
const filePath = root + '/' + folder + '/' + filename;  // 在 Windows 上会出错!

✅ 正确做法:使用 path 模块

js 复制代码
import path from 'node:path';
const filePath = path.join(root, folder, filename);  // 跨平台兼容

原因

  • Windows 用 \(反斜杠):C:\Users\name\file.txt
  • macOS/Linux 用 /(正斜杠):/home/name/file.txt
  • path 模块会自动处理这些差异

2.2 核心 API 详解

path.join() - 拼接路径

作用:将多个路径片段拼接成一个路径

js 复制代码
import path from 'node:path';

const projectRoot = '/Users/me/project';
const logDir = path.join(projectRoot, 'logs');
// 结果: '/Users/me/project/logs'

const logFile = path.join(logDir, '2024', 'app.log');
// 结果: '/Users/me/project/logs/2024/app.log'

// 自动处理多余的斜杠
path.join('a', 'b');      // 'a/b'
path.join('a/', '/b');    // 'a/b'(自动清理)
path.join('a', '.', 'b'); // 'a/b'(处理 .)

path.resolve() - 解析为绝对路径

作用:将相对路径转为绝对路径

js 复制代码
import path from 'node:path';

// 假设当前目录是 /Users/me/project

// 相对路径 → 绝对路径
const abs = path.resolve('logs', 'app.log');
// 结果: '/Users/me/project/logs/app.log'

// 如果遇到绝对路径,会从那里重新开始
const abs2 = path.resolve('a', '/b', 'c');
// 结果: '/b/c'(从 /b 重新开始)

💡 推荐用法

js 复制代码
// 项目中定位文件的最佳实践
import path from 'node:path';
import { fileURLToPath } from 'node:url';

// ESM 模块中获取当前文件所在目录
const __dirname = path.dirname(fileURLToPath(import.meta.url));

// 定位项目根目录的配置文件
const configPath = path.resolve(__dirname, '../config/app.json');

path.dirname() / path.basename() / path.extname() - 拆分路径

作用:提取路径的各个部分

js 复制代码
import path from 'node:path';

const filePath = '/Users/me/project/src/app.js';

path.dirname(filePath);   // '/Users/me/project/src'(目录部分)
path.basename(filePath);  // 'app.js'(文件名部分)
path.extname(filePath);   // '.js'(扩展名部分)

// basename 可以去掉扩展名
path.basename(filePath, '.js');  // 'app'

💡 实用案例:根据文件类型分类

js 复制代码
import path from 'node:path';

function getFileType(filename) {
  const ext = path.extname(filename).toLowerCase();
  
  if (['.jpg', '.png', '.gif'].includes(ext)) return '图片';
  if (['.mp4', '.avi'].includes(ext)) return '视频';
  if (['.pdf', '.docx'].includes(ext)) return '文档';
  return '其他';
}

console.log(getFileType('avatar.png'));  // '图片'
console.log(getFileType('video.mp4'));   // '视频'

path.parse() / path.format() - 结构化处理

作用:将路径拆解为对象,或从对象组装路径

js 复制代码
import path from 'node:path';

// 拆解路径
const parsed = path.parse('/Users/me/project/app.js');
console.log(parsed);
// {
//   root: '/',
//   dir: '/Users/me/project',
//   base: 'app.js',
//   name: 'app',
//   ext: '.js'
// }

// 修改后重新组装
const newPath = path.format({
  ...parsed,
  name: 'server',  // 改文件名
  ext: '.ts'       // 改扩展名
});
console.log(newPath);  // '/Users/me/project/server.ts'

💡 实用案例:批量重命名

js 复制代码
import path from 'node:path';
import { promises as fs } from 'node:fs';

// 将所有 .jpg 改为 .png
async function renameExtension(dir, oldExt, newExt) {
  const files = await fs.readdir(dir);
  
  for (const file of files) {
    if (path.extname(file) === oldExt) {
      const parsed = path.parse(file);
      const newName = path.format({ ...parsed, ext: newExt });
      
      await fs.rename(
        path.join(dir, file),
        path.join(dir, newName)
      );
      console.log(`${file} → ${newName}`);
    }
  }
}

await renameExtension('images', '.jpg', '.png');

path.normalize() - 清理路径

作用:清理路径中的多余部分

js 复制代码
import path from 'node:path';

path.normalize('/a/b/c/../d');      // '/a/b/d'(处理 ..)
path.normalize('/a//b///c');        // '/a/b/c'(去掉多余斜杠)
path.normalize('./src/../dist');    // 'dist'

⚠️ 注意normalize 只是字符串处理,不做安全检查!


path.relative() - 计算相对路径

作用:计算从一个路径到另一个路径的相对路径

js 复制代码
import path from 'node:path';

const from = '/Users/me/project/src';
const to = '/Users/me/project/dist/index.js';

const rel = path.relative(from, to);
// 结果: '../dist/index.js'

💡 实用案例:生成 import 语句

js 复制代码
import path from 'node:path';

function generateImport(fromFile, toFile) {
  const fromDir = path.dirname(fromFile);
  const rel = path.relative(fromDir, toFile);
  return `import something from './${rel}';`;
}

const statement = generateImport(
  '/project/src/app.js',
  '/project/src/utils/helper.js'
);
console.log(statement);
// import something from './utils/helper.js';

2.3 安全拼接 - 防止目录穿越攻击

🔹 什么是目录穿越攻击?(复习)

黑客通过 ../../ 来访问不该访问的文件:

js 复制代码
// 你的网站允许下载 /uploads 目录下的文件
const uploadDir = '/var/www/uploads';
const userInput = '../../etc/passwd';  // 黑客输入

// ❌ 危险:直接拼接会被攻击
const filePath = path.join(uploadDir, userInput);
// 结果: '/var/etc/passwd'(逃出了 uploads 目录!)

await fs.readFile(filePath);  // 黑客成功读取系统密码文件!

✅ 安全解决方案:检查最终路径

js 复制代码
import path from 'node:path';

/**
 * 安全拼接:确保最终路径不会逃出根目录
 */
function safeJoin(rootDir, userInput) {
  // 1. 拼接路径
  const targetPath = path.resolve(rootDir, userInput);
  
  // 2. 规范化根目录(加上斜杠)
  const normalizedRoot = path.resolve(rootDir) + path.sep;
  
  // 3. 检查目标路径是否以根目录开头
  if (!targetPath.startsWith(normalizedRoot)) {
    throw new Error('⚠️ 检测到目录穿越攻击,拒绝访问!');
  }
  
  return targetPath;
}

// 使用示例
const uploadDir = '/var/www/uploads';

try {
  // ✅ 正常访问
  const safe1 = safeJoin(uploadDir, 'avatar.png');
  console.log(safe1);  // '/var/www/uploads/avatar.png'
  
  // ❌ 攻击被拦截
  const safe2 = safeJoin(uploadDir, '../../etc/passwd');
} catch (err) {
  console.error(err.message);  // '⚠️ 检测到目录穿越攻击'
}

⚠️ 重要规则

  • 所有用户输入的路径必须使用 safeJoin 检查
  • 包括:文件上传、下载、静态文件服务等
  • 这是防止路径穿越的最核心方法

2.4 URL 与路径互转(ESM 模块必备)

💡 问题背景

type: module 的 ESM 项目中,__dirname__filename 不可用:

js 复制代码
// ❌ ESM 模块中会报错
console.log(__dirname);  // ReferenceError: __dirname is not defined

✅ 解决方案

js 复制代码
import path from 'node:path';
import { fileURLToPath } from 'node:url';

// 将当前模块的 URL 转为文件路径
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

console.log(__dirname);  // '/Users/me/project/src'

// 现在可以定位项目文件了
const configPath = path.resolve(__dirname, '../config/app.json');

三、速查清单

fs 模块常用 API

功能 推荐 API 说明
读小文件 readFile(path, 'utf8') < 10MB
写小文件 writeFile(path, data) 覆盖写
追加内容 appendFile(path, data) 日志场景
读大文件 createReadStream(path) + pipeline > 10MB
写大文件 createWriteStream(path) + pipeline > 10MB
创建目录 mkdir(path, { recursive: true }) 多级目录
读取目录 readdir(path, { withFileTypes: true }) 获取类型
复制文件 copyFile(src, dest)
移动/重命名 rename(old, new)
删除文件 rm(path, { force: true })
删除目录 rm(path, { recursive: true }) 危险操作
获取信息 stat(path) 大小、时间等
检查权限 access(path, constants.R_OK)

path 模块常用 API

功能 推荐 API 示例
拼接路径 path.join(a, b, c) 'a/b/c'
绝对路径 path.resolve(a, b) /abs/path/a/b
提取目录 path.dirname(p) '/a/b'
提取文件名 path.basename(p) 'file.txt'
提取扩展名 path.extname(p) '.txt'
计算相对路径 path.relative(from, to) '../other'
拆解路径 path.parse(p) 返回对象
组装路径 path.format(obj) 返回字符串

四、最佳实践总结

✅ 推荐做法

  1. 异步优先 :使用 fs.promises,避免同步方法(除了程序启动时)
  2. 路径安全 :用 path.join/resolve 拼接,禁止字符串拼接
  3. 原子写入:重要文件用"临时文件 + rename"模式
  4. 流式处理 :大文件(> 10MB)必须用 Stream + pipeline
  5. 并发控制:批量操作限制并发数(10-50)
  6. 安全检查 :用户输入路径必须用 safeJoin 验证
  7. 错误处理 :根据 err.code 提供友好提示

❌ 禁止做法

  1. ❌ 在服务器代码中使用同步方法(readFileSync 等)
  2. ❌ 字符串拼接路径:root + '/' + file
  3. ❌ 用 readFile 读取大文件(内存溢出)
  4. ❌ 手写流处理逻辑(用 pipeline
  5. ❌ 批量操作不限制并发(资源耗尽)
  6. ❌ 直接使用用户输入的路径(安全漏洞)
  7. ❌ 忽略错误码(用户体验差)

五、学习路线建议

第一步:基础练习(1-2 天)

  • 读写配置文件(JSON)
  • 遍历目录并统计文件数量
  • 练习 path.joinpath.resolve

第二步:进阶实战(3-5 天)

  • 实现原子写入函数
  • 用流复制大文件
  • 实现批量图片处理(带并发控制)

第三步:安全加固(2-3 天)

  • 实现 safeJoin 函数
  • 完善错误处理
  • 学习文件权限控制

第四步:项目实践

  • 在项目中创建 utils/fs.js 工具模块
  • 封装常用函数:safeJoinatomicWriteconcurrentPool
  • 统一项目的文件操作规范

六、常见问题 FAQ

Q1:什么时候可以用同步方法?

A:只有以下两种情况:

  • 程序启动时读取配置(只执行一次)
  • 简单的命令行工具脚本

其他情况(特别是服务器环境)必须用异步方法。


Q2:如何判断应该用流还是 readFile?

A:看文件大小:

  • < 10MB:用 readFile/writeFile
  • > 10MB:用 createReadStream/createWriteStream
  • 不确定大小:用流更安全

Q3:删除目录时 rm 和 unlink 有什么区别?

A:

  • unlink:只能删除文件
  • rm:可以删除文件和目录,支持 recursive 选项

推荐统一使用 rm(加 force: true 避免报错)。


Q4:path.join 和 path.resolve 有什么区别?

A:

  • path.join:简单拼接,不保证返回绝对路径
  • path.resolve:解析为绝对路径,基于当前工作目录

项目内定位文件推荐用 resolve


Q5:如何在 ESM 模块中获取 __dirname?

A:

js 复制代码
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

🎉 恭喜!你已经掌握了 Node.js 文件系统操作的核心知识。

💡 建议:将本文中的 safeJoinatomicWriteconcurrentPool 等函数保存到你的代码片段库,方便随时使用!

相关推荐
啊花是条龙1 天前
《产品经理说“Tool 分组要一条会渐变的彩虹轴,还要能 zoom!”——我 3 步把它拆成 1024 个像素》
前端·javascript·echarts
C_心欲无痕1 天前
css - 使用@media print:打印完美网页
前端·css
青茶3601 天前
【js教程】如何用jq的js方法获取url链接上的参数值?
开发语言·前端·javascript
脩衜者1 天前
极其灵活且敏捷的WPF组态控件ConPipe 2026
前端·物联网·ui·wpf
Mike_jia1 天前
Dockge:轻量开源的 Docker 编排革命,让容器管理回归优雅
前端
GISer_Jing1 天前
前端GEO优化:AI时代的SEO新战场
前端·人工智能
没想好d1 天前
通用管理后台组件库-4-消息组件开发
前端
文艺理科生1 天前
Google A2UI 解读:当 AI 不再只是陪聊,而是开始画界面
前端·vue.js·人工智能
晴栀ay1 天前
React性能优化三剑客:useMemo、memo与useCallback
前端·javascript·react.js