fs-extra 是 Node.js 原生 fs 模块的增强版,提供了更安全的文件操作方法和额外功能(如递归删除、确保目录存在)。memfs 和 memory-fs 提供了内存中的文件系统实现,常用于构建工具或沙箱环境,其中 memfs 是现代替代品。mock-fs 专注于单元测试,用于模拟 fs 模块的行为以隔离测试环境。这四个包分别解决了生产文件操作、运行时内存文件系统以及测试隔离的不同需求。
在 Node.js 开发生态中,处理文件系统(File System)是常见需求。无论是构建工具、服务器端渲染,还是单元测试,我们都需要可靠的方式来读写文件。fs-extra、memfs、memory-fs 和 mock-fs 是四个常被提及的包,但它们的目标场景截然不同。本文将从架构设计、API 特性及适用场景进行深度剖析。
这四个包虽然都涉及“文件系统”,但解决的问题域不同。
fs-extra 是原生 fs 的超集。
// fs-extra: 确保目录存在并写入文件
const fs = require('fs-extra');
async function writeConfig() {
// 如果 /tmp/config 不存在,会自动创建,不会报错
await fs.outputFile('/tmp/config/app.json', '{"theme":"dark"}');
}
memfs 是运行时的内存文件系统。
fs 模块,适合沙箱或构建工具。// memfs: 创建内存卷并写入
const { Volume } = require('memfs');
const vol = new Volume();
vol.fromJSON({ '/app/config.json': '{"debug":true}' });
// 可以像普通 fs 一样使用,但操作的是内存
const data = vol.readFileSync('/app/config.json', 'utf8');
memory-fs 是旧版的内存文件系统。
memfs 类似,但架构较老。// memory-fs: 实例化并写入(旧式 API)
const MemoryFileSystem = require('memory-fs');
const fs = new MemoryFileSystem();
fs.mkdirpSync('/tmp');
fs.writeFileSync('/tmp/file.txt', 'Hello');
// 读取文件内容
const content = fs.readFileSync('/tmp/file.txt', 'utf8');
mock-fs 是测试专用的模拟工具。
fs 的调用,返回虚构的数据。fs,防止污染环境。// mock-fs: 在测试中模拟文件结构
const mock = require('mock-fs');
mock({
'/fake/dir': {
'file.txt': 'file content here'
}
});
// 代码以为在读写真实磁盘,实际是在内存模拟中
const data = require('fs').readFileSync('/fake/dir/file.txt');
// 测试完后必须恢复
mock.restore();
在架构选型中,库的维护状态至关重要。
fs-extra:🟢 活跃维护。社区标准,广泛用于生产环境,API 稳定。memfs:🟢 活跃维护。现代内存文件系统首选,支持 Node.js 新特性。memory-fs:🔴 遗留/不推荐。GitHub 和 npm 上已无明显活跃维护,Webpack 5 已移除对其的依赖。新项目应避免使用。mock-fs:🟡 维护中。主要用于测试,但在复杂场景下(如与其他 mock 库混用)可能不稳定,部分团队转向使用 memfs 进行测试模拟。处理嵌套目录是常见痛点。原生 fs 在旧版本中不支持递归创建或删除,而这些库提供了不同解决方案。
fs-extra 提供了最直观的递归方法。
// fs-extra: 一键递归创建或删除
await fs.ensureDir('/tmp/complex/nested/path');
await fs.remove('/tmp/complex'); // 递归删除非空目录
memfs 在内存卷上支持类似操作,但需通过 Volume 实例调用。
// memfs: 内存卷上的递归操作
const vol = require('memfs').Volume.fromJSON({});
vol.mkdirSync('/tmp/complex/nested/path', { recursive: true });
vol.rmdirSync('/tmp/complex', { recursive: true });
memory-fs 也支持,但 API 风格较旧。
// memory-fs: 旧式递归创建
const fs = new (require('memory-fs'))();
fs.mkdirpSync('/tmp/complex/nested/path');
mock-fs 通过配置对象定义结构,隐式支持目录树。
// mock-fs: 通过对象结构定义目录树
mock({
'/tmp': {
'complex': {
'nested': {
'path': {} // 空目录
}
}
}
});
现代 Node.js 开发高度依赖 async/await。
fs-extra 原生支持 Promise。
// fs-extra: 直接返回 Promise
try {
await fs.copy('/src/file.txt', '/dest/file.txt');
} catch (err) {
console.error(err);
}
memfs 同样支持 Promise API。
// memfs: 支持 async/await
const vol = new (require('memfs')).Volume();
await vol.promises.writeFile('/test.txt', 'data');
memory-fs 主要是同步或回调风格,Promise 支持较弱或需包装。
// memory-fs: 主要是同步方法或回调
fs.writeFile('/test.txt', 'data', (err) => {
if (err) throw err;
});
mock-fs 模拟的是原生 fs,因此继承原生 fs 的 Promise 支持(Node.js 10+)。
// mock-fs: 依赖原生 fs.promises
const fs = require('fs').promises;
await fs.writeFile('/mocked/file.txt', 'data');
在单元测试中,我们通常不希望测试代码污染开发者的真实磁盘。
mock-fs 是专门为测试设计的。
require('fs') 实现的,可能与某些深层依赖原生绑定的库冲突。// mock-fs 测试示例
describe('File Processor', () => {
beforeEach(() => {
mock({ 'data.json': '{"valid":true}' });
});
afterEach(() => {
mock.restore();
});
it('should read mocked file', async () => {
const result = await processFile('data.json');
expect(result.valid).toBe(true);
});
});
memfs 也可以用于测试,提供更底层的控制。
// memfs 测试示例
describe('File Processor with Memfs', () => {
let vol;
beforeEach(() => {
vol = require('memfs').Volume.fromJSON({ 'data.json': '{"valid":true}' });
});
it('should read from volume', () => {
const content = vol.readFileSync('data.json', 'utf8');
expect(JSON.parse(content).valid).toBe(true);
});
});
fs-extra 不适合纯单元测试隔离,因为它操作真实磁盘。
tmp)使用,而不是单独用于模拟。// fs-extra 配合临时目录(非模拟,是真实写入)
const tmp = require('tmp');
const fs = require('fs-extra');
const dir = tmp.dirSync();
await fs.writeFile(dir.name + '/test.txt', 'data');
// 测试完需手动清理
dir.removeCallback();
memory-fs 由于维护状态不佳,不推荐用于新测试套件。
memfs 和 memory-fs 都在内存中运行,速度极快,但受限于服务器内存。
memfs 对内存管理做了优化,支持更大的文件树。fs-extra 和 mock-fs(模拟时)涉及磁盘 I/O 或模拟开销。
fs-extra 性能取决于磁盘速度。mock-fs 在大量文件模拟时可能会增加测试启动时间。| 特性 | fs-extra | memfs | memory-fs | mock-fs |
|---|---|---|---|---|
| 主要用途 | 生产环境文件操作 | 运行时内存文件系统 | 遗留内存文件系统 | 单元测试模拟 |
| 存储介质 | 真实磁盘 | 内存 | 内存 | 内存(模拟) |
| 维护状态 | 🟢 活跃 | 🟢 活跃 | 🔴 遗留/弃用 | 🟡 维护中 |
| Promise 支持 | ✅ 原生支持 | ✅ 支持 | ⚠️ 有限 | ✅ 继承原生 |
| 递归操作 | ✅ 便捷方法 | ✅ 支持 | ✅ 支持 | ✅ 配置定义 |
| 推荐场景 | 后端服务、CLI 工具 | 构建工具、沙箱 | 旧项目维护 | 单元测试 |
1. 生产环境文件操作:首选 fs-extra
不要重复造轮子。处理上传、日志轮转、配置读写时,fs-extra 的 ensureDir 和 move 能避免大量边界条件错误。它是 Node.js 后端开发的“标准库”之一。
2. 构建工具与沙箱:首选 memfs
如果你正在开发类似 Webpack、Vite 的构建工具,或者需要在无磁盘环境(如某些 Serverless 环境或浏览器端 Node 模拟)运行代码,memfs 是唯一现代化的选择。它可以挂载到 Node 的全局 fs,透明地替换底层实现。
3. 单元测试:mock-fs 或 memfs
对于简单的文件读取测试,mock-fs 设置最快。但如果你的测试涉及复杂的文件流或需要更底层的控制,memfs 提供更稳定的行为。避免在测试中使用真实磁盘,除非测试的就是磁盘 I/O 性能。
4. 避坑指南:远离 memory-fs
除非你在维护一个 5 年前的 Webpack 配置,否则不要在新项目中引入 memory-fs。它的 API 设计已过时,且缺乏对新 Node.js 版本特性的支持。迁移到 memfs 通常只需少量代码调整。
尽管用途不同,这些库在生态中常配合使用:
fs 接口所有库都尽量保持与原生 fs 模块的 API 一致性,降低学习成本。
// 所有库都支持类似的读取签名
fs.readFile(path, options, callback);
// 或 Promise
await fs.readFile(path, options);
处理大文件时,流是关键。fs-extra 和 memfs 都支持流式读写。
// fs-extra: 创建读取流
const readStream = fs.createReadStream('large-file.log');
// memfs: 内存卷创建流
const vol = new (require('memfs')).Volume();
const readStream = vol.createReadStream('/large-file.log');
都遵循 Node.js 标准错误优先回调或 Promise 拒绝模式。
// 统一的错误捕获模式
try {
await fs.access('/protected/file');
} catch (err) {
if (err.code === 'ENOENT') {
// 处理文件不存在
}
}
在 Node.js 架构设计中,文件系统抽象层的选择直接影响系统的可测试性和运行效率。
fs-extra 是生产力工具,让磁盘操作更安全、更简单。memfs 是架构工具,让文件系统在内存中运行,解耦硬件依赖。mock-fs 是测试工具,确保单元测试的纯净与隔离。memory-fs 是历史包袱,应尽快从依赖中移除。根据具体场景组合使用这些工具,例如用 fs-extra 处理生产日志,用 memfs 运行构建管道,用 mock-fs 验证配置加载逻辑,是构建健壮 Node.js 应用的最佳实践。
选择 fs-extra 如果你在生产环境中需要更稳健的文件操作,例如递归创建目录、移动文件或复制文件夹。它是行业标准,API 与原生 fs 高度兼容,适合需要额外安全保障和便捷方法的服务器端应用。
选择 memfs 如果你需要在运行时拥有一个完全在内存中的文件系统,例如在浏览器中运行 Node 代码、构建工具插件或需要快速读写的临时沙箱。它是 memory-fs 的现代维护版本,支持挂载到 Node 的 fs 模块。
不建议在新项目中使用 memory-fs。它已被视为遗留库,维护频率低,且 Webpack 等主流工具已转向 memfs。仅在维护依赖此库的旧版构建配置时考虑使用,否则应迁移到 memfs。
选择 mock-fs 如果你主要需要在单元测试中模拟文件系统行为,而不想实际写入磁盘。它允许你定义虚构的文件结构,测试代码对 fs 的调用,适合隔离测试用例,但注意它可能与其他依赖 fs 的库冲突。
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