踩坑日記-element ui樹形控制元件
最近在做一個管理系統,頁面左側需要一個目錄樹,便於檔案的操作,不想從頭開始造輪子,於是就考慮採用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-blur
和 on-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
下面的目錄結構: aaa
和 aaa1.children(aaaa)
, 那麼我們現在想要把它插入到對應的路徑 /a/aa
下面,要怎麼實現呢?
一開始的思路是通過路徑跟每個節點的id比較,因為我們在上面的遞迴函式中,把路徑賦給了每個節點的id,如果id = '/a/aa',那麼我們就將資料dataList插入到下面,整體思路是沒有問題的,但是要改變treeData對應層級的資料這一步卡住了,無法實現...
既然沒法通過改變treeData的資料結構,我就翻看起了element tree的官方文件,終於找到了解決方案...
我們可以官方提供的這個方法,這裡的 key
就是我們的 id
( /a/aa
), 而 value
則是dataList
完美~
寫在最後
這就是這一個龐大的元件我們遇到的坑,當然還有很多沒有寫下來,本文只是作為紀錄重要的幾個點,也方便有遇到同樣的問題的同學檢視,能提供一點小小的思路也是很榮幸哈~