fs-extra vs memfs vs memory-fs vs mock-fs
Node.js 文件系统工具库选型指南:生产增强、内存实现与测试模拟
fs-extramemfsmemory-fsmock-fs类似的npm包:

Node.js 文件系统工具库选型指南:生产增强、内存实现与测试模拟

fs-extra 是 Node.js 原生 fs 模块的增强版,提供了更安全的文件操作方法和额外功能(如递归删除、确保目录存在)。memfsmemory-fs 提供了内存中的文件系统实现,常用于构建工具或沙箱环境,其中 memfs 是现代替代品。mock-fs 专注于单元测试,用于模拟 fs 模块的行为以隔离测试环境。这四个包分别解决了生产文件操作、运行时内存文件系统以及测试隔离的不同需求。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
fs-extra09,61058.1 kB151 个月前MIT
memfs02,06864.8 kB535 天前Apache-2.0
memory-fs0878-307 年前MIT
mock-fs0918107 kB741 年前MIT

Node.js 文件系统工具库深度对比:fs-extra vs memfs vs memory-fs vs mock-fs

在 Node.js 开发生态中,处理文件系统(File System)是常见需求。无论是构建工具、服务器端渲染,还是单元测试,我们都需要可靠的方式来读写文件。fs-extramemfsmemory-fsmock-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 是运行时的内存文件系统。

  • 文件存在于内存中,重启即失。
  • 可以完全替换 Node 的 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 类似,但架构较老。
  • 曾是 Webpack 的默认内存实现,现已逐渐被弃用。
// 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': {} // 空目录
      }
    }
  }
});

异步与 Promise 支持

现代 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 是专门为测试设计的。

  • 优点:API 简单,专为测试用例设计,自动恢复。
  • 缺点:它是通过替换 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 也可以用于测试,提供更底层的控制。

  • 优点:更真实地模拟文件系统行为,支持挂载。
  • 缺点:配置稍复杂,需要手动管理 Volume。
// 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 由于维护状态不佳,不推荐用于新测试套件。

🚀 性能与内存管理

memfsmemory-fs 都在内存中运行,速度极快,但受限于服务器内存。

  • 适合存储构建中间产物、临时缓存。
  • memfs 对内存管理做了优化,支持更大的文件树。

fs-extramock-fs(模拟时)涉及磁盘 I/O 或模拟开销。

  • fs-extra 性能取决于磁盘速度。
  • mock-fs 在大量文件模拟时可能会增加测试启动时间。

📊 总结对比表

特性fs-extramemfsmemory-fsmock-fs
主要用途生产环境文件操作运行时内存文件系统遗留内存文件系统单元测试模拟
存储介质真实磁盘内存内存内存(模拟)
维护状态🟢 活跃🟢 活跃🔴 遗留/弃用🟡 维护中
Promise 支持✅ 原生支持✅ 支持⚠️ 有限✅ 继承原生
递归操作✅ 便捷方法✅ 支持✅ 支持✅ 配置定义
推荐场景后端服务、CLI 工具构建工具、沙箱旧项目维护单元测试

💡 架构师建议

1. 生产环境文件操作:首选 fs-extra 不要重复造轮子。处理上传、日志轮转、配置读写时,fs-extraensureDirmove 能避免大量边界条件错误。它是 Node.js 后端开发的“标准库”之一。

2. 构建工具与沙箱:首选 memfs 如果你正在开发类似 Webpack、Vite 的构建工具,或者需要在无磁盘环境(如某些 Serverless 环境或浏览器端 Node 模拟)运行代码,memfs 是唯一现代化的选择。它可以挂载到 Node 的全局 fs,透明地替换底层实现。

3. 单元测试:mock-fsmemfs 对于简单的文件读取测试,mock-fs 设置最快。但如果你的测试涉及复杂的文件流或需要更底层的控制,memfs 提供更稳定的行为。避免在测试中使用真实磁盘,除非测试的就是磁盘 I/O 性能。

4. 避坑指南:远离 memory-fs 除非你在维护一个 5 年前的 Webpack 配置,否则不要在新项目中引入 memory-fs。它的 API 设计已过时,且缺乏对新 Node.js 版本特性的支持。迁移到 memfs 通常只需少量代码调整。

🔗 共同点与生态互补

尽管用途不同,这些库在生态中常配合使用:

1. 兼容 Node.js fs 接口

所有库都尽量保持与原生 fs 模块的 API 一致性,降低学习成本。

// 所有库都支持类似的读取签名
fs.readFile(path, options, callback);
// 或 Promise
await fs.readFile(path, options);

2. 支持流(Streams)

处理大文件时,流是关键。fs-extramemfs 都支持流式读写。

// fs-extra: 创建读取流
const readStream = fs.createReadStream('large-file.log');

// memfs: 内存卷创建流
const vol = new (require('memfs')).Volume();
const readStream = vol.createReadStream('/large-file.log');

3. 错误处理机制

都遵循 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 vs memfs vs memory-fs vs mock-fs

  • fs-extra:

    选择 fs-extra 如果你在生产环境中需要更稳健的文件操作,例如递归创建目录、移动文件或复制文件夹。它是行业标准,API 与原生 fs 高度兼容,适合需要额外安全保障和便捷方法的服务器端应用。

  • memfs:

    选择 memfs 如果你需要在运行时拥有一个完全在内存中的文件系统,例如在浏览器中运行 Node 代码、构建工具插件或需要快速读写的临时沙箱。它是 memory-fs 的现代维护版本,支持挂载到 Node 的 fs 模块。

  • memory-fs:

    不建议在新项目中使用 memory-fs。它已被视为遗留库,维护频率低,且 Webpack 等主流工具已转向 memfs。仅在维护依赖此库的旧版构建配置时考虑使用,否则应迁移到 memfs

  • mock-fs:

    选择 mock-fs 如果你主要需要在单元测试中模拟文件系统行为,而不想实际写入磁盘。它允许你定义虚构的文件结构,测试代码对 fs 的调用,适合隔离测试用例,但注意它可能与其他依赖 fs 的库冲突。

fs-extra的README

Node.js: 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.

npm Package License build status downloads per month JavaScript Style Guide

Why?

I got tired of including mkdirp, rimraf, and ncp in most of my projects.

Installation

npm install fs-extra

Usage

CommonJS

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.

ESM

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

Sync vs Async vs Async/Await

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()

Methods

Async

Sync

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()

What happened to 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.

Third Party

CLI

fse-cli allows you to run fs-extra from a console or from npm scripts.

TypeScript

If you like TypeScript, you can use fs-extra with it: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/fs-extra

File / Directory Watching

If you want to watch for changes to files or directories, then you should use chokidar.

Obtain Filesystem (Devices, Partitions) Information

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.

Misc.

Hacking on fs-extra

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.

js-standard-style

What's needed?

  • First, take a look at existing issues. Those are probably going to be where the priority lies.
  • More tests for edge cases. Specifically on different platforms. There can never be enough tests.
  • Improve test coverage.

Note: If you make any big changes, you should definitely file an issue for discussion first.

Running the Test Suite

fs-extra contains hundreds of tests.

  • npm run lint: runs the linter (standard)
  • npm run unit: runs the unit tests
  • npm run unit-esm: runs tests for fs-extra/esm exports
  • npm test: runs the linter and all tests

When 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.

Windows

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.

Naming

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).

Credit

fs-extra wouldn't be possible without using the modules from the following authors:

License

Licensed under MIT

Copyright (c) 2011-2024 JP Richardson