将文件路径集合解析为目录菜单
开发过程中,若有遇到需要将含有文件路径的数组作为页面菜单使用的场景,那么本文的内容还算应景。
场景示例
本文主题虽然解决的是文件路径转目录的场景,但思路是通用的,也可以变通作为解决其他类似问题的参考。
接下来举一个问题场景,比如,现有一份文件路径集合,需要转为对应的目录结构,其数据与结果如下:
// 数据源
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:[]
// }
// ]
// }
// ]
// }
// ]