用Vue编写一个简单的仿Explorer文件管理器

?大家一定很熟悉你桌面左上角那个小电脑吧,学名Windows资源管理器,几乎所有的工作都从这里开始,文件云端化是一种趋势。怎样用浏览器实现一个Web版本的Windows资源管理器呢?今天来用Vue好好盘一盘它。
一、导航原理
首先操作和仔细观察导航栏,我们有几个操作途径:

  • 点击“向上”按钮回到上一个目录,点击地址栏的文件夹名称返回任意一个目录
  • 双击文件夹进入新目录
  • 点击“前进”,“后退”按钮操作导航
其中前进,后退操作,可以点击小三角查看一个列表,点击进入文件夹,列表会记录导航历史,哪怕反复进入同一个文件夹,列表仍然会记录下来,如下图:
用Vue编写一个简单的仿Explorer文件管理器
文章图片
用Vue编写一个简单的仿Explorer文件管理器
文章图片
?

那么我们就能分析并抽象出两个变量:
  1. 一个用于存储实际导航的变量(navigationStack)
  2. 另一个用于存储导航历史的变量(navigationHistoryStack)
导航堆栈用于存储每一个浏览文件夹的信息,拼接起这些文件夹就形成了当前路径, 一组简单的
  • 元素通过绑定导航堆栈,就能形成地址栏(web世界里也叫面包屑导航)了。
    navigationStack实际上是一个堆栈,用的是先进后出(FILO)原则
    导航历史则是单纯记录了用户的操作轨迹,不会收到导航目标的影响,如刚才所述,哪怕反复进入同一个文件夹,列表仍然会记录下来
    navigationHistoryStack实际上是一个队列,用的是先进先出(FIFO)原则
    接下来我们开始码代码
    我们先新建一个Vue项目(Typescript),打开App.vue文件
    script标签里编写代码如下:

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片

    二、文件夹跳转原理
    我们先来看如下数据结构
    export class FileDto { id: number; //唯一id parentId: number; //父id fileName: string; //文件名称 fileType: number; //文件类型:1-文件夹,2-常规文件 byteSize: number; //文件大小 }

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    FileDto是定义的文件描述类,这是描述一整个树形结构的基本单元,通过唯一id和指定它的上级parentId,通过递归就可以描述你的某一文件,某一文件夹具体在哪一层级的哪一个分支中。现在假设我们有一堆的文件树长这样:
    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    ?
    定义查询函数checkMessage和当前目录层级的文件集合listMessage:
    listMessage: new Array(), checkMessage: {},

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    再定义一个目录访问器gotoList函数,通过传入查询条件,更新当前目录层级的文件列表:
    gotoList() { this.listMessage = Enumerable.from(FileList) .where((c) => c.parentId == (this.checkMessage as any).parentId) .toArray(); },

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    编写UI部分,简单定义一个table,并绑定文件集合listMessage来显示所有文件:
    id 文件名 类型 大小
    {{ item.id }} {{ item.fileName }} {{ item.fileType == 1 ? "目录" : "文件" }} {{ item.fileType == 1 ? "/" : `${item.byteSize}M` }}

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    当调用gotoList函数的时候,相当与“刷新”功能,获取了当前查询条件下的所有文件
    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    ?
    三、编写导航逻辑
    导航堆栈处理函数
    刚刚我们分析了导航原理,导航堆栈的作用是形成地址,我们定义一个导航堆栈处理逻辑:
    1. 判断当前页面是否在导航堆栈中
    2. 若是,则弹出至目标在导航堆栈中所在的位置
    3. 若否,则压入导航堆栈
    其中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); } }

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    “向上”导航函数:
    向上的作用属于一个特定的导航堆栈处理:
    1. 直接弹出最上的条目,
    2. 拿到最上层条目并导航
    navigationBack() { this.NavigationStack.pop(); var lastItem = Enumerable.from(this.NavigationStack).lastOrDefault(); if (this.getIsNull(lastItem)) { return; } if (this.toFolder(lastItem)) { this.NavigationHistoryStack.push(lastItem); } }

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    定义跳转函数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; },

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    简单的写一下导航操作区域和地址栏的Ui界面:
    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    ?

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    四、编写历史导航处理逻辑
    “后退”函数
    1. 首先确定当前页面在历史导航的哪个位置
    2. 拿到角标后+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); } }

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    “前进”函数
    1. 首先确定当前页面在历史导航的哪个位置
    2. 拿到角标后-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); } }

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    然后我们需要一个函数,用于显示历史队列中(当前)标签:
    getIsCurrentHistoryNavigationItem(item) { var itemIndex = Enumerable.from(this.NavigationHistoryStack).indexOf( (c) => c.id == item.id ); var result = (this.checkMessage as any).parentId == itemIndex; return result; }

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    简单的写一下导航操作区域:
    导航按钮以及历史列表:
    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    ?
    代码如下:
    • {{ item.fileName }} (当前)

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    五、问题修复与优化
    问题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); } }

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    判断是否为当前的函数则简化为如下:
    getIsCurrentHistoryNavigationItem(item) { var result = item["isCurrent"]; return result; },

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    从导航历史队列跳转的目录,也需要处理导航堆栈,因此从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); } },

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    “前进”函数与“后退”函数分别改写为:
    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; } } },

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    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; } } },

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    问题2:文件描述对象重叠
    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    ?

    先看现象,重复进入“文件夹A”的时候,都标记为(当前),这显然是错误的
    请留意navigationTo中的这一段代码:
    if (this.toFolder(folder)) { this.navigationHistoryStack.forEach((element) => { element["isCurrent"] = false; }); folder["isCurrent"] = true; this.navigationHistoryStack.unshift(folder); }

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    这里隐藏了一个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); },

    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    这里加入了一个控制,历史队列最多容纳10个条目,大于10个有新的条目入队列时,将剔除最后一条(也就是最早的一条记录,记录越早角标越大)。
    接下来运行yarn serve来看看最终效果:
    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    用Vue编写一个简单的仿Explorer文件管理器
    文章图片
    ?

    代码仓库:
    jevonsflash/vue-explorer-sample (github.com)
    jevonsflash/vue-explorer-sample (gitee.com)


    【用Vue编写一个简单的仿Explorer文件管理器】?

      推荐阅读