用Vue編寫一個簡單的仿Explorer文件管理器


大家一定很熟悉你桌面左上角那個小電腦吧,學名Windows資源管理器,幾乎所有的工作都從這里開始,文件雲端化是一種趨勢。怎樣用瀏覽器實現一個Web版本的Windows資源管理器呢?今天來用Vue好好盤一盤它。

一、導航原理

首先操作和仔細觀察導航欄,我們有幾個操作途徑:

  • 點擊“向上”按鈕回到上一個目錄,點擊地址欄的文件夾名稱返回任意一個目錄
  • 雙擊文件夾進入新目錄
  • 點擊“前進”,“后退”按鈕操作導航

其中前進,后退操作,可以點擊小三角查看一個列表,點擊進入文件夾,列表會記錄導航歷史,哪怕反復進入同一個文件夾,列表仍然會記錄下來,如下圖:

 

那么我們就能分析並抽象出兩個變量:

  1. 一個用於存儲實際導航的變量(navigationStack)
  2. 另一個用於存儲導航歷史的變量(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函數的時候,相當與“刷新”功能,獲取了當前查詢條件下的所有文件

三、編寫導航邏輯

導航堆棧處理函數

剛剛我們分析了導航原理,導航堆棧的作用是形成地址,我們定義一個導航堆棧處理邏輯:

  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); } }

“向上”導航函數:

向上的作用屬於一個特定的導航堆棧處理:

  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); } }

定義跳轉函數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. 首先確定當前頁面在歷史導航的哪個位置
  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); } } 

“前進”函數

  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); } }

然后我們需要一個函數,用於顯示歷史隊列中(當前)標簽:

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)



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM