fs 是 Node.js 内置的文件系统模块,提供基础的文件和目录操作能力。fs-extra 在 fs 的基础上扩展了大量实用方法(如递归复制、删除目录、JSON 读写等),并原生支持 Promise。fs-extra-promise 曾是对 fs-extra 的 Promise 封装,但已被官方废弃,因其功能已被 fs-extra 内置支持所覆盖。这三个包都用于 Node.js 环境下的文件系统操作,但在 API 设计、功能完整性和维护状态上有显著差异。
在 Node.js 开发中,文件系统(File System)操作是基础但关键的能力。fs、fs-extra 和 fs-extra-promise 三个包都用于处理文件读写、目录管理等任务,但它们在 API 设计、异步支持和开发体验上有显著差异。本文将从工程实践角度深入比较三者,帮助你做出合理的技术选型。
fs 是 Node.js 内置模块,提供最原始的文件系统操作接口。它同时支持回调风格和 Promise 风格(通过 fs.promises 子模块)。
// fs: 回调风格
const fs = require('fs');
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
// fs: Promise 风格(Node.js 10+)
const { promises: fsp } = require('fs');
async function read() {
const data = await fsp.readFile('file.txt', 'utf8');
console.log(data);
}
fs-extra 在 fs 基础上扩展了大量实用方法(如 copy, remove, ensureDir 等),并原生支持 Promise,无需额外导入子模块。
// fs-extra: 原生 Promise 支持
const fse = require('fs-extra');
async function copyAndRead() {
await fse.copy('src', 'dest');
const data = await fse.readFile('dest/file.txt', 'utf8');
console.log(data);
}
fs-extra-promise 曾经是对 fs-extra 的简单封装,为其添加 Promise 支持。但根据其 npm 页面 和 GitHub 仓库信息,该包已被官方标记为废弃(deprecated),不再维护。作者明确建议直接使用 fs-extra,因为后者已内置完整的 Promise 支持。
⚠️ 重要提示:
fs-extra-promise不应再用于新项目。继续使用它会引入不必要的依赖,并可能错过fs-extra的安全更新和性能改进。
fs:需显式使用 fs.promises 才能获得 Promise API。若直接使用 fs.readFile 等方法,返回的是回调风格,容易导致“回调地狱”或需要手动 util.promisify。// fs: 需要区分使用方式
const fs = require('fs');
const fsp = fs.promises; // 必须这样用才能获得 Promise
// 错误示例:fs.readFile 不返回 Promise
// const data = await fs.readFile('file.txt'); // TypeError!
fs-extra:所有方法默认返回 Promise,同时保留回调风格(通过可选的 callback 参数)。这种设计兼顾了向后兼容性和现代开发习惯。// fs-extra: 统一的 Promise-first API
const fse = require('fs-extra');
// Promise 风格
await fse.ensureDir('tmp');
// 也支持回调(但不推荐)
fse.ensureDir('tmp', (err) => { /*...*/ });
fs-extra-promise:作为历史产物,其 Promise 封装已被上游吸收。使用它相当于多了一层无意义的包装,且无法享受 fs-extra 后续新增的功能(如 move、emptyDir 等)。fs 仅提供 POSIX 标准的文件操作,而 fs-extra 添加了大量开发者日常需要的高级功能:
| 功能 | fs | fs-extra | 说明 |
|---|---|---|---|
| 递归创建目录 | ❌ | ✅ ensureDir | 自动创建父目录 |
| 递归删除目录 | ❌ | ✅ remove | 删除非空目录 |
| 复制文件/目录 | ❌ | ✅ copy | 支持跨设备复制 |
| 移动/重命名 | ⚠️ | ✅ move | fs.rename 不能跨设备 |
| 读取 JSON 文件 | ❌ | ✅ readJson | 自动解析 JSON |
| 写入 JSON 文件 | ❌ | ✅ writeJson | 自动序列化 + 格式化 |
// fs-extra: 高级功能示例
const fse = require('fs-extra');
// 一键创建嵌套目录
await fse.ensureDir('project/src/utils');
// 安全复制整个目录
await fse.copy('old-project', 'new-project');
// 读写 JSON 无需手动 parse/stringify
await fse.writeJson('config.json', { port: 3000 });
const config = await fse.readJson('config.json');
相比之下,fs 要实现类似功能需组合多个低级操作,代码冗长且易错。例如,递归删除目录需手动遍历文件树;复制目录需递归创建目标路径并逐个复制文件。
fs:错误通过回调的第一个参数或 Promise reject 传递,符合 Node.js 标准。但缺乏对常见场景的优化(如“文件不存在”需手动检查 err.code === 'ENOENT')。
fs-extra:继承 fs 的错误模型,但部分方法提供更友好的行为。例如 ensureDir 在目录已存在时不会报错,而 fs.mkdir 会抛出 EEXIST。
fs-extra-promise:错误处理与 fs-extra 相同,但由于已废弃,无法获得后续改进。
此外,fs-extra 提供了完善的 TypeScript 类型定义,而 fs 作为内置模块也有良好支持。fs-extra-promise 的类型定义则可能滞后或缺失。
fs:零依赖,随 Node.js 运行时自带,版本与 Node.js 绑定。fs-extra:轻量级(仅依赖少数工具函数),积极维护,定期发布新版本修复问题和增加功能。fs-extra-promise:已废弃,最后更新于 2016 年,存在安全风险且无社区支持。| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 简单脚本、最小化依赖 | fs(配合 fs.promises) | 无需额外安装,适合一次性任务 |
| 项目开发、需要高级功能 | fs-extra | API 丰富、Promise 原生支持、活跃维护 |
| 新项目 | 绝对不要用 fs-extra-promise | 已废弃,功能被 fs-extra 完全覆盖 |
fs-extra:除非你有严格的零依赖要求,否则 fs-extra 的开发效率提升远超其微小的体积成本。fs-extra 或 fs.promises,不要混用回调和 Promise 风格。fs-extra-promise,应尽快替换为 fs-extra,只需修改 import 语句即可(API 完全兼容)。// 迁移示例:从 fs-extra-promise 到 fs-extra
// 旧代码
// const fse = require('fs-extra-promise');
// 新代码
const fse = require('fs-extra'); // 其他代码无需改动
fs 是基石,fs-extra 是现代化的增强版,而 fs-extra-promise 已成为历史。对于专业前端开发者(尤其是涉及 Node.js 工具链开发时),fs-extra 应作为默认选择——它用极小的成本换取了巨大的开发体验提升和代码健壮性。记住:不要为已解决的问题重复造轮子,更不要使用已被废弃的解决方案。
选择 fs-extra 作为绝大多数项目的默认方案。它提供丰富的高级功能(如 copy、remove、ensureDir、readJson),原生 Promise 支持,且积极维护。能显著减少样板代码,提升开发体验和代码健壮性,尤其适合构建工具、脚手架或任何涉及复杂文件操作的应用。
选择 fs 仅当你需要最小化依赖(如编写轻量级 CLI 工具)且仅使用基础文件操作(读/写/重命名)。必须显式使用 fs.promises 子模块才能获得 Promise 支持,否则需处理回调。适合对依赖数量极度敏感的场景,但会牺牲开发效率。
不要在新项目中使用 fs-extra-promise。该包已被官方废弃,其功能完全被 fs-extra 内置的 Promise API 覆盖。继续使用会引入不必要的依赖、安全风险和维护负担。现有项目应尽快迁移到 fs-extra。
fs-extra adds file system methods that aren't included in the native fs module and adds promise support to the fs methods. It also uses graceful-fs to prevent EMFILE errors. It should be a drop in replacement for fs.
I got tired of including mkdirp, rimraf, and ncp in most of my projects.
npm install fs-extra
fs-extra is a drop in replacement for native fs. All methods in fs are attached to fs-extra. All fs methods return promises if the callback isn't passed.
You don't ever need to include the original fs module again:
const fs = require('fs') // this is no longer necessary
you can now do this:
const fs = require('fs-extra')
or if you prefer to make it clear that you're using fs-extra and not fs, you may want
to name your fs variable fse like so:
const fse = require('fs-extra')
you can also keep both, but it's redundant:
const fs = require('fs')
const fse = require('fs-extra')
NOTE: The deprecated constants fs.F_OK, fs.R_OK, fs.W_OK, & fs.X_OK are not exported on Node.js v24.0.0+; please use their fs.constants equivalents.
There is also an fs-extra/esm import, that supports both default and named exports. However, note that fs methods are not included in fs-extra/esm; you still need to import fs and/or fs/promises seperately:
import { readFileSync } from 'fs'
import { readFile } from 'fs/promises'
import { outputFile, outputFileSync } from 'fs-extra/esm'
Default exports are supported:
import fs from 'fs'
import fse from 'fs-extra/esm'
// fse.readFileSync is not a function; must use fs.readFileSync
but you probably want to just use regular fs-extra instead of fs-extra/esm for default exports:
import fs from 'fs-extra'
// both fs and fs-extra methods are defined
Most methods are async by default. All async methods will return a promise if the callback isn't passed.
Sync methods on the other hand will throw if an error occurs.
Also Async/Await will throw an error if one occurs.
Example:
const fs = require('fs-extra')
// Async with promises:
fs.copy('/tmp/myfile', '/tmp/mynewfile')
.then(() => console.log('success!'))
.catch(err => console.error(err))
// Async with callbacks:
fs.copy('/tmp/myfile', '/tmp/mynewfile', err => {
if (err) return console.error(err)
console.log('success!')
})
// Sync:
try {
fs.copySync('/tmp/myfile', '/tmp/mynewfile')
console.log('success!')
} catch (err) {
console.error(err)
}
// Async/Await:
async function copyFiles () {
try {
await fs.copy('/tmp/myfile', '/tmp/mynewfile')
console.log('success!')
} catch (err) {
console.error(err)
}
}
copyFiles()
NOTE: You can still use the native Node.js methods. They are promisified and copied over to fs-extra. See notes on fs.read(), fs.write(), & fs.writev()
walk() and walkSync()?They were removed from fs-extra in v2.0.0. If you need the functionality, walk and walkSync are available as separate packages, klaw and klaw-sync.
fse-cli allows you to run fs-extra from a console or from npm scripts.
If you like TypeScript, you can use fs-extra with it: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/fs-extra
If you want to watch for changes to files or directories, then you should use chokidar.
fs-filesystem allows you to read the state of the filesystem of the host on which it is run. It returns information about both the devices and the partitions (volumes) of the system.
Wanna hack on fs-extra? Great! Your help is needed! fs-extra is one of the most depended upon Node.js packages. This project
uses JavaScript Standard Style - if the name or style choices bother you,
you're gonna have to get over it :) If standard is good enough for npm, it's good enough for fs-extra.
What's needed?
Note: If you make any big changes, you should definitely file an issue for discussion first.
fs-extra contains hundreds of tests.
npm run lint: runs the linter (standard)npm run unit: runs the unit testsnpm run unit-esm: runs tests for fs-extra/esm exportsnpm test: runs the linter and all testsWhen running unit tests, set the environment variable CROSS_DEVICE_PATH to the absolute path of an empty directory on another device (like a thumb drive) to enable cross-device move tests.
If you run the tests on the Windows and receive a lot of symbolic link EPERM permission errors, it's
because on Windows you need elevated privilege to create symbolic links. You can add this to your Windows's
account by following the instructions here: http://superuser.com/questions/104845/permission-to-make-symbolic-links-in-windows-7
However, I didn't have much luck doing this.
Since I develop on Mac OS X, I use VMWare Fusion for Windows testing. I create a shared folder that I map to a drive on Windows.
I open the Node.js command prompt and run as Administrator. I then map the network drive running the following command:
net use z: "\\vmware-host\Shared Folders"
I can then navigate to my fs-extra directory and run the tests.
I put a lot of thought into the naming of these functions. Inspired by @coolaj86's request. So he deserves much of the credit for raising the issue. See discussion(s) here:
First, I believe that in as many cases as possible, the Node.js naming schemes should be chosen. However, there are problems with the Node.js own naming schemes.
For example, fs.readFile() and fs.readdir(): the F is capitalized in File and the d is not capitalized in dir. Perhaps a bit pedantic, but they should still be consistent. Also, Node.js has chosen a lot of POSIX naming schemes, which I believe is great. See: fs.mkdir(), fs.rmdir(), fs.chown(), etc.
We have a dilemma though. How do you consistently name methods that perform the following POSIX commands: cp, cp -r, mkdir -p, and rm -rf?
My perspective: when in doubt, err on the side of simplicity. A directory is just a hierarchical grouping of directories and files. Consider that for a moment. So when you want to copy it or remove it, in most cases you'll want to copy or remove all of its contents. When you want to create a directory, if the directory that it's suppose to be contained in does not exist, then in most cases you'll want to create that too.
So, if you want to remove a file or a directory regardless of whether it has contents, just call fs.remove(path). If you want to copy a file or a directory whether it has contents, just call fs.copy(source, destination). If you want to create a directory regardless of whether its parent directories exist, just call fs.mkdirs(path) or fs.mkdirp(path).
fs-extra wouldn't be possible without using the modules from the following authors:
Licensed under MIT
Copyright (c) 2011-2024 JP Richardson