本次分享是对之前写的个人项目CodeasilyX进行一个回顾分享,主要是为了记录这些值得分享的经验,本文只涉及核心代码分享,不包括完整实现和样式实现,重在理解,不为copy

本文指的简单的ide文件管理组件CodeasilyX里面的那个文件管理组件,因为是自己业余时间纯手写的,只有一些基础功能,操作性功能有:新建文件新建文件夹删除文件重命名,辅助性功能有:文件名排序文件夹展开收缩记录文件打开状态。虽然功能少,但底层的基础结构得花不少时间,最后实现这几个功能只是在基础结构上开放出来而已。

功能展示

下面展示这个组件的功能

操作性功能:新建文件新建文件夹删除文件重命名

辅助性功能:文件名排序文件夹展开收缩记录文件打开状态

Your user agent does not support the HTML5 Video element.

结构设计

  1. 首先这个文件管理的目录结构是需要做到可序列化反序列化的,因为只有序列化了才能进行合理的存储和传输,这是一个网络应用不可少的。

  2. 我们还要考虑这个文件管理组件会有什么功能,前面有提到过有四个基本的功能,如果只考虑这四个基本功能的话,那么可以用简单一些的结构设计,如果有更复杂的功能(比如支持创建时间排序、复制剪切粘贴等)那就得设计更适合的结构才能支撑起这些功能。

根据上面的功能展示的动图,对应的结构就是下面这段json数据:

示例文件结构json

从上图可看出每个item有name,type,layer,index,pathchildren这几个字段,这里主要解释一下index这个字段,其他都很好理解,index代表文件在当前文件夹的位置,由层级layer进一步以尖括号>进一步区分。比如0>2代表第1个文件夹里的第3个文件。这个位置在文件操作上起到绝对性作用,它记录了文件的位置层级,从而能够解析如/xxx/xx这种字符串路径快速查找。

代码实现

界面上这种目录结构可以用vue的组件嵌套递归实现,核心的递归组件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<template>
<div class="file-component">
<div v-if="data.type == 'file'" class="file-type" :style="{paddingLeft: (data.layer-1) * 15 + 5 + 'px'}"
:class="{'active': data.path == _state.curFile}"
@click="changeFile(data.path)"
:data-type="data.type"
:data-path="data.path">
{{data.name}}
</div>
<div v-else class="folder-type">
<div class="folder-name" :style="{paddingLeft: (data.layer-1) * 1.5 + 'em'}" :data-type="data.type" :data-path="data.path" :class="{'expanded icon-folder-open': expandStatus, 'icon-folder': !expandStatus}" @click="handle_expand">{{data.name}}</div>
<div v-if="expandStatus">
<!-- 当文件夹展开时递归展示下层文件 -->
<file-component
v-for="(item, index) in data.childrens"
:key="item.name + index"
:data="item"
:changeFile="changeFile"/>
</div>

</div>
</div>
</template>

<script>
export default {
// 为组件命名可以在模板调用自身,达到递归的效果。
name: 'file-component',
props: [
'data',
'isExpand',
'changeFile'
],
data() {
return {
}
},
computed: {
expandStatus() {
return this.data.type == 'folder' ? !!this._state.folderExpand.match(this.data.path) : false
}
},
methods: {
handle_expand() {
if (this._state.isPlaying) {
return;
}
var path = this.data.path
if (~path.indexOf(this._state.folderExpand) || ~this._state.folderExpand.indexOf(path)) {
var isExpand = this._state.folderExpand.match(path);
var resultExpand = isExpand ? path.replace(/\/[^\/]+$/, '') : this._state.folderExpand + path.replace(this._state.folderExpand, '');
this._set({folderExpand: resultExpand});
} else {
this._state.folderExpand = path;
}
},
}
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="less" scoped>
.file-component {
width: 100%;
color: #fff;
}

.file-type {
cursor: pointer;
&:hover,&:active {
background: lighten(#32393D, 5%);
}
&.active {
background: lighten(#32393D, 20%);
}
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.folder-type {
.folder-name {
padding: 0 5px;
line-height: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover,&:active {
background: lighten(#32393D, 5%);
}
cursor: pointer;
&:before {
content: "\e92f";
display: inline-block;
margin-right: 5px;
font-size: 12px;
}
&.expanded {
&:before {
content: "\e930";
}
}
}
}
</style>

核心js

接下来就最核心的部分,管理着结构变动和目录索引的几个纯函数

  • 文件名排序和文件名正则

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 文件名排序
    export function fileNameSort(fileData) {
    return fileData.sort((a, b) => a.name.localeCompare(b.name))
    }

    // 文件路径正则
    export function filePathChack(srcPath) {
    return /^((\.\/)?|(\.\.\/)*|(\/\w+)*\/)\w+\.\w+$/.test(srcPath)
    }
  • 为目录增加层级标识
    这个函数做的就是递归整个目录结构的对象,给每个文件插入层级标识layer,还可以用于更新path字段,每次对文件有操作变动,都要调用这个函数进行更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 给目录增加层级标识
export function layerInclude(data) {
// 简单深拷贝
let newData = JSON.parse(JSON.stringify(data));
// 给文件目录增加层级标识
let layer = 0;
const layerData = function(data, parentIndex='', presetPath='') {
data.map((item, index) => {
index = index + ''
if (index == 0) {
layer++
item.layer = layer
} else {
item.layer = layer = data[index-1].layer;
}
// 记录文件路径位置
item.path = presetPath + '/' + item.name
if (item.childrens) {
item.index = parentIndex + index ;
layerData(fileNameSort(item.childrens), item.index + '>', item.path)
} else {
item.index = parentIndex ? parentIndex + index : index
}
})
}
layerData(fileNameSort(newData));
return newData;
}
  • 解析/xxx/xx字符串路径查找对应的文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 解析具名路径/xxx/xx查找文件对象
    export function pathToFileObj(filesData, path, isFolder) {
    var fileData = JSON.parse(JSON.stringify(filesData));
    const paths = path.split('/').filter((item) => !!item);
    var pathIndex = '';
    paths.map((path) => {
    pathIndex = fileData.findIndex((item) => item.name == path)
    if (~pathIndex) {
    if (fileData[pathIndex].type == 'folder') {
    fileData = fileData[pathIndex].childrens
    } else {
    if (!isFolder) {
    fileData = fileData[pathIndex];
    }
    }
    }
    })
    return fileData
    }
  • 通过index解析出对应的/xxx/xx字符串路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 传入文件index返回/xxx/x
export function indexToPath(filesData, index) {
if (!index) {
return '/'
}
// 先找到index对应的文件数据
var fileData = filesData
var result = '/';
var indexArr = index.split('>');
indexArr.map((key) => {
if (fileData[key].type === 'folder') {
result += fileData[key].name + '/';
fileData = fileData[key].childrens;
} else {
result += fileData[key].name
}
})
return result;
}
  • 对一个路径/xxx/xxx找到对应的位置修改名称返回新路径

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 对一个路径/xxx/xxx找到对应的位置修改名称返回新路径
    export function renamePath(originPath, oldName, newName, layer) {
    let layerPath = originPath.split('/');
    if ( layerPath[layer] === oldName ) {
    layerPath[layer] = newName;
    return layerPath.join('/');
    } else {
    return originPath
    }
    }
  • 最后就是最复杂的操作,通过传入指定的文件索引作为入口,从完整文件结构查找指定路径的对应文件对象,这里基本上每一步都打了注释解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 通过传入指定的文件索引作为入口,从完整文件结构查找指定路径的对应文件
export function getFileByEntryPath(filesData, indexFile, srcPath) {
var fileData = JSON.parse(JSON.stringify(filesData));
// 当前入口文件所属文件夹
fileData = pathToFileObj(fileData, indexFile, 'folder')
// 解析路径转为绝对路径// -- 匹配绝对路径 (/xxx/xxx)
if (/^(\/\w+)+\.*\w+$/.test(srcPath)) {
// 一行代码搞定
return pathToFileObj(filesData, srcPath)
}
// -- 匹配相对当前目录 (./xxx.xx || xxx.xx || ./xxx/xx/xx.xx)
else if (/^(\.\/)/.test(srcPath)) {
// 先把./去掉
srcPath = srcPath.replace(/^(\.\/)/, '')
// 从当前目录找到匹配的文件
return pathToFileObj(fileData, srcPath)
}
// -- 匹配相对上层目录 (../xx/xx.xx || ../../xx)
else if (/^(\.\.\/)+(\w+\/)*/.test(srcPath)) {
let upfileData = filesData;
// 取出所有上层标识../
var uplayerStr = srcPath.match(/^(\.\.\/)+/)[0];
// 取出上层标识之后的路径
let lastPath = srcPath.replace(uplayerStr, '');
// 获取有几层个上层标识
var layer = uplayerStr.match(/..\//g).length;
// 从入口文件索引拆分层级索引
var layerPath = indexFile.split('/');
// 去除最后一个文件的索引
layerPath.pop()
// 取得最终文件夹的索引数组
let folderPath = layerPath.slice(0, layerPath.length - layer - 1);
// 循环得出目标的文件夹
folderPath.map((path) => {
upfileData = upfileData.find((item) => item.path == path).childrens || upfileData;
})
// 最后从上层匹配到的文件夹继续往下找
return pathToFileObj(upfileData, lastPath)
}
}

总结

解释的比较少,主要是阅读代码自己理解学习为主,实现一个简单的文件管理组件需要考虑的还是比较多的,尤其是最初的基础数据格式设计,需要根据需求功能,设计适当的数据格式,性能也是着重考虑的部分,合适的数据格式能让操作性能大幅提升,如果需要更复杂的功能则可能需要重构。

希望本文对你有所帮助~