乐趣区

关于node.js:告诉你Nodejs的fs模块扫描文件夹什么时候结束了

原文见我的公众号文章 优雅的让你晓得 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",});
}

最初👂我解释

扫描指定门路下所有文件的根本流程

  1. 通过 fs.readdir 办法读取指定的门路,在此方的回调函数里会返回该门路下的 files
  2. 遍历这些 files 并通过 fs.stat 办法判断类型(是文件还是文件夹);
  3. 如果是文件夹,则反复 1 和 2;
  4. 如果是文件,则记录下来;
  5. 直到程序完结,阐明扫描结束!

如何晓得扫描完结?

要做到这点,首先要保障办法外部扫描过程是同步执行的。

异步过于不可控,它会让代码的执行程序变得凌乱,所以就会选用同步扫描办法 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);
}
退出移动版