用Vue编写一个简单的仿Explorer文件管理器
大家一定很熟悉你桌面左上角那个小电脑吧,学名Windows资源管理器,几乎所有的工作都从这里开始,文件云端化是一种趋势。怎样用浏览器实现一个Web版本的Windows资源管理器呢?今天来用Vue好好盘一盘它。
一、导航原理
首先操作和仔细观察导航栏,我们有几个操作途径:
- 点击“向上”按钮回到上一个目录,点击地址栏的文件夹名称返回任意一个目录
- 双击文件夹进入新目录
- 点击“前进”,“后退”按钮操作导航
其中前进,后退操作,可以点击小三角查看一个列表,点击进入文件夹,列表会记录导航历史,哪怕反复进入同一个文件夹,列表仍然会记录下来,如下图:
那么我们就能分析并抽象出两个变量:
- 一个用于存储实际导航的变量(navigationStack)
- 另一个用于存储导航历史的变量(navigationHistoryStack)
导航堆栈用于存储每一个浏览文件夹的信息,拼接起这些文件夹就形成了当前路径, 一组简单的<li>元素通过绑定导航堆栈,就能形成地址栏(web世界里也叫面包屑导航)了。
navigationStack实际上是一个堆栈,用的是先进后出(FILO)原则
导航历史则是单纯记录了用户的操作轨迹,不会收到导航目标的影响,如刚才所述,哪怕反复进入同一个文件夹,列表仍然会记录下来
navigationHistoryStack实际上是一个队列,用的是先进先出(FIFO)原则
接下来我们开始码代码
我们先新建一个Vue项目(Typescript),打开App.vue文件
script标签里编写代码如下:
<script lang='ts'>
export default {
name: "App",
data: () => {
return {
navigationStack: new Array<FileDto>(),
navigationHistoryStack: new Array<FileDto>(),
};
}
}
</script>
二、文件夹跳转原理
我们先来看如下数据结构
export class FileDto {
id: number; //唯一id
parentId: number; //父id
fileName: string; //文件名称
fileType: number; //文件类型:1-文件夹,2-常规文件
byteSize: number; //文件大小
}
FileDto是定义的文件描述类,这是描述一整个树形结构的基本单元,通过唯一id和指定它的上级parentId,通过递归就可以描述你的某一文件,某一文件夹具体在哪一层级的哪一个分支中。现在假设我们有一堆的文件树长这样:
定义查询函数checkMessage和当前目录层级的文件集合listMessage:
listMessage: new Array<FileDto>(),
checkMessage: {},
再定义一个目录访问器gotoList函数,通过传入查询条件,更新当前目录层级的文件列表:
gotoList() {
this.listMessage = Enumerable.from(FileList)
.where((c) => c.parentId == (this.checkMessage as any).parentId)
.toArray();
},
编写UI部分,简单定义一个table,并绑定文件集合listMessage来显示所有文件:
<table border="1">
<tr>
<th>id</th>
<th>文件名</th>
<th>类型</th>
<th>大小</th>
</tr>
<tr v-for="item in listMessage" :key="item.id">
<td>{{ item.id }}</td>
<td>
<a href="javascript:void(0)" @click="open(item)">{{
item.fileName
}}</a>
</td>
<td>{{ item.fileType == 1 ? "目录" : "文件" }}</td>
<td>{{ item.fileType == 1 ? "/" : `${item.byteSize}M` }}</td>
</tr>
</table>
当调用gotoList函数的时候,相当与“刷新”功能,获取了当前查询条件下的所有文件
三、编写导航逻辑
导航堆栈处理函数
刚刚我们分析了导航原理,导航堆栈的作用是形成地址,我们定义一个导航堆栈处理逻辑:
- 判断当前页面是否在导航堆栈中
- 若是,则弹出至目标在导航堆栈中所在的位置
- 若否,则压入导航堆栈
其中toFolder函数用于实际导航并刷新页面的,稍后介绍
navigationTo(folder: FileBriefWithThumbnailDto) {
var toIndex = Enumerable.from(this.NavigationStack).indexOf(folder);
if (toIndex >= 0) {
this.NavigationStack.splice(
toIndex + 1,
this.NavigationStack.length - toIndex - 1
);
} else {
this.NavigationStack.push(folder);
}
if (this.toFolder(folder)) {
this.navigationHistoryStack.unshift(folder);
}
}
“向上”导航函数:
向上的作用属于一个特定的导航堆栈处理:
- 直接弹出最上的条目,
- 拿到最上层条目并导航
navigationBack() {
this.NavigationStack.pop();
var lastItem = Enumerable.from(this.NavigationStack).lastOrDefault();
if (this.getIsNull(lastItem)) {
return;
}
if (this.toFolder(lastItem)) {
this.NavigationHistoryStack.push(lastItem);
}
}
定义跳转函数toFolder,之后许多函数引用此函数,这个函数单纯执行跳转,传入文件描述对象,执行导航,刷新页面,返回bool值代表成功与否:
toFolder(folder: FileDto) {
if ((this.checkMessage as any).parentId == folder.id) {
return false;
}
(this.checkMessage as any).parentId = folder.id;
this.gotoList();
return true;
},
简单的写一下导航操作区域和地址栏的Ui界面:
<div class="crumbs">
<ul>
<li v-for="(item, index) in navigationStack" :key="item.id">
{{ index > 0 ? " /" : "" }}
<a href="javascript:void(0)" @click="navigationTo(item)">{{
item.fileName
}}</a>
</li>
</ul>
</div>
四、编写历史导航处理逻辑
“后退”函数
- 首先确定当前页面在历史导航的哪个位置
- 拿到角标后+1(因为是队列,所以越早的角标越大),拿到历史导航队列中后一个页面条目,并执行导航函数
navigationHistoryBack() {
var currentIndex = Enumerable.from(this.NavigationHistoryStack).indexOf(
(c) => c.id == (this.checkMessage as any).parentId
);
if (currentIndex < this.NavigationHistoryStack.length - 1) {
var forwardIndex = currentIndex + 1;
var folder= this.NavigationHistoryStack[forwardIndex]
this.toFolder(folder);
}
}
“前进”函数
- 首先确定当前页面在历史导航的哪个位置
- 拿到角标后-1(因为是队列,所以越晚的角标越小),拿到历史导航队列中前一个页面条目,并执行导航函数
navigationHistoryForward() {
var currentIndex = Enumerable.from(this.NavigationHistoryStack).indexOf(
(c) => c.id == (this.checkMessage as any).parentId
);
if (currentIndex > 0) {
var forwardIndex = currentIndex - 1;
var folder= this.NavigationHistoryStack[forwardIndex]
this.toFolder(folder);
}
}
然后我们需要一个函数,用于显示历史队列中(当前)标签:
getIsCurrentHistoryNavigationItem(item) {
var itemIndex = Enumerable.from(this.NavigationHistoryStack).indexOf(
(c) => c.id == item.id
);
var result = (this.checkMessage as any).parentId == itemIndex;
return result;
}
简单的写一下导航操作区域:
导航按钮以及历史列表:
代码如下:
<div class="buttons">
<div>
<button @click="navigationHistoryBack">
<img
style="transform: rotate(180deg)"
:src="require('@/assets/arr.png')"
/>
</button>
</div>
<div>
<button @click="navigationHistoryForward">
<img :src="require('@/assets/arr.png')" />
</button>
</div>
<div>
<a @click="show">
<img
:src="require('@/assets/arr2.png')"
:style="{
transform: showHistory ? 'rotate(0deg)' : 'rotate(-180deg)',
}"
/>
</a>
</div>
<ul class="history" v-show="showHistory">
<li v-for="(item, index) in navigationHistoryStack" :key="index">
<span>{{ item.fileName }}</span>
<span v-if="getIsCurrentHistoryNavigationItem(item)"> (当前)</span>
</li>
</ul>
<div>
<button @click="navigationBack">
<img
style="transform: rotate(-90deg)"
:src="require('@/assets/arr.png')"
/>
</button>
</div>
</div>
五、问题修复与优化
问题1:历史条目判断错误
测试的时候会发现一个问题,用id判断当前页面所在的堆栈位置,会始终定位到最近一次,相当于FirstOrDefault,因为历史队列可以重复添加,所以需要引入一个isCurrent的bool值属性,来作为判断依据。
这相当于是增加了状态变量,从“无状态”变换成“有状态”,意味着我们要维护这个状态。好处是可以简单的从isCurrent就能判断状态,坏处就是要另写代码维护状态,增加了代码的复杂性。
将navigationTo函数改写成如下:
navigationTo(folder: FileBriefWithThumbnailDto) {
var toIndex = Enumerable.from(this.NavigationStack).indexOf(folder);
if (toIndex >= 0) {
this.NavigationStack.splice(
toIndex + 1,
this.NavigationStack.length - toIndex - 1
);
} else {
this.NavigationStack.push(folder);
}
if (this.toFolder(folder)) {
this.navigationHistoryStack.forEach((element) => {
element["isCurrent"] = false;
});
folder["isCurrent"] = true;
this.navigationHistoryStack.unshift(folder);
}
}
判断是否为当前的函数则简化为如下:
getIsCurrentHistoryNavigationItem(item) {
var result = item["isCurrent"];
return result;
},
从导航历史队列跳转的目录,也需要处理导航堆栈,因此从navigationTo函数中将这一部分剥离出来单独形成函数命名为dealWithNavigationStack:
dealWithNavigationStack(folder) {
var toIndex = Enumerable.from(this.navigationStack).indexOf(
(c) => c.id == folder.id
);
if (toIndex >= 0) {
this.navigationStack.splice(
toIndex + 1,
this.navigationStack.length - toIndex - 1
);
} else {
this.navigationStack.push(folder);
}
},
“前进”函数与“后退”函数分别改写为:
navigationHistoryForward() {
var currentIndex = Enumerable.from(this.navigationHistoryStack).indexOf(
(c) => c["isCurrent"]
);
if (currentIndex > 0) {
var forwardIndex = currentIndex - 1;
var folder = this.navigationHistoryStack[forwardIndex];
this.dealWithNavigationStack(folder);
if (this.toFolder(folder)) {
this.navigationHistoryStack.forEach((element) => {
element["isCurrent"] = false;
});
this.navigationHistoryStack[forwardIndex]["isCurrent"] = true;
}
}
},
navigationHistoryBack() {
var currentIndex = Enumerable.from(this.navigationHistoryStack).indexOf(
(c) => c["isCurrent"]
);
if (currentIndex < this.navigationHistoryStack.length - 1) {
var forwardIndex = currentIndex + 1;
var folder = this.navigationHistoryStack[forwardIndex];
this.dealWithNavigationStack(folder);
if (this.toFolder(folder)) {
this.navigationHistoryStack.forEach((element) => {
element["isCurrent"] = false;
});
this.navigationHistoryStack[forwardIndex]["isCurrent"] = true;
}
}
},
问题2:文件描述对象重叠
先看现象,重复进入“文件夹A”的时候,都标记为(当前),这显然是错误的
请留意navigationTo中的这一段代码:
if (this.toFolder(folder)) {
this.navigationHistoryStack.forEach((element) => {
element["isCurrent"] = false;
});
folder["isCurrent"] = true;
this.navigationHistoryStack.unshift(folder);
}
这里隐藏了一个bug,逻辑是将所有的历史队列条目去除当前标记,然后将最新的目标标记为当前并压入历史队列,这里的 folder这一对象来自于listMessages,
JavaScript在5中基本数据类型(Undefined、Null、Boolean、Number和String)之外的类型,都是按地址访问的,因此赋值的是对象的引用而不是对象本身,当重复进入文件夹时,folder与上一次进入添加到队列中的folder,实际上是同一个对象!
因此所有的“文件夹A”都被标记为“(当前)”了
我们需要将 this.navigationHistoryStack.unshift(folder);改写,提取出一个名称为pushNavigationHistoryStack的入队函数:
pushNavigationHistoryStack(item) {
var newItem = Object.assign({}, item);
if (this.navigationHistoryStack.length > 10) {
this.navigationHistoryStack.pop();
}
this.navigationHistoryStack.unshift(newItem);
},
这里加入了一个控制,历史队列最多容纳10个条目,大于10个有新的条目入队列时,将剔除最后一条(也就是最早的一条记录,记录越早角标越大)。
接下来运行yarn serve来看看最终效果:
代码仓库:
jevonsflash/vue-explorer-sample (github.com)
jevonsflash/vue-explorer-sample (gitee.com)
用Vue编写一个简单的仿Explorer文件管理器的更多相关文章
- Java入门篇(一)——如何编写一个简单的Java程序
最近准备花费很长一段时间写一些关于Java的从入门到进阶再到项目开发的教程,希望对初学Java的朋友们有所帮助,更快的融入Java的学习之中. 主要内容包括JavaSE.JavaEE的基础知识以及如何 ...
- 编写一个简单的C++程序
编写一个简单的C++程序 每个C++程序都包含一个或多个函数(function),其中一个必须命名为main.操作系统通过调用main来运行C++程序.下面是一个非常简单的main函数,它什么也不干, ...
- 使用Java编写一个简单的Web的监控系统cpu利用率,cpu温度,总内存大小
原文:http://www.jb51.net/article/75002.htm 这篇文章主要介绍了使用Java编写一个简单的Web的监控系统的例子,并且将重要信息转为XML通过网页前端显示,非常之实 ...
- 编写一个简单的Web Server
编写一个简单的Web Server其实是轻而易举的.如果我们只是想托管一些HTML页面,我们可以这么实现: 在VS2013中创建一个C# 控制台程序 编写一个字符串扩展方法类,主要用于在URL中截取文 ...
- javascript编写一个简单的编译器(理解抽象语法树AST)
javascript编写一个简单的编译器(理解抽象语法树AST) 编译器 是一种接收一段代码,然后把它转成一些其他一种机制.我们现在来做一个在一张纸上画出一条线,那么我们画出一条线需要定义的条件如下: ...
- 用 Go 编写一个简单的 WebSocket 推送服务
用 Go 编写一个简单的 WebSocket 推送服务 本文中代码可以在 github.com/alfred-zhong/wserver 获取. 背景 最近拿到需求要在网页上展示报警信息.以往报警信息 ...
- 用C语言编写一个简单的词法分析程序
问题描述: 用C或C++语言编写一个简单的词法分析程序,扫描C语言小子集的源程序,根据给定的词法规则,识别单词,填写相应的表.如果产生词法错误,则显示错误信息.位置,并试图从错误中恢复.简单的恢复方法 ...
- 用Python编写一个简单的Http Server
用Python编写一个简单的Http Server Python内置了支持HTTP协议的模块,我们可以用来开发单机版功能较少的Web服务器.Python支持该功能的实现模块是BaseFTTPServe ...
- 手把手教你编写一个简单的PHP模块形态的后门
看到Freebuf 小编发表的用这个隐藏于PHP模块中的rootkit,就能持久接管服务器文章,很感兴趣,苦无作者没留下PoC,自己研究一番,有了此文 0×00. 引言 PHP是一个非常流行的web ...
随机推荐
- 【Python爬虫】爬虫利器 requests 库小结
requests库 Requests 是一个 Python 的 HTTP 客户端库. 支持许多 HTTP 特性,可以非常方便地进行网页请求.网页分析和处理网页资源,拥有许多强大的功能. 本文主要介绍 ...
- 1.k8s的前世今生
k8s是Kubernetes的缩写,Google 于 2014 年开源了 Kubernetes 项目. 一.k8s的历史演变 k8s的演变过程:首先从传统的服务-->虚拟机部署-->容器部 ...
- Swift可选类型
可选类型 可选类型的介绍 注意: 可选类型时swift中较理解的一个知识点 暂时先了解,多利用Xcode的提示来使用 随着学习的深入,慢慢理解其中的原理和好处 概念: 在OC开发中,如果一个变量暂停不 ...
- js图片预览代码
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- Java线程--BlockingQueue使用
原创:转载需注明原创地址 https://www.cnblogs.com/fanerwei222/p/11871704.html Java线程--BlockingQueue使用 阻塞队列就是内容满了之 ...
- CSS3自定义滚动条样式-webkit内核
自定义滚动条实现 此部分针对webkit内核的浏览器,使用伪类来改变滚动条的默认样式,详情如下: 滚动条组成部分 1. ::-webkit-scrollbar 滚动条整体部分 2. ::-webkit ...
- pytest-html 测试报告
前言 上一篇文章pytest简介中,执行测试用例后,在 pycharm 控制台(方式一)或 Terminal(方式二)中可以查看测试结果.但是在实际的接口自动化项目中一般需要生成直观的测试报告,这个测 ...
- Ansible playbook实现apache批量部署,并对不同主机提供以各自IP地址为内容的index.html
1.基于key验证免密授权 1.1 生成kekgen # ssh-keygen Generating public/private rsa key pair. Enter file in which ...
- webhook触发jenkins进行sonar检测
目的 jenkins仅需创建一个job,git推送后自动进行sonar代码检测并上传到sonarqube jenkins插件 已按社区推荐安装基本插件 Generic Webhook Trigger ...
- netstat 竟然还能这么玩儿?
一次摸鱼的机会,看到群里小伙伴问了一嘴 netstat -tnpl 这个命令是干啥的,这个命令用过很多,但是我其实也没有认真研究过,但是这是一个问题,我不能放过它,而且 netstat 这个命令我日常 ...