好巧前端

Node仿Tree指定层级输出树形文件目录结构

2018-02-06  本文已影响87人  临水照影233

前言

这段时间空余时间蛮少,后来特地腾出晚上的时间来开发自己的玩具。 今天讲的是为玩具所开发的一个小模块的一个功能。 具体来说它是一个仿Tree命令能够罗列给定目录的树形结构。而且我的做法是开发一个命令行工具,但这里先不提,我们就专注这个目录列表功能。 需要输出相同的结构。我们先来看下需要达到的效果

正文

我们先来探讨一下如何获得目录结构。因为我们最终需要的是给路径->获得目录结构->数据处理(输出/另外操作)

在Node里有这么几个API

fs.readdir(path, callback)

该方法是 readdir(3) 的异步执行版本,用于读取一个目录的内容。callback 接收两个参数 (err, files),其中 files 是一个数组,数组成员为当前目录下的文件名,不包含 . 和 ..。

fs.readdirSync(path)

该方法是 readdir(3) 的同步执行版本,返回一个不包含 . 和 .. 的文件名数组。

...这里就列两个,其他的请自行去官网查API0-0

一开始我试图用异步方法来获取,但是最后因为没处理好Promise和其它异步操作,导致最后数据没有存储下来。因此现在我们讲的是用同步方式获取。异步方式也将在这个模块写完之后再去写一版本。

基本上网上的方法都是递归调用方法来获取整个目录结构。

这里也不例外。不过我们需要注意我们需要的将整个目录关系都缓存在一个Object里。而且因为我需要指定层级目录输出。因此每个文件/目录还将有一个属性就是deep,它作为一个告诉调用者该目录层级的深度。如果是0,说明是指定目录的根目录下同级的文件/目录。依次类推。

当然这里就直接贴出代码

_getAllNames: function(level, dir) {

        var filesNameArr = []

        let cur = 0

            // 用个hash队列保存每个目录的深度

        var mapDeep = {}

        mapDeep[dir] = 0

            // 先遍历一遍给其建立深度索引

        function getMap(dir, curIndex) {

            var files = fs.readdirSync(dir) //同步拿到文件目录下的所有文件名

            files.map(function(file) {

                //var subPath = path.resolve(dir, file) //拼接为绝对路径

                var subPath = path.join(dir, file) //拼接为相对路径

                var stats = fs.statSync(subPath) //拿到文件信息对象

                    // 必须过滤掉node_modules文件夹

                if (file != 'node_modules') {

                    mapDeep[file] = curIndex + 1

                    if (stats.isDirectory()) { //判断是否为文件夹类型

                        return getMap(subPath, mapDeep[file]) //递归读取文件夹

                    }

                }

                //console.log(subPath)

            })

        }

        getMap(dir, mapDeep[dir])

            //console.log(mapDeep)

        function readdirs(dir, folderName,myroot) {

            var result = { //构造文件夹数据

                path: dir,

                name: path.basename(path),

                type: 'directory',

                deep: mapDeep[folderName]

            }

            var files = fs.readdirSync(dir) //同步拿到文件目录下的所有文件名

            result.children = files.map(function(file) {

                //var subPath = path.resolve(dir, file) //拼接为绝对路径

                var subPath = path.join(dir, file) //拼接为相对路径

                var stats = fs.statSync(subPath) //拿到文件信息对象

                    //console.log(subPath)

                if (stats.isDirectory()) { //判断是否为文件夹类型

                    // console.log(mapDeep[file])

                    return readdirs(subPath, file,file) //递归读取文件夹

                }

                return { //构造文件数据

                    path: subPath,

                    name: file,

                    type: 'file'

                }

            })

            return result //返回数据

        }

        filesNameArr.push(readdirs(dir, dir))

        return filesNameArr

    },

过滤掉node_modules是必须的,因为开发中这个目录一直存在。。。是个极大干扰源。

这里我代码都加了注释应该很好理解。

我们来输出下缓存的目录结构

{ path: './',

  name: '[object Object]',

  type: 'directory',

  deep: 0,

  children:

  [ { path: '.DS_Store', name: '.DS_Store', type: 'file' },

    { path: '.babelrc', name: '.babelrc', type: 'file' },

    { path: '.gitignore', name: '.gitignore', type: 'file' },

    { path: 'README.MD', name: 'README.MD', type: 'file' },

    { path: 'bin',

      name: '[object Object]',

      type: 'directory',

      deep: 1,

      children: [Object] },

    { path: 'lib',

      name: '[object Object]',

      type: 'directory',

      deep: 1,

      children: [Object] },

    { path: 'package.json', name: 'package.json', type: 'file' },

    { path: 'test',

      name: '[object Object]',

      type: 'directory',

      deep: 1,

      children: [Object] } ] }

[ { path: './',

    name: '[object Object]',

    type: 'directory',

    deep: 0,

    children:

    [ [Object],

      [Object],

      [Object],

      [Object],

      [Object],

      [Object],

      [Object],

      [Object] ] } ]

针对./这个当前目录的路径,程序返回上面的结构。 这样我们就得到了第一步最基础的数据。

现在第一个步骤是为了模仿Tree工具输出命令。

这里给个例子参考:

├── .DS_Store

├── .babelrc

├── .gitignore

├── README.MD

├── bin

│   ├── .DS_Store

│   ├── folderTree.js

│   ├── lib2

│   │   ├── .DS_Store

│   │   └── testa

│   └── testb

│      └── .DS_Store

├── lib

│   ├── .DS_Store

│   ├── folderFactory.js

│   └── testlib

│      └── testlibfile.js

├── package.json

└── test

   ├── .DS_Store

   ├── index.js

    └── testFolder

       ├── .DS_Store

       ├── a

       ├── b

       └── c

它有几个注意点,一个是每当你的文件或者目录是当前层级最后一个,那么输出└──前缀如果是普通的输出├── 前缀。 如果是多层级,那么最前面依旧需要│ 来进行上下层级的联系。

而且还有很多小细节需要处理。

一开始最好的解决方法其实是递归。但是我觉得有点复杂,我想试下迭代方法。最后结果如开头的效果。基本上如果根目录中最后一个file是目录类型。那么它是正常的。但是如果根目录中最后一个文件是文件类型类型。我这段迭代并没有进行处理。

我们先来看下这个有缺陷的代码:

    var stack = [obj[0]]

        var isFinal = false

        function printFolder(arr, folderName, isLastFolder) {

            if (arr.deep <= level) {

                for (var i = 0; i < arr.children.length; i++) {

                    var isLastFile = i == arr.children.length - 1 ? true : false

                    var isRootLast = i == arr.children.length - 1

                    isFinal = isFinal == true ? true :  arr.deep == 0 && arr.children[i].type == 'directory' && i == arr.children.length-1

                    printFile(arr.children[i], folderName, arr.deep, isLastFile, isLastFolder,isFinal)

                    if (arr.children[i].type == 'directory') {

                        //console.log('directory')

                        var t = arr.children[i].path

                        if (i == arr.children.length - 1) {

                            printFolder(arr.children[i], t, true)

                        } else {

                            printFolder(arr.children[i], t, false)

                        }

                    }

                }

            }

        }

        function printFile(file, folderName, deep, isLastFile, isLastFolder) {

            if (file[0] != '.') {

                // console.log("Folder:"+folderName)

                // console.log(deep)

                //console.log(isLastFile)

                //console.log(isLastFolder)

                var name = file.path.replace(folderName + '/', '')

                //console.log(isFinal)

                if (deep == 0) {

                    if (isLastFile) {

                        console.log('└── ' + name)

                    } else {

                        console.log('├── ' + name)

                    }

                }

                if (deep > 0) {

                    if (!isLastFolder) {

                        if (!isLastFile) {

                            console.log('│   '.repeat(deep) + '├── ' + name)

                        } else {

                            console.log('│   '.repeat(deep) + '└── ' + name)

                        }

                    } else {

                        if (!isLastFile) {

                            console.log('    '.repeat(deep) + '├── ' + name)

                        } else {

                        if(isFinal){

                            console.log('    '.repeat(deep) + '└── ' + name)

                        }else{

                          var str = '    '.repeat(deep) + '└── ' + name

                          var temp = str.split('')

                          temp[0] = '│'

                            console.log(temp.join(''))

                        }

                        }

                    }

                }

            }

        }

        printFolder(obj[0], '', false)

        console.log('目录及文件罗列完毕')

我做了很多判断,但还是低估了一个目录它可能的复杂性(目录中有空目录,目录中有空文件,文件和目录的多个嵌套等等)。因为我开发的时候是根据几个example来进行调节判断。当我最后写完,重新再去测试几个目录的时候发现出现上述的file缺陷。 如图: 

因此需要改写为递归方式。我们不可能根据别的属性来进行判断。因此我们需要依赖的还是之前的那份目录结构缓存。

这里也直接贴源码:

_showList(obj, level) {

        var sourceStruct = obj[0]

        var dirCount = 0

        var fileCount = 0

            // 字符集

        var charSet = {

            'node': '├── ', //节点

            'pipe': '│  ', // 上下链接

            'last': '└── ', // 最后的file或folder需要回勾

            'indent': '    ' // 缩进

        };

        function log(file, depth, parentHasNextSibling) {

          // console.log("log:")

            if (!parentHasNextSibling && depth.length > 1) {

                // Replace a pipe with an indent if the parent does not have a next sibling.

                depth[depth.length - 2] = charSet.indent;

            }

            if(file.lastIndexOf('/') > -1){

                file = file.substring(file.lastIndexOf('/')+1)

            }

            console.log(depth.join('') + file);

        }

        // 由于已经有缓存数据了,因此对数据进行遍历搜索

        // 如果type 是file 就不需要继续

        // 如果type 是directory 对临时数组

        function walk(path, depth, parentHasNextSibling) {

          //  console.log(path)

            var childrenLen = path.length - 1

          // console.log(childrenLen)

            var loop = true

            if (depth.length >= level) {

                loop = false

            }

            if (loop) {

                path.forEach(function walkChildren(child, index) {

                    var newdepth = !!depth ? depth.slice(0) : []

                    var isLast = (index >= childrenLen)

                    if (isLast) {

                        newdepth.push(charSet.last)

                    } else {

                        newdepth.push(charSet.node)

                    }

                    if(child.type == "file"){

                      log(child.name, newdepth, parentHasNextSibling)

                    }else{

                      log(child.path, newdepth, parentHasNextSibling)

                    }

                    if (child.type == "directory") {

                        var childPath = child.children

                        if (!isLast) {

                            newdepth.pop()

                            newdepth.push(charSet.pipe)

                        }

                        walk(childPath, newdepth, !isLast)

                    }

                })

                loop = !loop

            }

        }

        walk(sourceStruct.children, [])

        //console.log(sourceStruct)

        console.log('level:' + level)

        console.log('目录及文件罗列完毕')

    },

这里只需要判断文件类型,利用递归的特性将每个文件的路径存于newpath,然后判断长度,如果大于level就跳过。具体注释都在源码里了。

结尾

第一个模块总算完成,四月第一篇文章居然在中旬也算对得起拖延症了~

上一篇 下一篇

猜你喜欢

热点阅读