fs模块
fs
模块是File System
的缩写,模拟了Linux
环境,提供了Nodejs
对文件系统交互的各种功能API
。通过fs
模块,即可执行增、删、改、查等操作。
- 大多数操作文件
API
具有同步和异步的形式- 要使用基于同步和回调的方式
require('node:fs')
- 要使用基于
Promise
的方式require('node:fs/promises')
- 要使用基于同步和回调的方式
- 文件夹是一种特殊的文件
读取文件
常用的读取文件API
read:读取文件流内容,搭配文件描述符使用
jsconst fs = require("node:fs"); const { resolve } = require("node:path"); const filePath = resolve(__dirname, "./data.json"); // 1.创建文件描述符 fs.open(filePath, (err, fd) => { if (err) { console.error(err); return; } // 2.通过Buffer文件流读取文件 fs.read(fd, Buffer.alloc(1024), 0, 1024, 0, (err, bytesRead, buffer) => { if (err) { console.error(err); return; } console.log(buffer.slice(0, bytesRead).toString()); // 3.用完后记得关闭文件 fs.close(fd) }); });
TIP
文件描述符
在
POSIX
系统中会把一切看做文件,当进程打开现有文件或者创建文件时,内核会向进程返回一个文件描述符File Descriptor
,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行I/O
操作的系统调用都会通过文件描述符。文件描述符由
fs.open()
创建的对象FileHandle
提供。open
操作和close
操作通常是搭配一起使用,有打开文件,用完就要关闭文件,不然容易造成闭包内存泄漏。jsconst fs = require('node:fs') fs.open('./a.txt',(err,fd)=>{ if(err)return // fd参数就是该文件的文件描述符 // Nodejs提供了一系列的根据文件描述符去操作文件的API // fs.fchmod // fs.fstat // fs.fsync // ... fs.close(fd) })
TIP
Nodejs
模块支持支持以标准POSIX
函数为模型的方式与文件系统交互,也就是导入语句前的node
声明。readFile:回调读取文件
jsconst fs = require("node:fs"); const { resolve } = require("node:path"); const filePath = resolve(__dirname, "./data.json"); fs.readFile(filePath, { encoding: "utf8" }, (err, data) => { if (err) { console.error(err); return; } console.log(data); });
TIP
常见的文件编码
encoding
选项读取文件时,默认读到的是一些
Buffer
二进制数据,需要传递文件编码选项告诉它转成什么样的数据,在读取到Buffer
二进制数据时,都可以这么操作。常见的有gbk
utf-8
gb18030
gb2312
TIP
需要注意的是,代码在执行时,Nodejs会动态拼接命令执行时所在的目录,容易出现路径拼接错误, 在使用fs模块的API时,最好手动拼接好绝对路径,就像上面提供的🌰 resolve 所做的一样。
readFileSync:同步读取文件
jsconst fs = require("node:fs"); const { resolve } = require("node:path"); const filePath = resolve(__dirname, "./data.json"); // 读文件的文件系统标志默认为 "r" const fileContent = fs.readFileSync(filePath, { encoding: "utf8", flag: "r" }); console.log(fileContent);
TIP
文件系统
Flag
以下标志在
flag
选项接受字符串的任何地方可用。'a'
:打开文件进行追加。如果文件不存在,则创建该文件。'ax'
:类似于'a'
但如果路径存在则失败。'a+'
:打开文件进行读取和追加。如果文件不存在,则创建该文件。'ax+'
:类似于'a+'
但如果路径存在则失败。'as'
:以同步模式打开文件进行追加。如果文件不存在,则创建该文件。'as+'
:以同步模式打开文件进行读取和追加。如果文件不存在,则创建该文件。'r'
:打开文件进行读取。如果文件不存在,则会发生异常。'rs'
:打开文件以同步模式读取。如果文件不存在,则会发生异常。'r+'
:打开文件进行读写。如果文件不存在,则会发生异常。'rs+'
:以同步模式打开文件进行读写。指示操作系统绕过本地文件系统缓存。这主要用于在 NFS 挂载上打开文件,因为它允许跳过可能过时的本地缓存。它对 I/O 性能有非常实际的影响,因此除非需要,否则不建议使用此标志。这不会将fs.open()
或fsPromises.open()
变成同步阻塞调用。如果需要同步操作,应该使用类似fs.openSync()
的东西。'w'
:打开文件进行写入。创建(如果它不存在)或截断(如果它存在)该文件。'wx'
:类似于'w'
但如果路径存在则失败。'w+'
:打开文件进行读写。创建(如果它不存在)或截断(如果它存在)该文件。'wx+'
:类似于'w+'
但如果路径存在则失败。
open(打开文件描述符)
jsconst fs = require("fs"); const { resolve } = require("path"); const filePath = resolve(__dirname, "./data.json"); fs.open(filePath, (err, fd) => { if (err) { console.error(err); return; } // fd是文件描述符文件参数 console.log(fd); // 用完需要关闭,不然容易造成内存泄漏 fs.close(fd) });
stat(读取文件或文件目录信息,可以用stat方法,或者用文件描述符的方式去读取fstat)
jsconst fs = require("fs"); const { resolve } = require("path"); const filePath = resolve(__dirname, "./data.json"); // stat fs.stat(filePath, (err, stats) => { if (err) { console.error(err); return; } console.log(stats); }); // fstat fs.open(filePath, (err, fd) => { if (err) { console.error(err); return; } fs.fstat(fd, (err, stats) => { if (err) { console.error(err); return; } console.log(stats); fs.close(fd) }); });
写入文件
常用的写文件API
写入文件会大量使用到文件系统Flag
准备工作,定义几个生成文件名、文件路径、文件内容的方法,提供几个方法调用:
const fs = require("node:fs");
const { resolve } = require("node:path");
// 从26个字母随机截取任一字符作为文件名
const generateRandomFilename = () => String.fromCharCode(65 + Math.floor(Math.random() * 26)).toLocaleLowerCase();
// 生成1-100长度内的随机字符串
const generateContent = () =>
Array.from({ length: Math.floor(Math.random() * 100) }, () =>
String.fromCharCode(65 + Math.floor(Math.random() * 26))
).join("");
// 生成文件路径,每次调用均生成不同的文件名
const generateFilePath = () => resolve(__dirname, `./${generateRandomFilename()}.txt`);
write:文件流写入,传入文件描述符
jsfs.open(generateFilePath(), "w", (err, fd) => { if (err) return; fs.write(fd, generateContent(), (err) => { if (err) return; fs.close(fd); }); });
writeFile:写入字符串或文件流
jsfs.writeFile(generateFilePath(), generateContent(), { flag: "w" }, (err) => { if (err) { console.log(err); } });
writeSync:同步写入
jstry { fs.writeFileSync(generateFilePath(), generateContent(), { flag: "w" }); } catch (err) { console.log(err); }
复制、移动、重命名、删除
基础代码:
const fs = require("node:fs");
const { resolve, extname, basename } = require("node:path");
const filepath = resolve(__dirname, `../data/data.json`);
// 文件后缀
const fileExt = extname(filepath);
// 文件名
const fileName = basename(filepath, fileExt);
copyFile
jsfs.copyFile(filepath, resolve(__dirname, `../data/${fileName}-copy${fileExt}`), (err) => { if (err) { console.error(err); return; } console.log("复制文件成功"); });
cp
jsfs.cp(filepath, resolve(__dirname, `../data-move/${fileName}${fileExt}`), (err) => { if (err) { console.error(err); return; } console.log("移动文件成功"); });
rename
jsfs.rename(filepath, resolve(__dirname, `../data/${fileName}-rename${fileExt}`), (err) => { if (err) { console.error(err); return; } console.log("重命名文件成功"); });
mkdir
jsfs.mkdir(resolve(__dirname, `../data-dir`), (err) => { if (err) { console.error(err); return; } console.log("创建文件夹成功"); });
rm
jsfs.rm(filepath, { recursive: true }, (err) => { if (err) { console.error(err); return; } console.log("删除文件成功"); });
目录操作
通过手写删除嵌套文件夹,掌握操作目录的常用API
,学习异步串行、异步并行的解决方法。
同步删除:
const fs = require("fs");
const path = require("path");
// 同步删除文件夹
function rmdirSync(dir) {
const statObj = fs.statSync(dir);
if (statObj.isFile()) {
// 如果当前为文件,直接删除
fs.unlinkSync(dir);
} else {
// 否则是目录,需要递归删除
let dirs = fs.readdirSync(dir);
dirs = dirs.map((file) => path.join(dir, file));
dirs.forEach((dir) => rmdirSync(dir));
// 最后需要把最外层的给删除掉
fs.rmdirSync(dir);
}
}
rmdirSync(path.resolve(__dirname, "../a"));
异步删除:
const fs = require("fs");
const path = require("path");
// 异步删除文件夹
function rmdir(dir, cb) {
fs.stat(dir, (err, statObj) => {
if (statObj.isFile()) {
fs.unlink(dir, cb);
} else {
fs.readdir(dir, (err, dirs) => {
// 异步串行删除
dirs = dirs.map((file) => path.join(dir, file));
let index = 0;
function next() {
if (dirs.length === index) {
// 最后一个,说明此时并没有子节点,需要删除自己
return fs.rmdir(dir, cb);
}
rmdir(dirs[index++], next);
}
next();
// 异步并行删除
if (dirs.length === 0) {
return fs.rmdir(dir, cb);
}
dirs = dirs.map((file) => path.join(dir, file));
let index = 0;
function done() {
if (++index === dirs.length) {
fs.rmdir(dir, cb);
}
}
dirs.forEach((file) => rmdir(file, done));
});
}
});
}
rmdir(path.resolve(__dirname, "../a"), () => {
console.log("删完了");
});
文件夹实际上就是一颗树的结构:
删除文件夹要先删除文件夹下的内容,才能把整个文件夹删除,对于树来说,也就是需要将子节点删除,然后再移除掉整个节点。这种方式实际上就是树的广度遍历,先遍历每层的子节点并移除,最后移除父节点。
树的遍历
树的遍历分为广度优先遍历和深度优先遍历,其中深度优先遍历又能根据二叉树的情况分为前序、中序、后序遍历。
广度优先遍历:从根节点开始,遍历下一层中所有的子节点,然后再遍历下一层的所有子节点,以此类推,直到遍历所有的节点。
深度优先遍历:从根节点开始,尽可能深地搜索树的分支。递归地深度优先遍历每个子节点,重复 递归步骤,直至所有节点都被访问。
实战:递归目录扫描
递归扫描目录的内容,并统计不同类型文件的数量和大小:
const fs = require("node:fs");
const path = require("node:path");
const filepath = path.resolve(__dirname, "./a");
// 文件类型统计
const stats = {
totalFiles: 0,
totalDirs: 0,
byExtension: {},
};
const scanDirectory = (dirpath) => {
const dirs = fs.readdirSync(dirpath);
stats.totalDirs++;
// 遍历每个目录,判断是否为目录,目录继续递归,否则计数++
for (const item of dirs) {
const itemPath = path.resolve(dirpath, item);
try {
const stat = fs.statSync(itemPath);
if (stat.isDirectory()) {
scanDirectory(itemPath);
} else if (stat.isFile()) {
stats.totalFiles++;
stats.totalSize += stat.size;
const ext = path.extname(item).toLowerCase() || "(无拓展名)";
if (!stats.byExtension[ext]) {
stats.byExtension[ext] = {
count: 0,
size: 0,
};
}
stats.byExtension[ext].count++;
stats.byExtension[ext].size += stat.size;
}
} catch (error) {
console.log(error);
}
}
};
scanDirectory(filepath);
console.log(stats);
/*
{
totalFiles: 3,
totalDirs: 3,
byExtension: { '(无拓展名)': { count: 3, size: 0 } }
}
*/