【实用函数】将文件路径集合解析为目录菜单

82 阅读3分钟

将文件路径集合解析为目录菜单

开发过程中,若有遇到需要将含有文件路径的数组作为页面菜单使用的场景,那么本文的内容还算应景。

场景示例

本文主题虽然解决的是文件路径转目录的场景,但思路是通用的,也可以变通作为解决其他类似问题的参考。

接下来举一个问题场景,比如,现有一份文件路径集合,需要转为对应的目录结构,其数据与结果如下:

// 数据源
const dataSource = [
  {
    path: "user/demo/index.js",
  },
  {
    path: "user/example/index.js",
  },
  {
    path: "other/docs/index.md",
  },
]

// 期望输出
const menu = [
  {
    name:"user",
    children:[
      {
        name:"demo",
        children:[
          {
            name:"index.js",
            children:[]
          }
        ]
      },
      {
        name:"example",
        children:[
          {
            name:"index.js",
            children:[]
          }
        ]
      }
    ]
  },
  {
    name:"other",
    children:[
      {
        name:"docs",
        children:[
          {
            name:"index.md",
            children:[]
          }
        ]
      }
    ]
  }
]

思路

这种场景其实处理起来不是那么麻烦,通常在开发过程中比较耗时的都是设计思路。

如上,需要将文件路径转为目录,我们可以来拆解下实现思路。

先聊一个场景,若是只有'a/b/c'这么一段字符串生成目录,是不是很简单。

大概过程应是:首先split('/'), 然后循环数组,组装数据结构,依次向上一级的数据结构push当前值即可。

现在需要处理的数据由单一字符串升级为了字符串数组。而它们的处理过程是相通的,只不过需要考虑的场景多一些,比如:

  • 当前处理的项在整个结果数据内是否存在,存在就更新,不存在就新增。
  • 如何控制当前数据指针正确的移动。

梳理之后不外乎这两个点,所以实现代码也很简单,就直接贴完整代码了,内有详细备注。

完整代码

以下代码可以作为参考,可以根据具体业务场景进行自定义修改。

// 文件路径解析器 - 生成相应的菜单json
class FilePathParser {
  #result; // 处理结果
  #dataSource; // 数据源

  /**
   * 构造函数 - 初始化一些信息
   * @description 代码内的一些处理节点可以根据实际情况自行扩展的
   * @param {array} dataSource - 源数据,实际使用需要校验下
   */
  constructor(dataSource) {
    this.#result = [];
    this.#dataSource = JSON.parse(JSON.stringify(dataSource));
    this.#init();
  }

  getResult() {
    return JSON.parse(JSON.stringify(this.#result));
  }

  /**
   * 初始化
   */
  #init() {
    this.#formatPath();
  }

  /**
   * 将数组的每项path地址,进行额外处理。
   * @description 将源数据的每项扩展一个属性:pathList
   * @example 如: `'path/index.js' => ['path', 'index.js']`
   */
  #formatPath() {
    const dataSource = this.#dataSource.map(item => {
      // 这里自行根据情况处理即可,如不想展示最后一项文件名index.js,就可以pop下
      item.pathList = item.path.split('/');
      return item;
    });

    // 遍历dataSource,处理每一项,同时与当前result做比较处理
    dataSource.forEach(item => {
      this.#parsePath(item.pathList, this.#result, item);
    });
  } 

  /**
   * 地址解析函数
   * @description 使用递归依次处理路径,将符合的数据做处理。
   * @description 若当前项可以在当前result内找到,说明是同一级,则继续处理下一项数据即可
   * @description 若当前项可以在当前result内找不到,说明是子级,需要先将当前push到result,然后继续处理下一项数据即可
   * @description 下一项的数据通过shift()函数控制,当namespaces为空,说明处理结束了
   * @param {array} namespaces - 路径链路空间,代码的处理会实时改变这个数据,若不想影响源数据,可以拷贝下
   * @param {array} result - 最终数据
   * @param {object} currentItem - 当前处理的对象,可以做自行使用
   */
  #parsePath(namespaces, result, currentItem) {
    // 组装整体数据格式,根据场景调整
    const currentData = {
      name: namespaces[0],
      path: currentItem.path,
      children: [],
    };

    // 在当前result数组查找namespaces[0],判断是否存在
    const findCurrentData = result.find(item => item.name === namespaces[0]);
    // 不存在则新增
    if (!findCurrentData) {
      result.push(currentData);
    }
    // 继续处理下一个值
    namespaces.shift();
    // 若namespaces不为空,说明还有待处理的值
    if (namespaces.length) {
      this.#parsePath(namespaces, findCurrentData?.children || currentData.children, currentItem);
    }
  }
}


// 使用示例

const dataSource = [
  {
    path: "user/demo/index.js",
  },
  {
    path: "user/example/index.js",
  },
  {
    path: "other/docs/index.md",
  },
]

const filePathParser = new FilePathParser(dataSource); 
console.log(filePathParser.getResult());
// => 数据如下:
// [
//   {
//     name:"user",
//     path:"user/demo/index.js",
//     children:[
//       {
//         name:"demo",
//         path:"user/demo/index.js",
//         children:[
//           {
//             name:"index.js",
//             path:"user/demo/index.js",
//             children:[]
//           }
//         ]
//       },
//       {
//         name:"example",
//         path:"user/example/index.js",
//         children:[
//           {
//             name:"index.js",
//             path:"user/example/index.js",
//             children:[]
//           }
//         ]
//       }
//     ]
//   },
//   {
//     name:"other",
//     path:"other/docs/index.md",
//     children:[
//       {
//         name:"docs",
//         path:"other/docs/index.md",
//         children:[
//           {
//             name:"index.md",
//             path:"other/docs/index.md",
//             children:[]
//           }
//         ]
//       }
//     ]
//   }
// ]