← process.getegid() process.hrtime() →

Node.js fs.rmdir()专业教程:从废弃API到fs.rm()迁移的工程决策

著
原创 2026-10-18 Node.js 已有人查阅

一、先认清现状:这个API已经不再是“递归删除”的入口

fs.rmdir()在Node.js中的原始职责是删除一个目录,底层对应POSIX的rmdir(2)。它的语义从一开始就很明确:只能删除空目录。非空目录会返回ENOTEMPTY(POSIX)或EEXIST(Windows的某些场景)。

Node.js曾在一个阶段内为fs.rmdir()增加了recursive选项,允许递归删除非空目录。但这个选项随后被标记为废弃,并最终移除。根据Node.js官方废弃文档DEP0147的说明,fs.rmdir、fs.rmdirSync和fs.promises.rmdir的recursive选项已被移除,替代方案是fs.rm(path,{recursive:true,force:true})及其同步和Promise版本。

当前版本的fs.rmdir(path[,options],callback)签名中,options参数仍然被接受以保持向后兼容,但没有任何可用选项。文档明确写道:“目前没有可用的选项。以前有recursive、maxBusyTries和emfileWait的选项,但它们已被弃用并移除。”

这意味着围绕fs.rmdir()写教程或写代码,核心议题不是“如何使用recursive选项”,而是理解它现在能做什么、不能做什么,以及什么场景下应当直接选用fs.rm()。

二、版本差异:recursive选项的生命周期与迁移

recursive选项在Node.jsv12.10.0中引入,使得fs.rmdirSync(dir,{recursive:true})可以删除非空目录。但后续的版本迭代中,这个用法被标记为运行时废弃(RuntimeDeprecation),并最终走向寿命终止(End-of-Life)。

DEP0147的类型是End-of-Life,意味着该功能已经不再工作。在受影响的Node.js版本中,向fs.rmdir()传入{recursive:true}不会执行递归删除,而是会抛出错误或产生不符合预期的行为。官方提供了自动迁移工具:npxcodemod@latest@nodejs/rmdir。

从Node.jsv14到v16的迁移指南中,也记录了对应的codemod用法:将fs.rmdir(path,{recursive:true},callback)替换为fs.rm(path,{recursive:true,force:true},callback),同步版本和Promise版本同理。

关于PHP侧的对照:PHP的rmdir()函数从PHP4起就存在,语义始终是“删除空目录”,没有内置的递归删除选项。PHP开发者若需递归删除,通常需要自行编写递归函数,或使用RecursiveDirectoryIterator配合RecursiveIteratorIterator先删除子项再删除父目录。这与Node.js早期fs.rmdir的recursive选项思路不同:PHP从未在rmdir上附加递归能力,而Node.js则是先附加后移除。如果从PHP迁移到Node.js,对“删除目录”的预期需要调整:在Node.js中,递归删除的入口是fs.rm(),不是fs.rmdir()。

三、fs.rmdir()当前的实际行为与错误码

删除空目录

const fs = require('fs');

fs.rmdir('empty_dir', (err) => {
  if (err) {
    console.error(err.code);
    return;
  }
  console.log('空目录已删除');
});

删除非空目录

fs.rmdir('non_empty_dir', (err) => {
  if (err) {
    console.error(err.code); // 'ENOTEMPTY'(POSIX)或 'EEXIST'(Windows 部分场景)
    return;
  }
});

ENOTEMPTY是当前fs.rmdir()在遇到非空目录时的预期错误码。原始资料示例中对ENOTEMPTY的判断是准确的,但需要理解它的前提:在recursive选项已移除的版本中,非空目录必然触发此错误,不存在“重试后成功”的可能。

对文件路径调用

文档明确指出:在文件(而不是目录)上使用fs.rmdir(),Windows上导致ENOENT,POSIX系统上导致ENOTDIR。这是一个跨平台行为差异,代码中若对错误码做精确分支,需要同时覆盖两种可能。

路径不存在

返回ENOENT。fs.rmdir()没有force选项,不会静默忽略不存在的路径。这与fs.rm({force:true})的行为形成对比。

四、与fs.rm()的职责对比

维度 fs.rmdir() fs.rm()
删除空目录 支持 支持
删除非空目录 不支持(recursive已移除) 通过{recursive:true}支持
路径不存在时静默 不支持 通过{force:true}支持
对文件路径调用 POSIXENOTDIR,WindowsENOENT 删除文件
当前状态 仍可用,但功能收窄为“删除空目录” 推荐用于文件与目录的统一删除

fs.rm()的设计目标之一是提供类似rm-rf的行为。文档中的表述是:要获得类似于rm-rfUnix命令的行为,请使用fs.rm(),并设置选项{recursive:true,force:true}。

一个实际的迁移示例:

// 旧代码(recursive 选项已移除,不能继续使用)
fs.rmdir(dir, { recursive: true }, callback);

// 新代码
fs.rm(dir, { recursive: true, force: true }, callback);

force:true的作用是忽略路径不存在时的异常。是否使用force取决于业务语义:如果“目录不存在”应当被视为错误,则省略force;如果删除操作的意图是“确保该路径最终不存在”,则使用force可以简化调用方的错误处理。

五、单元测试与边界场景

测试空目录删除成功

const fs = require('fs');
const os = require('os');
const path = require('path');

const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rmdir-test-'));
fs.rmdir(testDir, (err) => {
  console.assert(err === null, '空目录删除不应报错');
  console.assert(!fs.existsSync(testDir), '目录应已被删除');
});

测试非空目录触发ENOTEMPTY

const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rmdir-nonempty-'));
fs.writeFileSync(path.join(dir, 'file.txt'), 'data');

fs.rmdir(dir, (err) => {
  console.assert(err !== null, '非空目录应报错');
  console.assert(err.code === 'ENOTEMPTY' || err.code === 'EEXIST', '错误码应符合平台预期');
  // 清理
  fs.rmSync(dir, { recursive: true, force: true });
});

这个测试的关键在于不要假设只有一种错误码。POSIX与Windows在非空目录上的错误码存在差异,测试断言应当使用逻辑或覆盖两种可能。

测试对文件路径调用

const filePath = path.join(os.tmpdir(), 'rmdir-file-test.txt');
fs.writeFileSync(filePath, 'x');

fs.rmdir(filePath, (err) => {
  const expected = process.platform === 'win32' ? 'ENOENT' : 'ENOTDIR';
  console.assert(err && err.code === expected, `应返回 ${expected}`);
  fs.unlinkSync(filePath);
});

边界:传入不存在的路径

返回ENOENT,不会静默成功。如果调用方希望在“目录本就不存在”时也视为成功,需要在回调中显式判断错误码并忽略,或者改用fs.rm({force:true})。

六、常见误区速查

误区一:fs.rmdir()传{recursive:true}仍然可以递归删除。recursive选项已被移除。传入该选项不会产生递归效果。当前版本的文档写明options“未被使用”。需要递归删除时使用fs.rm()。

误区二:fs.rmdir()的错误码在所有平台上一致。对文件路径调用时,POSIX返回ENOTDIR,Windows返回ENOENT。非空目录在Windows的部分场景中也可能返回EEXIST而非ENOTEMPTY。跨平台代码的错误分支需要覆盖这些差异。

误区三:fs.rmdir()可以替代fs.rm()做通用删除。它只处理空目录。文件删除用fs.unlink(),非空目录用fs.rm({recursive:true}),空目录可以用fs.rmdir()或fs.rm()。选择哪一个取决于你对“空目录”这一前提的确定程度。

误区四:原始资料中“fs.rmdir()可以递归删除”的描述仍然成立。这是过时信息。原始资料示例二的逻辑建立在一个已经移除的功能之上。以当前Node.js文档为准,fs.rmdir()不具备递归能力。

误区五:fs.rmdir()的callback会收到除err之外的参数。文档写明:“除了可能的异常外,没有其他参数会传递给完成回调。”回调签名为(err)=>void,没有第二个参数。

七、项目中的实际选择:什么时候仍然用fs.rmdir()

在一个已经明确知道目录为空、且代码运行在受控环境中的场景,fs.rmdir()仍然是一个语义清晰的调用。例如,在清理流程中,子项已经被逐个删除,之后一步删除空壳目录:

async function removeEmptyShell(dir) {
  const entries = await fs.promises.readdir(dir);
  if (entries.length > 0) {
    throw new Error('目录非空,终止删除');
  }
  await fs.promises.rmdir(dir);
}

这种用法保留了fs.rmdir()的核心价值:它在遇到非空目录时拒绝执行,而不是像fs.rm({recursive:true})那样继续深入删除。如果业务逻辑要求“确认目录已空才允许删除”,fs.rmdir()的非空目录错误恰好充当了一道守卫。

反过来,如果删除的意图是“无论目录里有什么都清掉”,直接使用fs.rm({recursive:true,force:true})更直接,也避免了先readdir再判断的额外步骤。

八、练习与思考

练习一:在一个临时目录中创建一个空子目录和一个包含文件的子目录。分别对两者调用fs.rmdir(),记录错误码。将非空目录的错误码与fs.rmSync(dir,{recursive:true,force:true})的执行结果对比,说明为什么后者不需要ENOTEMPTY的错误处理。

练习二:写一个函数removeDirIfEmpty(dirPath),仅当目录存在且为空时删除它,其他情况返回false。要求处理ENOENT(目录不存在)和ENOTEMPTY(目录非空)两种错误,不抛出异常。用fs.rmdir()实现,然后思考:如果改用fs.rm(),能否保持“仅删除空目录”的语义?需要哪些额外检查?

练习三:阅读你当前项目中的目录删除代码,找出所有fs.rmdir调用。判断每一处是否仍然假设recursive选项可用。如果存在这种假设,使用官方codemod或手动替换为fs.rm(),并补充针对空目录和非空目录的回归测试。

九、延伸阅读与参考文献

← process.getegid() process.hrtime() →
分享笔记 (共有 篇笔记)
验证码:
微信公众号