【问题标题】:javascript to build category tree from flat json array (data structure )javascript 从平面 json 数组(数据结构)构建类别树
【发布时间】:2021-04-19 22:39:35
【问题描述】:

我正在尝试使用 javascript 动态转换给定的平面 JSON 数组以构建类别树。 但是我不确定如何以最佳方式解决这个问题,以便解决方案可以扩展。类别树需要从以“/”分隔的“名称”属性派生。 这是我的初步尝试。

let data = [{"name":"file1.txt"},{"name":"folderA/file2.txt"},{"name":"folderA/folderB/file3.txt"},{"name":"file4.txt"}];
let tree = [];
function buildTree(data){
    data.forEach(obj => {
    let fileNameArr = obj.name.split("/");
    if(fileNameArr.length > 1){
     // Not sure
      }
    } else {
        let node = obj;
        node.type = "file";
        tree.push(node)
    }
  })
}

console.log(buildTree(data));

预期输出是:

[{"name":"file1.txt",
  "type":"file"
  },
 {
  "name":"folderA",
  "type":"Folder",
  "children":[
    {"name":"file2.txt",
     "type":"file"
    },
    {"name":"folderB",
     "type":"folder",
     "children":[{"name":"file3.txt","type":"file"}]
    }]
  },
 {"name":"file4.txt",
   "type":"file"
  }
]

【问题讨论】:

    标签: javascript arrays json recursion data-structures


    【解决方案1】:

    为此,我发现进行两次转换更容易。第一个会将上面的内容转换为这种结构:

    {
        "file1.txt": {
            "name": "file1.txt",
            "type": "file",
            "children": {}
        },
        "folderA": {
            "name": "folderA",
            "type": "folder",
            "children": {
                "file2.txt": {
                    "name": "file2.txt",
                    "type": "file",
                    "children": {}
                },
                "folderB": {
                    "name": "folderB",
                    "type": "folder",
                    "children": {
                        "file3.txt": {
                            "name": "file3.txt",
                            "type": "file",
                            "children": {}
                        }
                    }
                }
            }
        },
        "file4.txt": {
            "name": "file4.txt",
            "type": "file",
            "children": {}
        }
    }
    

    第二个会将其扁平化为一个数组,对孩子们递归地做同样的事情。这是我的方法:

    // utility function
    const call = (fn, ...args) => 
      fn (...args)
    
    // main functions
    const nest = (files) => files .reduce ((tree, file) => {
      file .name .split ('/') .reduce ((node, name, i, a) => {
        return (node [name] || (node[name] = {
          name, 
          type: i == a.length - 1 ? 'file' : 'folder', 
          children: {}
        })).children
      }, tree)
      return tree 
    }, {})
    
    const flattenChildren = (tree) =>
      Object .values (tree)
        .map (({children, ...rest}) => call (
          (kids) => ({
            ... rest, 
            ... (kids.length ? {children: kids} : {})
          }),
          children ? flattenChildren (children) : []
        ))
    
    
    // public function
    const makeTree = (files) =>
      flattenChildren (nest (files))
    
    
    // sample input
    const input = [{name: 'file1.txt'}, {name: 'folderA/file2.txt'}, {name: 'folderA/folderB/file3.txt'}, {name: 'file4.txt'}]
    
    
    // demo
    console .log (makeTree (input))
    .as-console-wrapper {max-height: 100% !important; top: 0}
    • call 是一个简单的辅助函数,它使基于表达式的编程变得更容易并避免不必要的赋值。它接受任意数量的参数,第一个参数是一个函数,然后用剩余的参数调用该函数。

    • nest 将您的输入转化为我描述的中间结构。

    • flattenChildren 将此中间格式转换为最终格式。

    • makeTree是结合以上的公共函数。

    这里的风格有点奇怪。通常,如果我在解决方案的一个关键功能中使用递归技术,我会为所有这些功能使用递归技术。但是在这里,即使flattenChildren 是递归的,我想到的nest 的第一个版本使用嵌套的.reduce 调用。我确信我可以改变这一点,但我现在还没准备好花更多时间在这上面。

    【讨论】:

      【解决方案2】:

      您可以通过查看实际级别并找到想要的名称来迭代数组并构建嵌套文件夹结构。如果找不到,则添加一个新对象并返回子数组以推送文件。

      const
          data = [{ name: "file1.txt" }, { name: "folderA/file2.txt" }, { name: "folderA/folderB/file3.txt" }, { name: "file4.txt" }],
          tree = data.reduce((r, { name }) => {
              const
                  folders = name.split('/'),
                  file = folders.pop();
      
              folders
                  .reduce((level, name) => {
                      let temp = level.find(q => q.name === name);
                      if (!temp) level.push(temp = { name, type: 'folder', children: [] });
                      return temp.children;
                  }, r)
                  .push({ name: file, type: 'file' });
      
              return r;
          }, []);
      
      console.log(tree);
      .as-console-wrapper { max-height: 100% !important; top: 0; }

      一个没有使用find的版本,而是一个包含结果的对象。

      const
          data = [{ name: "file1.txt" }, { name: "folderA/file2.txt" }, { name: "folderA/folderB/file3.txt" }, { name: "file4.txt" }],
          tree = data.reduce((_, { name }) => {
              const
                  folders = name.split('/'),
                  file = folders.pop();
      
              folders
                  .reduce((level, name) => {
                      if (!level[name]) {
                          level[name] = { _: [] };
                          level._.push({ name, type: 'folder', children: level[name]._ });
                      }
                      return level[name];
                  }, { _ })
                  ._
                  .push({ name: file, type: 'file' });
              return _;
          }, []);
      
      console.log(tree);
      .as-console-wrapper { max-height: 100% !important; top: 0; }

      【讨论】:

        【解决方案3】:

        您将整个数组简化为使用 root(基本数组)初始化的 Map。每当您创建一个文件夹时,您还可以使用它的路径作为键将其添加到地图中,同时也将其添加到当前文件夹中。

        获取/添加最后一个文件夹(存储在current)后,将文件添加到当前文件夹。

        整个树存储在 Map 的 root 键中。

        const data = [{ name: "file1.txt" }, { name: "folderA/file2.txt" }, { name: "folderA/folderB/file3.txt" }, { name: "file4.txt" }];
        
        const createFile = name => ({ name, type: 'file' });
        const createFolder = name => ({ name, type: 'folder', children: [] });
        
        const tree = data.reduce((acc, { name }) => {
          const path = name.split('/'); // get the path
          
          const file = createFile(path.pop()); // create the file
          
          let current = acc.get('root'); // init current at the root
          
          // iterate current to get to the last folder, while creating missing levels
          path.forEach((folderName, i) => {
            const key = path.slice(0, i + 1).join('/'); // create the key for the current folder
            
            if(!acc.has(key)) { // if folder not part of the Map, create it
              const folder = createFolder(folderName);
              acc.set(key, folder); // add the folder to the Map
              (current.children || current).push(folder) // add folder to current children / root
            }
          
            current = acc.get(key); // move current to the folder
          });
          
          (current.children || current).push(file) // add file to current children / root
        
          return acc;
        }, new Map([['root', []]])) // init the the Map with a root folder
        .get('root'); // get the tree from the root
        
        console.log(tree);
        .as-console-wrapper { max-height: 100% !important; top: 0; }

        我们可以通过使用while 循环向后迭代path 数组来稍微优化这一点,并在文件夹已经存在或我们位于根目录时停止迭代。

        const data = [{ name: "file1.txt" }, { name: "folderA/file2.txt" }, { name: "folderA/folderB/file3.txt" }, { name: "file4.txt" }];
        
        const createFile = name => ({ name, type: 'file' });
        const createFolder = (name, child) => ({ name, type: 'folder', children: [child] });
        
        const tree = data.reduce((acc, { name }) => {
          const path = name.split('/'); // get the path
          
          let branch = createFile(path.pop()); // create the file
        
          while(path.length && !acc.get(path.join('/'))) { // iterate while path not empty, and folder doesn't exist
            branch = createFolder(path[path.length - 1], branch); // create the folder and add the previous item to the children
            
            acc.set(path.join('/'), branch); // add to the Map
            
            path.pop(); // remove the last folder from the path
          };
          
          acc.get(path.length ? path.join('/') : 'root').children.push(branch); // add to pre-existing folder or to the root
        
          return acc;
        }, new Map([['root', { children: [] }]])) // init the the Map with a root folder
        .get('root').children; // get the tree from the root
        
        console.log(tree);
        .as-console-wrapper { max-height: 100% !important; top: 0; }

        【讨论】:

          猜你喜欢
          • 2017-07-19
          • 1970-01-01
          • 1970-01-01
          • 2019-12-24
          • 2021-02-13
          • 1970-01-01
          • 2016-09-25
          • 2017-05-26
          • 2021-08-21
          相关资源
          最近更新 更多