踩坑日記-element ui樹形控制元件

linliqzh發表於2018-10-20

最近在做一個管理系統,頁面左側需要一個目錄樹,便於檔案的操作,不想從頭開始造輪子,於是就考慮採用iview或者element的tree,調研後發現iview的tree還是有點侷限,沒有拖拽移動功能,沒有懶載入子目錄的功能等等,而element則比較符合我們的需求,雖然坑也是有點多...

lazy & load

<el-tree>中加入lazy屬性,可以讓樹變成懶載入的tree,即預設渲染左邊的下拉小箭頭,點選每個小箭頭可以觸發一次load操作,可以實現動態獲取樹下面節點的操作

這裡遇到了第一個問題:怎麼獲取每個節點對應的路徑?

我們需要根據每個節點所在的路徑向後臺傳送請求,節點的路徑就是我們請求的資源路徑,當然拿到這個路徑的方法就是字串拼接,拿到當前節點的node物件,根據它是否存在parent,將它的parent推入我們的currentPath陣列中,每次推進陣列之後需要將當前節點設定為它的parent,當然這個思路還是費了一點時間才想到的-_-!!

  • 獲取每個節點對應路徑的方法
# 獲取當前檔案所在路徑
getCurrentPath (node) {
  if (node && node.data && node.data.name) {
    let nodeParent = node.parent
    this.currentPath = [node.data.name]
    while (nodeParent && nodeParent.data && nodeParent.data.name
           && typeof nodeParent.data === 'object') {
      this.currentPath.unshift(nodeParent.data.name)
      nodeParent = nodeParent.parent
    }
  }
}
複製程式碼

props

我們建立檔案的時候,是不需要lazy load時生成的小箭頭的,因為檔案下面是不能建立檔案的,因此,需要做一下配置,在建立檔案的時候給el-tree傳一下型別,跟它說我要建立的是檔案,不要給我渲染一個小箭頭了,那要怎麼配置呢?其實這個問題element官方文件有具體的例子

第二個問題:怎麼選擇性渲染lazy load生成的小箭頭?

<el-tree
  :props="props1"
  :load="loadNode1"
  lazy>
</el-tree>

<script>
  export default {
    data() {
      return {
        props1: {
          label: 'name',
          children: 'zones',
          isLeaf: 'leaf'
        },
      };
    },
    methods: {
      loadNode1(node, resolve) {
        if (node.level === 0) {
          return resolve([{ name: 'region' }]);
        }
        if (node.level > 1) return resolve([]);

        setTimeout(() => {
          const data = [{
            name: 'leaf',
            leaf: true
          }, {
            name: 'zone'
          }];

          resolve(data);
        }, 500);
      }
    }
  };
</script>
複製程式碼

renderContent

renderContent會監聽data裡面的屬性值來決定是否渲染和渲染對應的檢視,如果有對某個data的屬性值判斷的需要,需要對那個屬性值進行初始化

例如: 根據node節點的data.type決定渲染的內容,一開始需要給data.type賦初始值,如果不賦值則監聽不到變化(我就是因為一開始沒有初始化type,直接設定data.type='edit',然後檢視一直沒更新......)

第三個問題: 為什麼data.type變化了,檢視一直沒更新?

  # 重新命名編輯框
  if (data.type === 'edit') {
    return h('input', {
      attrs: {
        id: 'treeInput',
        value: this.currentNodeData.name
      },
      on: {
        blur: (e) => {
          this.updateCurrentNode(e.target.value || data.name)
        },
        keyup: (e) => {
          if (e.keyCode === 13 || e.keyCode === 27) {
            e.target.blur()
          }
        }
      }
    })
  }
  # 新建編輯框
  if (data.type === 'input') {
    return h('input', {
      attrs: {
        id: 'treeInput'
      },
      on: {
        blur: (e) => {
          this.createNewNode(e.target.value)
        },
        keyup: (e) => {
          if (e.keyCode === 13 || e.keyCode === 27) {
            e.target.blur()
          }
        }
      }
    })
  }
複製程式碼

這裡還有一個小問題:on-bluron-keyup本來我寫的都是this.createNewNode(e.target.value),但是觸發了兩次create操作,原來是keyup的同時輸入框也會失去焦點,所以就觸發了blur,因此就用e.target.blur()代替了原本的寫法,統一用blur來實現觸發create的操作

瀏覽器渲染問題

第四個問題: 為什麼需要setTimeout?

我們對樹的操作的過程經常需要使用到setTimeout(fn, 0),例如:

tryToCreateNode (type) {
  this.$refs.tree.append({
    id: 'treeInput',
    type: 'create'
  }, this.currentNodeData.id)
  this.currentNode.expanded = true
  this.createNewWay = 'append'
  setTimeout(() => {
    this.$el.querySelector('#treeInput').focus()
  }, 0)
}
複製程式碼

這是因為當我們執行了append操作時,觸發了瀏覽器的重排和重繪,需要重新構建dom樹,這個過程是比較耗費時間的,如果我們接下來直接執行this.$el.querySelector('#treeInput').focus(),這時候dom樹是還沒有treeInput這個元素的,setTimeout會將querySelector操作放進任務佇列中去,等到主程式完成dom樹的構建後再執行setTimeout裡面的操作,這時候我們就可以拿到我們的treeInput了

從後端遞迴獲取目錄樹資料

第五個問題:如果不用懶載入,我們怎麼渲染目錄樹?

因為我們這個專案後端儲存檔案的方式就是一個檔案系統,就跟我們在本地看到的一樣,一層一層地存檔案和資料夾,因此我們前端獲取檔案也是得一層一層地發請求,拿到對應層級的檔案,這就得考慮通過遞迴的方式,將每次獲取得到的資料儲存在一個treeData物件中,如第一層的資料就是treeData[0],treeData[1],第二層的資料就是treeData[0].children, treeData[1].children等等

那這個遞迴函式要怎麼實現呢?

  • 獲取某一層資料

  • 將上一層獲取到的資料夾型別的資料再傳入遞迴函式

  • 當獲取那個層級的檔案數為0, 或者都是檔案的時候,結束遞迴

# 遞迴獲取目錄樹資料
async getTreeDataRecursively (path) {
  # getDirByPath是我們自己定義的獲取對應目錄檔案的函式
  let dataList = await this.getDirByPath(path)
  if (dataList && dataList.length >= 0) {
    if (!dataList || dataList.length === 0 || dataList.every(el => el.type === 'file')) {
      return dataList
    } else {
      for (let i = 0; i < dataList.length; i++) {
        let path = dataList[i].id
        if (dataList[i].isDir) {
          this.$set(dataList[i], 'children', await this.getTreeDataRecursively(path))
        }
      }
      return dataList
    }
  }
}
複製程式碼

在vue中,如果直接通過賦值的方式myObj.name = 'aaa'這樣的方式為一個物件的新增某個屬性,不會觸發檢視的更新,可以通過$set來新增,從而觸發更新,詳細見官方文件

在目錄樹中插入子節點

遞迴獲取到的資料要怎麼插入到對應的節點呢?

在上一個問題中,我們解決了每個節點下面子節點的獲取,得到了某個路徑下包含所有子節點的一個物件,這個物件需要插入到對應的目錄結構,例如:

--- a
   --- aa
     --- aaa
     --- aaa1
        --- aaaa
複製程式碼

我們通過getTreeDataRecursively('/a/aa')獲取到了/a/aa下面的目錄結構: aaaaaa1.children(aaaa), 那麼我們現在想要把它插入到對應的路徑/a/aa下面,要怎麼實現呢?

一開始的思路是通過路徑跟每個節點的id比較,因為我們在上面的遞迴函式中,把路徑賦給了每個節點的id,如果id = '/a/aa',那麼我們就將資料dataList插入到下面,整體思路是沒有問題的,但是要改變treeData對應層級的資料這一步卡住了,無法實現...

既然沒法通過改變treeData的資料結構,我就翻看起了element tree的官方文件,終於找到了解決方案...

踩坑日記-element ui樹形控制元件

我們可以官方提供的這個方法,這裡的key就是我們的id(/a/aa), 而value則是dataList

完美~

寫在最後

這就是這一個龐大的元件我們遇到的坑,當然還有很多沒有寫下來,本文只是作為紀錄重要的幾個點,也方便有遇到同樣的問題的同學檢視,能提供一點小小的思路也是很榮幸哈~

相關文章