likes
comments
collection
share

五、fs 模块指南

作者站长头像
站长
· 阅读数 10

1、前言

你见过脾气最好的人是谁?

我一直以为自己是脾气好,后来才发现是窝囊;温和且坚定,实在是非常吸引人的性格~

Node.js 的 fs 模块给我的印象就不太坚定,因为形式变化多样,支持 Promise回调同步 3 种形式

2、fs 及其 3 种形式

fs 用来操作文件系统,是 Node 最常用的模块之一。广泛应用于文件读取,备份等场景

fs 几乎所有 api 都支持 Promise回调同步 3 种方式,除了一些已经废弃或将要废弃的 api,如 fs.exists;它们之间的区别主要在于模块引入、方法名、错误处理

[重要]一般情况下,我们进行单个 api 的测试,会使用 Promise.then().catch 形式,方便查看错误及结果信息;而在实际编程中,则会结合 async/await 来组合 api;避免使用同步 api

2.1、同步形式

同步的 API 会阻塞代码执行,直到操作完成。 异常会被立即地抛出,可以使用 try…catch 来处理

const fs = require('fs')

try {
  fs.copyFileSync('src/source.txt', 'src/destination.txt');
} catch (error) {
  console.log(error);
}
  • 1、引入 fs 模块
  • 2、使用 fs 的同步方法 copyFileSync
  • 3、使用 try catch 捕获错误

2.2、回调形式

回调的 API 异步地执行所有操作,不会阻塞事件循环,然后在完成或错误时调用回调函数。

回调形式执行文件系统操作不是同步的也不是线程安全的,对同一文件执行多个并发修改时必须小心,否则可能会损坏数据

const fs = require('fs')
fs.copyFile('source.txt', 'test2.txt', (err) => {
  if (err) {
    console.log(err)
    return;
  }
  console.log(`复制成功`)
})
  • 1、引入 fs 模块
  • 2、使用 fs 的异步方法 copyFile
  • 3、通过回调处理结果,如果 err 存在,则代表处理异常;否则,代表成功

2.3、Promise 形式(推荐)

回调形式如果层级太深,就会陷入回调地狱,Node 14 版本开始,暴露为 require('fs/promises'); 操作文件系统,向 ES6 异步解决方案 async/await 靠拢,也是推荐的方式

const fs = require('fs/promises'); // const fs = require('fs').promises

async function test(params) {
  try {
    await fs.copyFile('source.txt', 'test2.text');
    console.log(`复制成功`)
  } catch (error) {
    console.log(err)
  }
}

test()
  • 1、require('fs/promises') 形式引入 Promise 形式模块;也可以写作 require('fs').promises
  • 2、结合 async/await 使用,避免回调地狱
  • 3、使用 Promise 的异常捕获

我们也可以借助 Node 全局工具方法将异步 api 转为 Promise 形式

const fs = require('fs');
const promiseCopyFile = util.promisify(fs.copyFile);

async function test(params) {
  try {
    await fs.promiseCopyFile('source.txt', 'test2.text');
    console.log(`复制成功`)
  } catch (error) {
    console.log(err)
  }
}

test()

2.4、异常处理

同步形式和 Promise 形式异常处理需要通过 try...catch 的形式来捕获处理;回调形式则通过判断 err 是否存在来检查是否出现错误

如果在程序执行过程中出现了未捕获的异常,那么程序就会崩溃,因此必须把错误进行捕获,保证后续代码也能被顺利运行;Node 提供了 process 模块监听 uncaughtException 事件,可以捕获到整个进程包含异步中的错误信息

同步 js 报错或者 Promise reject 的异常都将被 uncaughtException 捕获

process.on('uncaughtException', (e) => { 
    console.error('process error is:', e.message); 
    process.exit(1); // 杀死进程
    // 重启服务 
});

当异常出现时,对应执行栈中断,process 捕获的异常事件导致 v8 引擎的垃圾回收功能不能按照正常流程工作,然后开始出现内存泄漏问题;所以当捕获到异常时,显式的手动杀掉进程,并开始重启 node 进程,即保证释放内存,又保证了保证服务后续正常可用

3、常用 API

文件操作系统的 API 主要包括以下几个方面

  • 检测文件/文件夹是否存在
  • 文件操作,如创建、写入内容、复制、删除等
  • 文件夹操作,如创建、删除等

3.1、路径处理

文件相关的操作,路径就绕不开的话题,在此之前,必须先简单了解一些 path 模块常用的 api

__dirname

全局变量,表示当前模块的目录名;注意,不会包含文件名,它的值是相对于当前执行脚本的位置;

五、fs 模块指南

__filename

全局变量,表示当前模块的文件名;包含上述完整目录

path 处理

node 内置了 path 模块用来辅助处理路径

const path = require('path')

console.log(path.dirname('test/my/logs/')); // test/my
console.log(path.dirname('test/my/logs/20231024.log')); // test/my/logs

console.log(path.extname('index.')); // .
console.log(path.extname('.index')); // ''
console.log(path.extname('test.index.md')); // .md

let baseTestName = 'test/index.md';
console.log(path.basename(baseTestName)); // index.md
console.log(path.basename(baseTestName, path.extname(baseTestName))); // index

console.log(path.sep); // 当前平台的目录分隔符
console.log(path.join('./test/logs/a').split(path.sep)); // ['test', 'logs', 'a']
console.log(path.join('./test/logs/a').split(path.sep)); // ['.', 'test', 'logs', 'a']

console.log(path.join('a', '../b')); // b
console.log(path.resolve('/a', '../b')); // C:\a
  • path.dirname(path) 返回路径的上一级目录的路径;如果传入的是目录,则返回父文件夹路径
  • path.extname(path) 返回 path 的最后一次出现的 .(句点)字符到字符串的结尾;即文件的拓展名;如果 . 前面没有内容,返回空;如果有多个,则返回最后一个;如果 . 后面没有内容,返回 .
  • path.basename(path[, suffix]) 返回文件名;支持传入第二个参数代表要删除的内容,结合拓展名用来获取不带后缀的文件名;注意,删除内容严格区分大小写
  • path.sep 返回当前平台的目录分隔符;Window 下是 \;mac 下则是 /;这是一个非常有用的属性,常用来结合 split 将路径分隔(这在检查路径是否可访问时非常有用)
  • path.joinpath.resolve 都用来拼接路径

3.1.1、路径与路径规范化

路径可以分为两类:绝对路径与相对路径和路径

  • 绝对路径:以 / 开头的路径或盘符开头,如 /a/a/b/C:/a 等都是绝对路径
  • 相对路径:以 ./../不带 / 开头的路径,如 ./a../aaa/ 等都是相对路径

路径规范化:

  • 路径中间的 ./ 规范化为当前目录;
  • 路径 ../ 规范化为上一级目录;
  • 多个 / 规范化为一个 /
  • 尾部的 / 会被忽略。

3.1.2、path.join 与 path.resolve 对比

path.join 的作用是将多个路径字符串以分隔符拼接,然后规范化为链接

console.log(path.join('/a', './b', '../c', 'd', 'e/', '/f'));

以分隔符拼接,得到 /a/./b/../c/d/e/;规范化:

  • /a 后面的 ./ 即为当前目录为 /a 得到 /a/b
  • /a/b../ 上一级目录为 /a 得到 /a/c
  • 以此类推,得到 /a/c/d/e/

path.resolve 则是将多个路径拼接,规范化为链接后

  • resolve 从后往前拼接,遇到绝对地址则结束
  • 拼接后规范化,得到一个链接
  • 如果得到的是绝对链接,则以盘符进行拼接
  • 如果得到的是相对链接,则以当前目录 __dirname 进行拼接
console.log(path.resolve('/a', '/b', './c', '../d', 'e'));

上述示例中,从后往前,直到 /b 才是绝对链接,因此得到 /b/./c/../d/e,规范化后得到 /b/d/e 是绝对链接;再以盘符拼接(脚本在 C 盘),得到 C:\b\d\e

path.resolve('/a', '/b') 第一个链接 /b 即为绝对路径,直接与盘符拼接 C:\b

path.resolve('b') 只有一个相对链接,与相对路径拼接 C:\Users\51CTO\Desktop\learn-node\src\path\b

以上即为 path.join 与 path.resolve 的主要区别;一般情况下,我们都以当前目录为基准(__dirname)来处理,如生成 webpack 的 ouput 路径: path.join(__dirname, '../output')path.resolve(__dirname, '../output')

3.2、可访问性检查

无论是文件夹操作还是文件操作,首先要做的就是检测路径;比如你要创建一个多层目录,/a/b,必须父级目录存在,否将会报错

3.2.1、fs.access

fs.access 可以用来检查文件或文件夹是否存在、是否可读、是否可写

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

let path = 'file/copy.js'

// 检查文件是否存在
fs.access(path).then(() => {
  console.log(`文件存在`)
}).catch(err => {
  console.log(`文件不存在`);
})

// 检查文件是否存在
fs.access(path, fs.constants.F_OK).then(() => {
  console.log(`文件存在`)
}).catch(err => {
  console.log(`文件不存在`);
})

// 检查文件存在,且可读写
fs.access(path, fs.constants.F_OK | fs.constants.R_OK | fs.constants.W_OK).then(() => {
  console.log(`可以进行操作`)
}).catch(err => {
  console.log(`不能进行操作`);
})

第二个参数可以缺省,默认为检查文件是否存在;

在调用 fs.open()fs.readFile() 或 fs.writeFile() 之前,不要使用 fs.access() 检查文件的可访问性。 这样做会引入竞争条件,因为其他进程可能会在两次调用之间更改文件的状态。 而是,用户代码应直接打开/读取/写入文件,并处理无法访问文件时引发的错误

3.2.2、fs.stat

stat 译为统计,fs.stat 则是获取路径的详细信息;

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

let path = 'file/copy.js'
fs.stat(path).then((stats) => {
  console.log(stats);
  console.log(stats.isFile()); // 判断路径是否为文件
  console.log(stats.isDirectory()); // 判断路径是否为目录
}).catch(err => {
  console.log(err);
})

如果目录存在,则输出详细信息,如添加时间、大小、最近一次修改时间、文件类型(目录还是文件

五、fs 模块指南

如果目录不存在,则抛出错误 ENOENTError No Entry 意思是:没有这样的目录条目

更多关于 fs.stat 的信息,参见官网

五、fs 模块指南

3.3、文件操作

3.3.1、fs.readFile

fs.readFile 默认按 Buffer 形式读取文件内容,支持传入编码类型读取为字符串 fs.readFile('./static/a.json', { encoding: 'utf-8' }) 也可以简写为 fs.readFile('./static/a.json', 'utf-8')

const fs = require('fs/promises')

// 读取 json 文件
fs.readFile('./static/a.json', { encoding: 'utf-8' }).then(data => {
  console.log(JSON.parse(data).id)
}).catch(err => {
  console.log(err)
})

注意:

  • 读取的内容是字符串类型,如读取的 json 字符串,需要 JSON.parse 处理一下
  • 如果读取的文件不存在,则会报错 ENOENT

3.3.2、fs.writeFile 与 fs.appendFile

两者均可以用来向文件写入内容;writeFile 是覆盖式写入;而 appendFile 则是追加式写入

  • 如果写入文件的父目录不存在,均会报错 ENOENT
const fs = require('fs/promises')

// 创建文件并写入内容;如果文件已存在,则覆盖内容
let writeContent = `${JSON.stringify({ id: '111', desc: '我是覆盖写入' })},\n`;
fs.writeFile('./write/a.txt', writeContent).then(() => {
  console.log(`覆盖写入成功`)
}).catch(err => {
  console.error(err)
})

// 创建文件并写入内容;如果文件已存在,则覆盖内容
let appendContent = `${JSON.stringify({ id: '222', desc: '我是追加写入' })},\n`;
fs.appendFile('./write/a.txt', appendContent).then(() => {
  console.log(`追加写入成功`)
}).catch(err => {
  console.error(err)
})

3.3.3、fs.copyFile

fs.copyFile('./static/a.json', './test/test.json').then(() => {
  console.log(`复制成功`)
}).catch(err => {
  console.log(err)
})
  • 复制文件,如果源目录或者目标目录不存在,均会报错 ENOENT
  • 如果 dest 已经存在,则会被覆盖。
  • 如果源文件很大,可能会导致内存问题。在处理大型文件时建议使用可读流和可写流来代替 fs.copyFile 方法

无法保证复制操作的原子性。 如果在打开目标文件进行写入后发生错误,则将尝试删除目标文件。

如何理解上述 Node 官网对于复制出错删除的描述?复制操作,可以拆解为打开并写入文件(即创建一个新文件),那么如果写入失败,就相当于没有创建成功;那么啥时候会出现写入失败呢?比如磁盘空间不足、权限问题等都将导致出错

fs.symlink、fs.readlink、fs.link

symlink 这个概念在之前 pnpm 就介绍过,它类似一个 Window 的快捷方式,可以用来访问目标路径;比如我们可以右键桌面上的 vscode 快捷方式 --> 属性,快捷方式

五、fs 模块指南

我们就能看到这个快捷方式指向的真实目标地址,这个目标是可以修改的;比如,我们打开朋友电脑,将他桌面上的游戏软件快捷方式(什么 Steam、WeGame)将其目标统统设置为上述 vscode 的地址;当然还需点一下更改图标,把图标改回游戏软件的图标(选为游戏软件的目标地址的 exe 文件即可),这样就可以愉快的帮助你的朋友学习了~

Node.js 中,我们可以通过 fs.symlink 来创建一个软链接;通过 fs.readlink 来读取软链接的真实目标

const fs = require('fs/promises')

// 创建软链接
fs.symlink('./static/authInfo.json', './symlinkAuth.json').then(() => {
  console.log(`软链接创建成功`)
}).catch(err => {
  console.log(err)
})

// 读取软链接真实目标
fs.readlink('./symlinkAuth.json').then(target => {
  console.log(`软链接目标地址为:${target}`)
}).catch(err => {
  console.log(err)
})

// 读取非软链接真实路径:报错 EINVAL
fs.readlink('./static/authInfo.json').then(target => {
  console.log(`软链接目标地址为:${target}`)
}).catch(err => {
  console.log(err)
})

// 读取软链接文件
fs.readFile('./symlinkAuth.json', { encoding: 'utf8' }).then(data => {
  console.log(JSON.parse(data).id)
}).catch(err => {
  console.log(err)
})
  • 创建软链接时,如果该软链接已存在,则报错 EEXISTERROR EXIST 软链接已存在
  • fs.readlink 用来读取真实目标地址;如果地址是一个软链接,则返回软链接的真实地址;如果地址不存在,则报错 ENOENT;如果读取的是一个真实地址,则报错 EINVAL 不合法
  • 创建软链接成功后,可以直接用软链接来操作目标文件,如读取、复制、写入等等
  • 如果目标地址的文件发生变化,软链接将失效,所有操作如读取、复制、写入等都将报错 ENOENT; 但仍然能读取真实的目标地址
  • 不要以已经存在的路径作为软链接名

fs.symlink 创建软链接对应 fs.link 将创建一个硬链接 hard link

const fs = require('fs/promises')

// 创建硬链接
fs.link('./static/authInfo.json', './hardlinkAuth.json').then(() => {
  console.log(`软链接创建成功`)
}).catch(err => {
  console.log(err)
})

// 读取硬链接 json 文件
fs.readFile('./hardlinkAuth.json', { encoding: 'utf8' }).then(data => {
  console.log(JSON.parse(data).id)
}).catch(err => {
  console.log(err)
})
  • 硬链接并没有对应的读取方法,因此命名更显得重要,以 hardlink 开头
  • 硬链接创建后使用和软链接基本一致,除了没有读取方法(读取需要借助一些库)

fs.stat 查看文件信息来对比源地址、软链接、硬链接

// 目标地址信息
fs.stat('./static/authInfo.json').then((info) => {
  console.log(info)
}).catch(err => {
  console.log(err)
})

// 软链接信息
fs.stat('./symlinkAuth.json').then((info) => {
  console.log(info)
}).catch(err => {
  console.log(err)
})

// 硬链接信息
fs.stat('./hardlinkAuth.json').then((info) => {
  console.log(info)
}).catch(err => {
  console.log(err)
})

得到对应的信息

// 源地址的核心文件信息
Stats {
  dev: 2728893735,
  mode: 33206,
  nlink: 3,
  uid: 0,
  gid: 0,
  rdev: 0,
  blksize: 4096,
  ino: 72057594037995140,
  size: 97,
  blocks: 0,
}
// 软链接核心文件信息
Stats {
  dev: 2728893735,
  mode: 33206,
  nlink: 3,
  uid: 0,
  gid: 0,
  rdev: 0,
  blksize: 4096,
  ino: 72057594037995140,
  size: 97,
  blocks: 0,
}
// 硬链接核心文件信息
Stats {
  dev: 2728893735,
  mode: 33206,
  nlink: 3,
  uid: 0,
  gid: 0,
  rdev: 0,
  blksize: 4096,
  ino: 72057594037995140,
  size: 97,
  blocks: 0,
}

软链接与硬链接对比

  • ino 字段用来标识文件,我们可以发现,所有硬链接与源文件公用一个;软链接则是一个全新的 ino 标识
  • size 字段代表文件的大小,不论是硬链接或软链接都不会将原本的源文件复制一份,只会占用非常少量的磁盘空间。
  • 源文件的删除,硬链接依然可以使用;但软链接则将报错 ENOENT
  • 两者创建成功都将生成一个对应的 json 配置文件
  • 硬链接只能针对文件创建不能针对目录,软链接则可以针对目录;同时针对文件时,两者的拓展名可以和源文件不同

应用对比:

  • 软链接跨文件系统访问,应用广泛,如上述提到的 pnpm、Window 快捷方式、共享网络地址访问等
  • 硬链接由于不受源文件的删除影响,常用来做备份,防止误删除,具有非常小的 size

3.3.4、fs.unlink

fs.unlink 用来删除源文件、软链接、硬链接;不能用来删除目录,删除目录需要使用 fs.rmdirfs.rm

const fs = require('fs/promises')

fs.unlink('./static/authInfo.json').then(() => {
  console.log(`删除硬链接成功`)
}).catch(err => {
  console.log(err)
})

fs.unlink('symlinkAuth.json').then(() => {
  console.log(`删除硬链接成功`)
}).catch(err => {
  console.log(err)
})

fs.unlink('hardlinkAuth.json').then(() => {
  console.log(`删除硬链接成功`)
}).catch(err => {
  console.log(err)
})
  • 如果路径不存在,则报错 ENOENT
  • 如果是软链接、硬链接,则删除的对应的 json 文件,如下图所示

五、fs 模块指南

3.4、文件夹操作

3.4.1、fs.readdir

const fs = require('fs/promises')

fs.readdir('../file').then(info => {
  console.log(info)
}).catch(err => {
  console.log(err)
})
  • 读取成功回调返回所有文件及文件夹
  • 如果读取的目录不存在,将报错 ENOENT 没有目录
  • 如果读取的不是目录而是文件,将报错 ENOTDIR 不是目录

3.4.2、fs.mkdir

const fs = require('fs/promises')

fs.mkdir('./img/test').then(() => {
  console.log(`目录创建成功')
}).catch(err => {
  console.log(err)
})

  • 如果父级目录不存在,则报错 ENOENT
  • 如果创建的目录已经存在,则报错 EEXIST

3.4.3、fs.rmdir

const fs = require('fs/promises')

fs.rmdir('../check/test').then(() => {
  console.log(`目录删除成功`)
}).catch(err => {
  console.log(err)
})
  • 如果要删除的目录不存在,则报错 ENOENT

3.5、查漏补缺

fs.rename

fs.rename 用来重命名源文件、软链接、硬链接;同时还可以用来重命名目录

const fs = require('fs/promises')

// 硬链接重名
fs.rename('hardlinkA.json', 'hardlinkB.json').then(() => {
  console.log(`硬链接重命名成功`)
}).catch(err => {
  console.log(err)
})

// 软链接重名
fs.rename('symlinkA.json', 'symlinkB.json').then(() => {
  console.log(`软链接重命名成功`)
}).catch(err => {
  console.log(err)
})

// 源文件重命名
fs.rename('./static/authInfo.json', './static/authInfo2.json').then(() => {
  console.log(`文件名重命名成功`)
}).catch(err => {
  console.log(err)
})

// 文件夹重命名
fs.rename('./static/', './static2').then(() => {
  console.log(`文件夹重命名成功`)
}).catch(err => {
  console.log(err)
})

// 跨多级目录重命名,报错
fs.rename('../file/static/', './static').then(() => {
  console.log(`文件夹重命名成功`)
}).catch(err => {
  console.log(err)
})
  • 源文件、软链接、硬链接、文件夹不存在时,将报错 ENOENT
  • 重命名文件夹或源文件后,软链接将失效
  • 不允许重命名多级目录,将报错 EPERM

fs.rm

用来删除文件或目录

fs.open

一般情况下,我们可以通过 fs.readFilefs.writeFilefs.appendFile 用来读写文件;

fs.open 将打开一个文件,成功会调用 FileHandle 类的实例;允许你进行更复杂的操作,当上述读写不满足你的需求时,你可能需要通过 filehandle 来操作文件,具体可参考官方文档

五、fs 模块指南

3.6、总结

删除

  • fs.unlink 用来删除文件、软链接、硬链接
  • fs.rmdir 用来删除目录
  • fs.rm 用来删除目录和文件

文件操作实战

作业:编写一个 Node 脚本,实现以下功能

  • 1、嵌套的文件系统具体有哪些文件,并不直观,将所有 src 下的文件遍历出来,生成一个文件名与路径的 json
  • 2、找到 src 下所有名称为 test 的文件,合并为 test.log;即相同目录下有多个不同拓展名的 test 文件,则统一合并为 test.log;如果只有一个则重命名为 test.log
  • 3、将 src 下所有 test 的文件夹下的内容复制到与 src 同级的 test 文件夹下,并删除对应的 test 目录
  • 4、将 src 下所有文件以硬链接的形式备份到名为今天日期的文件夹下,如果一天执行多次备份,则覆盖备份

4、结束语

2024 年春节将在 2 月 10 日(初一)至 17 日(初八)放假调休,同时鼓励各单位结合带薪年休假等制度落实,安排职工在除夕(2 月 9 日)休息。形成史上最长年假,但除夕没有纳入假日安排难免引发非议。

....鼓励这个词,是在考验领导的良心吗?除夕不放假,是等着给韩国申遗吗...

除夕不放假的话,央视除夕春晚采访要改问大家都下班了吗....

转载自:https://juejin.cn/post/7293797226932961318
评论
请登录