踩坑日記-element ui樹形控件

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

lazy & load

<el-tree>中加入lazy屬性,可讓樹變成懶加載的tree,即默認渲染左邊的下拉小箭頭,點擊每一個小箭頭能夠觸發一次load操做,能夠實現動態獲取樹下面節點的操做前端

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

咱們須要根據每一個節點所在的路徑向後臺發送請求,節點的路徑就是咱們請求的資源路徑,固然拿到這個路徑的方法就是字符串拼接,拿到當前節點的node對象,根據它是否存在parent,將它的parent推入咱們的currentPath數組中,每次推動數組以後須要將當前節點設置爲它的parentnode

  • 獲取每一個節點對應路徑的方法
# 獲取當前文件所在路徑
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官方文檔有具體的例子react

第二個問題:怎麼選擇性渲染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變化了,視圖一直沒更新?bash

# 重命名編輯框
  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的官方文檔,終於找到了解決方案...

咱們能夠官方提供的這個方法,這裏的key就是咱們的id(/a/aa), 而value則是dataList

寫在最後

這就是這一個龐大的組件咱們遇到的坑,固然還有不少沒有寫下來,本文只是做爲紀錄重要的幾個點,也方便有遇到一樣的問題的同窗查看,能提供一點小小的思路也是很榮幸哈~

相關文章
相關標籤/搜索