Node文件I/O操作之 fs 模块详解

199 阅读7分钟

在现代 Web 应用中,数据的持久化存储、日志记录、配置管理等操作都是日常开发中不可或缺的部分。无论是保存用户上传的文件,还是记录系统运行日志,文件输入输出(File I/O)操作都在其中发挥着至关重要的作用。

在 Node.js 中,处理文件 I/O 的核心工具是 fs 模块。这个模块提供了一套丰富的 API,支持从文件的读取、写入到监控文件的变化,能够帮助开发者高效地管理文件操作。

本文将通过一系列典型的文件 I/O 操作,详细介绍 fs 模块的常用方法。

文件 I/O

文件 I/O 是程序对外部存储设备(如硬盘、固态硬盘等)的输入(input)和输出(output)

具体来说,指的是读取文件中的数据(输入)以及将数据写入文件(输出)

需要注意的是,因为磁盘 I/O 速度远低于内存和 CPU 之间的交互速度,所以文件 I/O 操作通常比较耗时,它直接影响到程序的响应速度和效率,所以文件 I/O 的性能是我们必须要考虑的。

文件 I/O 的基本操作包括打开文件 Open、读取文件 Read、写入文件 Write、关闭文件 Close 等,在不同编程语言中,文件 I/O 的实现方法各不相同,但基本原理相似。所以后面,我们以 Node.js 为例,来具体介绍文件 I/O 的基本操作。

fs 模块

在 Node.js 中,可以使用 fs 模块进行文件 I/O 操作。

File system | Node.js latest-v16.x Documentation

fs 模块提供了同步异步两种方式来操作文件,各个异步 api 对应的 Sync 方法是同步的(例如 fs.resdFileSync)。其中异步方式是比较推荐的方式,而同步方法通常只用在初始化时运行有限的次数。

ES6 之后,新增了 promise 形式的 API,返回一个 Promise 对象。目前,可以用不同的方法来引入这些 promise 形式的 API:

引入 promise 形式的 API:

const fs = require('node:fs/promises');

引入回调形式的 API:

const fs = require('node:fs');

readFile 读取文件

fsPromises.readFile(path[, options]) 方法用于读取文件内容,其中 path 是文件路径,options 是可选参数。

const fs = require('node:fs/promises');
// 1.txt 文件内容为 hello world
const filename = path.resolve(__dirname, './myfiles/1.txt');

async function test() {
	try {
		const content = await fs.readFile(filename, 'utf-8');
		// 如果不加 'utf-8',则返回的是 Buffer: <Buffer 68 65 6c 6c 6f 20 77 6f 72 6c 64>
		console.log(content); // 输出 hello world
	} catch (err) {
		console.error(err);
	}
}
test();

writeFile 写入文件

fsPromises.writeFile(path[, data[, options]])

path 文件路径,data 要写入的数据,options 可选参数。

const fs = require('node:fs/promises');
const filename = path.resolve(__dirname, './myfiles/2.txt');

async function test() {
	try {
		await fs.writeFile(filename, 'hello world 哈哈哈 124');
		console.log('写入成功');
	} catch (err) {
		console.error(err);
	}
}
test();

写入时,为什么不需要把内容'hello world 哈哈哈 124'转化为 buffer Buffer.from("hello world 哈哈哈 124", "utf-8") 呢?

因为传入字符串时,Node.js 会自动使用 UTF-8 编码将其转换为 Buffer,并将其写入文件。

可选参数 options 可以是一个对象,包含以下属性:

  • encoding:指定写入文件的字符编码,默认为 utf8
  • mode:指定文件的权限,默认为 0o666
  • flag:指定文件的打开标志,默认为 'w',表示写入模式。

所以我们这里增加一个 flag 参数,表示以追加的方式写入文件:

await fs.writeFile(filename, '追加内容1', {
	flag: 'a',
});

// 2.txt 文件内容为 hello world 哈哈哈 124追加内容1

stat 获取文件或目录信息

fsPromises.stat(path[, options])

path:文件或目录的路径,options:可选参数。

const fs = require('node:fs/promises');
const path = require('path');
const filename = path.resolve(__dirname, './myfiles/');

async function test() {
	try {
		const stat = await fs.stat(filename);
		console.log(stat);
		console.log('是否是目录', stat.isDirectory());
		console.log('是否是文件', stat.isFile());
	} catch (err) {
		console.error(err);
	}
}
test();

输出结果:

image.png

stat 常用属性:

属性名描述
size占用字节
atime上次访问时间
mtime上次文件内容被修改时间
ctime上次文件状态被修改时间
birthtime文件创建时间
isDirectory()判断是否是目录
isFile()判断是否是文件

readdir 获取目录中的文件和子目录

fsPromises.readdir(path[, options])

path:目录路径,options:可选参数。

const fs = require('node:fs/promises');
const path = require('path');
const dirname = path.resolve(__dirname, './myfiles/');

async function test() {
	try {
		const paths = await fs.readdir(dirname);
		console.log(paths);
		// [ '1.jpeg', '1.txt', '2.txt', 'sub' ]
	} catch (err) {
		console.error(err);
	}
}
test();

得到的是一个数组,数组中的元素是目录中的文件和子目录的名称。

mkdir 创建目录

fsPromises.mkdir(path[, options])

path:目录路径,options:可选参数。

const fs = require('node:fs/promises');
const path = require('path');
const dirname = path.resolve(__dirname, './myfiles/');

async function test() {
	try {
		await fs.mkdir(dirname + '/test');
		// project/myfiles/test 创建成功
		console.log('创建成功');
	} catch (err) {
		console.error(err);
	}
}
test();

如果原本该目录就存在,则会报错EEXIST

传入多级目录路径时,如果任何目录不存在,会导致 ENOENT 错误。

那么如何创建多级目录呢?那么就需要用到可选参数 options ,其包含以下属性:

  • recursive:指定是否递归创建目录,默认为 false
  • mode:指定目录的权限,默认为 0o777

recursive 设置为 true,即可递归创建目录:

await fs.mkdir(dirname + '/test1/test2/test3', {
	recursive: true,
});

unlink 删除文件或符号链接

fsPromises.unlink(path[, options])

path:文件或符号链接的路径,options:可选参数。

const fs = require('node:fs/promises');
const path = require('path');
const dirname = path.resolve(__dirname, './myfiles/');

async function test() {
	try {
		await fs.unlink(dirname + '/test.txt');
		// project/myfiles/test.txt 删除成功
		console.log('删除成功');
	} catch (err) {
		console.error(err);
	}
}
test();

注意:

  1. 如果删除的是符号链接,则删除的是符号链接本身,而不是它指向的文件。

    symbolic link 符号链接: 是一种特殊类型的文件,它指向另一个文件或目录。可以看作是一个快捷方式或指针。

  2. 如果删除的是目录,则会报错 EPERM
  3. 如果删除的文件不存在,则会报错 ENOENT

rmdir /rm 删除目录

fsPromises.rmdir(path[, options])

path:目录路径,options:可选参数。

const fs = require('node:fs/promises');
const path = require('path');
const dirname = path.resolve(__dirname, './myfiles/');

async function test() {
	try {
		await fs.rmdir(dirname + '/test');
		// project/myfiles/test 目录删除成功
		console.log('删除成功');
	} catch (err) {
		console.error(err);
	}
}
test();

如果删除的目录不存在,则会报错 ENOENT

rmdir 是用来删除空目录的, 如果删除的目录project/myfiles/test不是空目录,则会报错 ENOTEMPTY

Node.js v14.14.0 版本中引入了 fs.rm,它是 fs.rmdir 的增强版,可以用来删除 文件、空目录和非空目录,并且支持递归删除

关于删除文件,类似于 fs.unlink,仅删除符号链接,而不会删除链接目标。

可选参数 options

  • recursive:指定是否递归删除目录,默认为 false。注意:fs.rm默认无法删除目录,需要加上 recursive: true 选项才能删除目录及其内容。
  • force:指定是否强制删除,默认为 false,用于忽略一些权限错误或保护措施。

假设当前文件结构:

project
├── myfiles
│   ├── 1.jpeg
│   ├── 1.txt
│   ├── 2.txt
│   ├── sub
│   │   ├── 3.txt
│   └── test
│       ├── 4
│       │   ├── 5.txt
│       └── 6.txt
async function test() {
	try {
		await fs.rm(dirname + '/test/4', {
			recursive: true,
		});
		// project/myfiles/test/4 目录删除成功
		console.log('删除成功');
	} catch (err) {
		console.error(err);
	}
}
test();

判断文件或目录是否存在

fs.exists 可以判断文件或目录是否存在,但是它已经被废弃,建议使用 fs.access 或 fs.stat。

下面我们写一个 exists 方法,使用 fs.stat 来判断文件或目录是否存在

const fs = require('node:fs/promises');
const path = require('path');

async function exists(filename) {
	try {
		await fs.stat(filename);
		return true;
	} catch (err) {
		// 如果文件不存在,则fs.stat抛出错误,通过判断错误类型来处理
		if (err.code === 'ENOENT') {
			//文件不存在
			return false;
		}
		throw err;
	}
}

使用:

const dirname = path.resolve(__dirname, './myfiles/');

async function test() {
	try {
		const result = await exists(dirname + '/test');
		console.log('文件或目录是否存在:', result);
	} catch (err) {
		console.error(err);
	}
}
test();

小结

本文介绍了 Node.js 中的文件 I/O 操作及 fs 模块的常用方法。在 fs 模块中,我们掌握了如何进行文件读取、写入、更新和删除等基本操作。

在掌握了这些后,下一步可以进一步探索 Node.js 中的文件流(Streams)。流是处理大文件或需要高效数据传输的场景下的关键技术。流不仅可以避免一次性加载整个文件带来的内存开销,还可以使程序在处理大规模数据时更加高效和稳定。