在现代 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();
输出结果:
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();
注意:
- 如果删除的是符号链接,则删除的是符号链接本身,而不是它指向的文件。
symbolic link 符号链接: 是一种特殊类型的文件,它指向另一个文件或目录。可以看作是一个快捷方式或指针。
- 如果删除的是目录,则会报错
EPERM
。 - 如果删除的文件不存在,则会报错
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)。流是处理大文件或需要高效数据传输的场景下的关键技术。流不仅可以避免一次性加载整个文件带来的内存开销,还可以使程序在处理大规模数据时更加高效和稳定。