用Vue編寫一個簡單的仿Explorer檔案管理器

林 小發表於2022-03-11

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

一、導航原理

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

  • 點選“向上”按鈕回到上一個目錄,點選位址列的資料夾名稱返回任意一個目錄
  • 雙擊資料夾進入新目錄
  • 點選“前進”,“後退”按鈕操作導航

其中前進,後退操作,可以點選小三角檢視一個列表,點選進入資料夾,列表會記錄導航歷史,哪怕反覆進入同一個資料夾,列表仍然會記錄下來,如下圖:

用Vue編寫一個簡單的仿Explorer檔案管理器

 

那麼我們就能分析並抽象出兩個變數:

  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>
用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檔案管理器

定義查詢函式checkMessage和當前目錄層級的檔案集合listMessage:

      listMessage: new Array<FileDto>(),
      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來顯示所有檔案:

      <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>
用Vue編寫一個簡單的仿Explorer檔案管理器

當呼叫gotoList函式的時候,相當與“重新整理”功能,獲取了當前查詢條件下的所有檔案

用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檔案管理器

    <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>
用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檔案管理器

程式碼如下: 

<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>
用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檔案管理器

 

先看現象,重複進入“資料夾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檔案管理器

 

 程式碼倉庫:

jevonsflash/vue-explorer-sample (github.com)

jevonsflash/vue-explorer-sample (gitee.com)


相關文章