原文见我的公众号文章 优雅的让你知道Node.js的fs模块扫描目录何时结束了
需求
提交软件著作权申请需要的软件源代码一份。
问题分析
读取项目目录,过滤某些文件夹(如:.git,.vscode等),只提取指定类型的文件的内容(如:js,wxml,wxss)。
即把某项目下指定类型的代码提取出来,写入到同一个文件中。
封装满足以下特点的工具函数:
- 可自动扫描指定路径下的所有文件及文件夹
- 提供指定过滤(不扫描)某些文件夹的参数
ignoreDirs
- 提供指定需要的文件类型的参数
allowExts
- 提供读取到文件的监听事件
onFile
- 提供读取路径失败的监听事件
onError
- 提供当指定路径下无可扫描文件(扫描结束)的监听事件,方法内部扫描过程是同步执行的
onComplete
- 函数本身只提供指定目录扫码任务,不含文件本身的读写操作
先上代码
/**
* [printDirSync 同步遍历指定文件夹下的所有文件(夹),支持遍历结束回调 onComplete]
* @param {*} dirPath
* @param {*} options {
allowExts: [], //指定需要的文件类型的路径,不指定则默认允许所有类型(不同于文件夹,文件类型太多,用忽略的方式太麻烦,所以用了允许)
ignoreDirs: [],//指定不需要读取的文件路径,不指定则默认读取所有文件夹
onFile: (fileDir, ext, stats) => {},
onError: (fileDir, err) => {},
onComplete: (fileNum) => {},
}
*/
function printDirSync(
dirPath,
options = {
allowExts: [],
ignoreDirs: [],
onFile: (fileDir, ext, stats) => {},
onError: (fileDir, err) => {},
onComplete: (filePaths) => {},
}
) {
const { allowExts, ignoreDirs, onFile, onComplete, onError } = options;
let onPrintingNum = 0; //记录正在遍历的文件夹数量,用来判断是否所有文件遍历结束
let findFiles = []; //统计所有文件,在onComplete中返回
// 因为fs.stat是异步方法,通过回调的方式返回结果,不可控的执行顺序影响是【否遍历结束】的判断
// 所以这里返回promise,配合sync/await模拟同步执行
const stat = (path) => {
return new Promise((resolve, reject) => {
fs.stat(path, function (err, stats) {
if (err) {
console.warn("获取文件stats失败");
if (onError && typeof onError == "function") {
onError(path, err);
}
} else {
if (stats.isFile()) {
const names = path.split(".");
const ext = names[names.length - 1];
// 对文件的处理回调,可通过allowExts数组过滤指定需要的文件类型的路径,不指定则默认允许所有类型
if (
!allowExts ||
allowExts.length == 0 ||
(allowExts.length && allowExts.includes(ext))
) {
if (onFile && typeof onFile == "function") {
findFiles.push(path);
onFile(path, ext, stats);
}
}
}
// 这里是对文件夹的回调,可通过ignoreDirs数组过滤不想遍历的文件夹路径
if (stats.isDirectory()) {
if (
!ignoreDirs ||
ignoreDirs.length == 0 ||
(ignoreDirs.length && !ignoreDirs.includes(path))
) {
print(path); //递归遍历
}
}
}
resolve(path + " stat结束");
});
});
};
// 处理正在遍历的文件夹遍历结束的逻辑:onPrintingNum-1 且 判断整体遍历是否结束
const handleOnPrintingDirDone = () => {
if (--onPrintingNum == 0) {
if (onComplete && typeof onComplete == "function") {
onComplete(findFiles);
}
}
};
// 遍历路径,记录正在遍历路径的数量
const print = async (filePath) => {
onPrintingNum++; //进入到这里,说明当前至少有一个正在遍历的文件夹,因此 onPrintingNum+1
let files = fs.readdirSync(filePath); //同步读取filePath的内容
let fileLen = files.length;
// 如果是空文件夹,不需要遍历,也说明当前正在遍历的文件夹结束了,onPrintingNum-1
if (fileLen == 0) {
handleOnPrintingDirDone();
}
//遍历目录下的所有文件
for (let index = 0; index < fileLen; index++) {
let file = files[index];
let fileDir = path.join(filePath, file); //获取当前文件绝对路径
try {
await stat(fileDir); //同步执行路径信息的判断
// 当该文件夹下所有文件(路径)都遍历完毕,也说明当前正在遍历的文件夹结束了,onPrintingNum-1
if (index == fileLen - 1) {
handleOnPrintingDirDone();
}
} catch (err) {}
}
};
print(dirPath);
}
再看咋用
const { fs, printDir, printDirSync } = require("./file-tools");
let dirPath = "/Users/yourprojectpath/yourprojectname";
let allowExts = ["wxml", "wxss", "js", "txt", "md"];
let ignoreDirs = [`${dirPath}/.git`, `${dirPath}/.DS_Store`, `${dirPath}/dist`];
printDirSync(dirPath, {
allowExts,
ignoreDirs,
onFile: (fileDir, ext, stats) => {
let fileContent = fs.readFileSync(fileDir, "utf-8"); //同步读取文件内容
writeFile( `文件路径:${fileDir.replace("/Users/yourprojectpath/","")}\n${fileContent}\n\n`);
},
onComplete: (files) => {
console.log("忽略的文件夹:", ignoreDirs);
console.log("指定的文件类型:", allowExts);
console.log(
`路径 [${dirPath}] 遍历结束,发现 ${files.length}个 文件如下\n`,
files
);
},
});
function writeFile(data = "") {
let outDir = "./dist/codes.txt";
fs.appendFileSync(outDir, data, {
encoding: "utf8",
});
}
最后我解释
扫描指定路径下所有文件的基本流程
- 通过
fs.readdir
方法读取指定的路径,在此方的回调函数里会返回该路径下的files
; - 遍历这些
files
并通过fs.stat
方法判断类型(是文件还是文件夹); - 如果是文件夹,则重复1和2;
- 如果是文件,则记录下来;
- 直到程序结束,说明扫描完毕!
如何知道扫描结束?
要做到这点,首先要保证方法内部扫描过程是同步执行的。
异步过于不可控,它会让代码的执行顺序变得混乱,所以就会选用同步扫描方法 fs.readdirSync
。
再者,因为在扫描过程中需要获取文件的 stats
信息来判断是 文件
还是 文件夹
,
而这个方法也是异步的(通过回调方法获取结果),同样会给执行顺序带来不确定性。
所以可以将 fs.stat
这部分功能提取出来,并通过 Promise
的形式返回结果,结合 async/awat
,
达到同步执行文件信息判断的效果。
这样便保证了代码执行流程的可控性,但是这样还不够!
到底如何才能知道所有文件都已经扫描结束呢?
核心的一步,这里我通过设定一个监测变量 onPrintingNum
,它记录正在扫描的文件夹数量,用来判断是否所有文件扫描结束。
当我们指定了一个路径,并执行 printDirSync
方法,onPrintingNum
初始化为0,在方法内部,独立封装了负责扫描的方法 print
。
自动执行一次 print
方法来扫描路径下的所有文件和文件夹,这个方法每次被调用,onPrintingNum
就会累加1,
当遇到空文件夹(fs.readdirSync
返回的 files
为空数组)或者遍历到 files
的结尾,onPrintingNum
先减少1;
然后紧接着判断 onPrintingNum
是否为0,若为0,则说明遍历结束了。
可以结合代码及里面的注释理解下,经过本人大量测试(复杂的文件结构和高频扫描)未发现问题。若有漏洞或不足请评论指出,一起探讨。
在处理这个问题的过程中也发现了一些 fs.readdirSync
方法读取文件的特点:输出的 files
数组(读取文件遍历)顺序并非固定,
这里没有深入研究。
若不关心扫描结束的动作,这里再提供一版异步的扫描方法,理论上效率更高。
/**
* [printDir 异步遍历文件夹的所有文件,只关注遇到文件的处理,不关注是否全部遍历结束]
* @param {String} dirPath [要遍历的文件夹路径]
* @return {Object} options {
allowExts: [], //指定需要的文件类型的路径,不指定则默认允许所有类型(不同于文件夹,文件类型太多,用忽略的方式太麻烦,所以用了允许)
ignoreDirs: [], //指定不需要读取的文件路径,不指定则默认读取所有文件夹
onFile: (fileDir, ext, stats) => {},
onError: (fileDir, err) => {},
onComplete: (fileNum) => {},
}
*/
function printDirAsync(
dirPath,
options = {
allowExts: [],
ignoreDirs: [],
onFile: (fileDir, ext, stats) => {},
onError: (fileDir, err) => {},
}
) {
const { ignoreDirs, onFile, onError } = options;
const print = (filePath) => {
fs.readdir(filePath, function (err, files) {
if (err) {
return console.error(err);
}
let fileLen = files.length;
//遍历目录下的所有文件
for (let index = 0; index < fileLen; index++) {
let file = files[index];
let path = path.join(filePath, file); //获取当前文件绝对路径
fs.stat(path, function (err, stats) {
if (err) {
console.warn("获取文件stats失败");
if (onError && typeof onError == "function") {
onError(path, err);
}
} else {
// 对文件的处理回调,可通过allowExts数组过滤指定需要的文件类型的路径,不指定则默认允许所有类型
if (stats.isFile()) {
const names = path.split(".");
const ext = names[names.length - 1];
if (
!allowExts ||
allowExts.length == 0 ||
(allowExts.length && allowExts.includes(ext))
) {
findFiles.push(path);
if (onFile && typeof onFile == "function") {
onFile(path, ext, stats);
}
}
}
// 这里是对文件夹的回调,可通过ignoreDirs数组过滤不想遍历的文件夹路径
if (stats.isDirectory()) {
if (
ignoreDirs.length == 0 ||
(ignoreDirs.length && !ignoreDirs.includes(path))
) {
print(path); //递归遍历
}
}
}
});
}
});
};
print(dirPath);
}