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