del、fs-extra、remove 和 rimraf 都是 Node.js 生态中用于删除文件或目录的工具,但它们的定位和维护状态截然不同。rimraf 是底层递归删除的标准实现,del 是基于 rimraf 的现代 Promise 封装,fs-extra 提供了增强版的文件系统 API(包含删除功能),而 remove 包已过时不再推荐使用。
在 Node.js 开发中,清理构建产物、重置临时目录或删除旧日志是常见需求。虽然原生 fs 模块提供了 unlink 和 rmdir,但处理递归删除和通配符匹配非常麻烦。del、fs-extra、remove 和 rimraf 试图解决这些问题,但它们的实现方式和适用场景有很大区别。
不同的包采用了不同的异步处理模式,这直接影响代码的可读性和维护成本。
del 基于 Promise 设计,天然支持 async/await,符合现代 JavaScript 标准。
// del: 原生 Promise 支持
import { deleteAsync } from 'del';
await deleteAsync(['dist/*.js', 'dist/*.css']);
// 返回已删除路径的数组
fs-extra 同时提供回调和 Promise 风格,API 与原生 fs 模块高度一致。
// fs-extra: 支持 Promise 和 Callback
import fs from 'fs-extra';
// Promise 风格
await fs.remove('dist');
// 回调风格
fs.remove('dist', err => { /*...*/ });
rimraf 早期版本主要基于回调,v5+ 版本转向 ESM 和 Promise,但底层逻辑依然保持简洁。
// rimraf: 现代版本支持 Promise
import { rimraf } from 'rimraf';
await rimraf('dist');
// 专注于递归删除,不返回复杂结果
remove 包采用老旧的回调模式,不支持 Promise,需要手动封装才能在现代代码中使用。
// remove: 仅支持回调 (已过时)
import remove from 'remove';
remove('dist', function (err) {
if (err) throw err;
console.log('deleted');
});
处理批量文件删除时,是否支持 Glob 模式(如 *.js)是关键差异点。
del 内置了 globby,直接支持通配符匹配,无需额外配置。
// del: 内置 Glob 支持
await deleteAsync(['tmp/**/*.log', '!tmp/important.log']);
// 支持排除模式,非常灵活
rimraf 支持简单的通配符,但主要设计用于删除单个路径或简单模式。
// rimraf: 基础 Glob 支持
await rimraf('tmp/*.log');
// 复杂模式可能需要配合 glob 包使用
fs-extra 本身不支持通配符,需要配合 glob 包手动实现。
// fs-extra: 需配合 glob 使用
import glob from 'glob';
import fs from 'fs-extra';
const files = await glob('tmp/*.log');
await Promise.all(files.map(f => fs.remove(f)));
remove 不支持通配符,只能删除指定路径的文件或目录。
// remove: 无 Glob 支持
remove('tmp/file.log', cb);
// 批量删除需要自己写循环
选择库时,维护状态直接关系到项目的长期安全和技术债务。
remove 包已经多年未更新,社区普遍认为它已过时。它的功能完全被 fs-extra 的 remove 方法覆盖。在新项目中引入它是一个明显的技术债务信号。
rimraf 是行业标准,被无数核心工具依赖。虽然 API 简单,但非常稳定。注意 v5 版本之后仅支持 ESM 模块。
del 保持活跃更新,专门针对前端构建场景优化。它处理了 Windows 路径锁定等边缘情况,比直接调用 rimraf 更省心。
fs-extra 是文件系统操作的瑞士军刀,维护非常积极。如果你的项目已经用它来做 copy 或 move,那么用它来做 remove 是自然的选择。
你需要在构建前清理 dist 目录,并删除所有临时日志。
del// 构建脚本示例
import { deleteAsync } from 'del';
await deleteAsync(['dist', 'tmp/*.log']);
你正在编写一个内部工具库,需要统一的文件操作 API。
fs-extra// 工具库示例
import fs from 'fs-extra';
await fs.remove('/tmp/cache');
你正在开发一个需要极致兼容性的命令行工具。
rimraf// CLI 工具示例
import { rimraf } from 'rimraf';
await rimraf(process.argv[2]);
你正在维护一个 5 年前的老项目。
remove 包,建议计划迁移到 fs-extra。| 特性 | del | fs-extra | rimraf | remove |
|---|---|---|---|---|
| 异步模式 | Promise | Promise / Callback | Promise / Callback | Callback |
| Glob 支持 | ✅ 内置 | ❌ 需配合 glob | ⚠️ 基础支持 | ❌ 无 |
| 维护状态 | 🟢 活跃 | 🟢 活跃 | 🟢 活跃 | 🔴 过时 |
| 主要用途 | 构建清理 | 通用文件操作 | 底层递归删除 | 已淘汰 |
| 模块类型 | ESM | CJS / ESM | ESM (v5+) | CJS |
del 是前端构建任务的首选 🧹 — 它解决了通配符和 Promise 的痛点,让清理代码变得非常干净。
fs-extra 是通用文件操作的最佳拍伴 🤝 — 如果你已经在使用它,就不要引入额外的删除库,保持一致性更重要。
rimraf 是底层实现的基石 🪨 — 当你需要最纯粹的递归删除功能,或者作为库的依赖时,它是最可靠的选择。
remove 包应该被移除 🗑️ — 它不再符合现代开发标准,迁移成本很低,但收益很高。
最终建议:在现代前端工程中,优先使用 del 处理构建清理,使用 fs-extra 处理应用内的文件操作。避免引入过时的 remove 包,除非你正在维护无法修改的遗留代码。
选择 del 如果你正在编写构建脚本或需要处理通配符模式(如 dist/*.js)。它基于 Promise,支持异步操作,并且默认忽略不存在的文件,非常适合现代前端工作流。
选择 fs-extra 如果你的项目已经依赖它来处理其他文件操作(如拷贝、移动)。它的 remove 方法 API 一致性好,支持同步和异步,且维护活跃。
不要在新技术栈中使用 remove 包。它已多年未更新,缺乏 Promise 支持,且功能已被 fs-extra 和 rimraf 完全覆盖,存在维护风险。
选择 rimraf 如果你需要最底层的递归删除能力,或者在编写 CLI 工具。它是许多其他工具(包括 del)的底层依赖,兼容性极强,但 API 相对基础。
Delete files and directories using globs
Similar to rimraf, but with a Promise API and support for multiple files and globbing. It also protects you against deleting the current working directory and above.
npm install del
import {deleteAsync} from 'del';
const deletedFilePaths = await deleteAsync(['temp/*.js', '!temp/unicorn.js']);
const deletedDirectoryPaths = await deleteAsync(['temp', 'public']);
console.log('Deleted files:\n', deletedFilePaths.join('\n'));
console.log('\n\n');
console.log('Deleted directories:\n', deletedDirectoryPaths.join('\n'));
The glob pattern ** matches all children and the parent.
So this won't work:
deleteSync(['public/assets/**', '!public/assets/goat.png']);
You have to explicitly ignore the parent directories too:
deleteSync(['public/assets/**', '!public/assets', '!public/assets/goat.png']);
To delete all subdirectories inside public/, you can do:
deleteSync(['public/*/']);
Suggestions on how to improve this welcome!
Note that glob patterns can only contain forward-slashes, not backward-slashes. Windows file paths can use backward-slashes as long as the path does not contain any glob-like characters, otherwise use path.posix.join() instead of path.join().
Returns Promise<string[]> with the deleted paths.
Returns string[] with the deleted paths.
Type: string | string[]
See the supported glob patterns.
Type: object
You can specify any of the globby options in addition to the below options. In contrast to the globby defaults, expandDirectories, onlyFiles, and followSymbolicLinks are false by default.
Type: boolean
Default: false
Allow deleting the current working directory and outside.
Type: boolean
Default: false
See what would be deleted.
import {deleteAsync} from 'del';
const deletedPaths = await deleteAsync(['temp/*.js'], {dryRun: true});
console.log('Files and directories that would be deleted:\n', deletedPaths.join('\n'));
Type: boolean
Default: false
Allow patterns to match files/folders that start with a period (.).
This option is passed through to fast-glob.
Note that an explicit dot in a portion of the pattern will always match dot files.
Example
directory/
├── .editorconfig
└── package.json
import {deleteSync} from 'del';
deleteSync('*', {dot: false});
//=> ['package.json']
deleteSync('*', {dot: true});
//=> ['.editorconfig', 'package.json']
Type: number
Default: Infinity
Minimum: 1
Concurrency limit.
Type: (progress: ProgressData) => void
Called after each file or directory is deleted.
import {deleteAsync} from 'del';
await deleteAsync(patterns, {
onProgress: progress => {
// …
}});
{
totalCount: number,
deletedCount: number,
percent: number,
path?: string
}
percent is a value between 0 and 1path is the absolute path of the deleted file or directory. It will not be present if nothing was deleted.See del-cli for a CLI for this module and trash-cli for a safe version that is suitable for running by hand.