将文件路径数组拆分为 JavaScript 中的分层对象

Split array of file paths into hierarchical object in JavaScript

使用 JSZip 解压缩文件时会给我一个文件夹和文件列表。例如当我 运行

files.forEach((relativePath, file) => {
  console.log(relativePath);
});

我得到:

three-dxf-master/
three-dxf-master/.DS_Store
three-dxf-master/.gitignore
three-dxf-master/LICENSE
three-dxf-master/README.md
three-dxf-master/bower.json
three-dxf-master/bower_components/

其中一些项目是目录,一些是文件。 我可以通过检查 file.dir 来判断哪些是目录。 我想将其拆分为分层数据结构。我想这样拆分:

{
  "three-dxf-master": [
    ".DS_Store",
    ".gitignore",
    "LICENSE",
    "README.md",
    "bower.json",
    {
      "bower_components": [
        ".DS_Store",
        {
          "dxf-parser": [...]
        }
      ]
    }
  ]
}

这样我就可以将它发送到 Vue 并在一个漂亮的文件查看器中对其进行格式化。我查看了文档,但没有看到为文件创建分层数据结构的简单方法。我开始通过在拆分后抓取文件路径中的最后一个来对此进行调查。

这是一个示例代码,它也处理根目录下的文件。

参见下面代码段的解释

var paths = [
    "three-dxf-master/",
    "three-dxf-master/.DS_Store",
    "three-dxf-master/.gitignore",
    "three-dxf-master/LICENSE",
    "three-dxf-master/README.md",
    "three-dxf-master/bower.json",
    "three-dxf-master/bower_components/",
    "three-dxf-master/bower_components/.DS_Store",
    "three-dxf-master/bower_components/dxf-parser/",
    "three-dxf-master/bower_components/dxf-parser/foo",
    "three-dxf-master/bower_components/dxf-parser/bar",
    "three-dxf-master/dummy_folder/",
    "three-dxf-master/dummy_folder/foo",
    "three-dxf-master/dummy_folder/hello/",
    "three-dxf-master/dummy_folder/hello/hello",
]

// Extract a filename from a path
function getFilename(path) {
    return path.split("/").filter(function(value) {
        return value && value.length;
    }).reverse()[0];
}

// Find sub paths
function findSubPaths(path) {
    // slashes need to be escaped when part of a regexp
    var rePath = path.replace("/", "\/");
    var re = new RegExp("^" + rePath + "[^\/]*\/?$");
    return paths.filter(function(i) {
        return i !== path && re.test(i);
    });
}

// Build tree recursively
function buildTree(path) {
    path = path || "";
    var nodeList = [];
    findSubPaths(path).forEach(function(subPath) {
        var nodeName = getFilename(subPath);
        if (/\/$/.test(subPath)) {
            var node = {};
            node[nodeName] = buildTree(subPath);
            nodeList.push(node);
        } else {
            nodeList.push(nodeName);
        }
    });
    return nodeList;
}

// Build tree from root
var tree = buildTree();

// By default, tree is an array
// If it contains only one element which is an object, 
// return this object instead to match OP request
if (tree.length == 1 && (typeof tree[0] === 'object')) {
    tree = tree[0];
}

// Serialize tree for debug purposes
console.log(JSON.stringify(tree, null, 2));

说明

function getFilename(path) {
    return path.split("/").filter(function(value) {
        return value && value.length;
    } ).reverse()
    [0];
}

To get filename, path is splitted by /.

/path/to/dir/ => ['path', 'to', 'dir', '']

/path/to/file => ['path', 'to', 'file']

Only values with a length are kept, this handle dir path.

The filename is the last value of our array, to get it we simple reverse the array and get the first element.

function findSubPaths(path) {
    // slashes need to be escaped when part of a regexp
    var rePath = path.replace("/", "\/");
    var re = new RegExp("^" + rePath + "[^\/]*\/?$");
    return paths.filter(function(i) {
        return i !== path && re.test(i);
    });
}

To find sub paths of a path, we use a filter on paths list.

The filter use a regular expression (a demo is available here) to test if a path is starting with the parent path and ending with either a / (this is a dir path) or end of line (this is a file path).

If the tested path isn't equal to parent path and match the regexp, then it's accepted by the filter. Otherwise it's rejected.

function buildTree(path) {
    path = path || "";
    var nodeList = [];
    findSubPaths(path).forEach(function(subPath) {
        var nodeName = getFilename(subPath);
        if(/\/$/.test(subPath)) {
            var node = {};
            node[nodeName] = buildTree(subPath);
            nodeList.push(node);            
        }
        else {
            nodeList.push(nodeName);
        }   
    });
    return nodeList;
}

Now that we have methods to extract a filename from a path and to find sub paths, it's very easy to build our tree. Tree is a nodeList.

If sub path ends with / then it's a dir and we call buildTree recursively before appending the node to nodeList.

Otherwise we simply add filename to nodeList.

附加代码

if (tree.length == 1 && (typeof tree[0] === 'object')) {
    tree = tree[0];
}

By default, returned tree is an array.

To match OP request, if it contains only one element which is an object, then we return this object instead.

您可以将行拆分为记录,然后将每条记录拆分为字段。处理时判断一个字段是目录还是文件。如果是一个目录,看看它是否是一个子目录,如果不存在则创建它。那就搬进去吧。

如果是文件,直接压入当前目录即可。

OP 中的格式不允许根目录中的文件,因此如果遇到以下错误,则会抛出错误。要允许根目录下的文件,基础对象必须是数组(不过好像是对象)

以下还允许路径以任何顺序排列并且不按顺序创建,例如它将接受:

foobar/fum

不需要:

foobar/
foobar/fum

希望评论够用

var data = 'three-dxf-master/' +
           '\nfoobar/fumm' +
           '\nthree-dxf-master/.DS_Store' +
           '\nthree-dxf-master/.gitignore' +
           '\nthree-dxf-master/LICENSE' +
           '\nthree-dxf-master/README.md' +
           '\nthree-dxf-master/bower.json' +
           '\nthree-dxf-master/bower_components/' +
           '\nthree-dxf-master/bower_components/.DS_Store' +
           '\nthree-dxf-master/bower_components/dxf-parser/';

function parseData(data) {
  var records = data.split(/\n/);
  var result = records.reduce(function(acc, record) {
    var fields = record.match(/[^\/]+\/?/g) || [];
    var currentDir = acc;
       
    fields.forEach(function (field, idx) {

      // If field is a directory...
      if (/\/$/.test(field)) {
        
        // If first one and not an existing directory, add it
        if (idx == 0) {
          if (!(field in currentDir)) {
            currentDir[field] = [];
          }
          
          // Move into subdirectory
          currentDir = currentDir[field];
          
        // If not first, see if it's a subdirectory of currentDir
        } else {
          // Look for field as a subdirectory of currentDir
          var subDir = currentDir.filter(function(element){
            return typeof element == 'object' && element[field];
          })[0];
          
          // If didn't find subDir, add it and set as currentDir
          if (!subDir) {
            var t = Object.create(null);
            t[field] = [];
            currentDir.push(t);
            currentDir = t[field];
            
          // If found, set as currentDir
          } else {
            currentDir = subDir[field];
          }
        }
        
      // Otherwise it's a file. Make sure currentDir is a directory and not the root
      } else {
        if (Array.isArray(currentDir)) {
          currentDir.push(field);
          
        // Otherwise, must be at root where files aren't allowed
        } else {
          throw new Error('Files not allowed in root: ' + field);
        }
      }
    });
    
    return acc;
    
  }, Object.create(null));
  return result;
}

//console.log(JSON.stringify(parseData(data)));
console.log(parseData(data));

信息

在尝试了此页面中的所有解决方案后正在寻找实现,每个解决方案都有错误。

终于找到了this

解决方案

您需要在 jszip 路径输出中添加“/”才能使用该算法,您可以使用 forEach 循环。

var paths = [
    '/FolderA/FolderB/FolderC/Item1',
    '/FolderA/FolderB/Item1',
    '/FolderB/FolderD/FolderE/Item1',
    '/FolderB/FolderD/FolderE/Item2',
    '/FolderA/FolderF/Item1',
    '/ItemInRoot'
];

function arrangeIntoTree(paths, cb) {
    var tree = [];

    // This example uses the underscore.js library.
    _.each(paths, function(path) {

        var pathParts = path.split('/');
        pathParts.shift(); // Remove first blank element from the parts array.

        var currentLevel = tree; // initialize currentLevel to root

        _.each(pathParts, function(part) {

            // check to see if the path already exists.
            var existingPath = _.findWhere(currentLevel, {
                name: part
            });

            if (existingPath) {
                // The path to this item was already in the tree, so don't add it again.
                // Set the current level to this path's children
                currentLevel = existingPath.children;
            } else {
                var newPart = {
                    name: part,
                    children: [],
                }

                currentLevel.push(newPart);
                currentLevel = newPart.children;
            }
        });
    });

    cb(tree);
}

arrangeIntoTree(paths, function(tree) {
    console.log('tree: ', tree);
});

我还需要在交互式树中显示数据,我使用了 angular-tree-control,它接受确切的格式。