最近在做一個管理系統,頁面左側需要一個目錄樹,便於檔案的操作,不想從頭開始造輪子,於是就考慮採用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
完美~
寫在最後
這就是這一個龐大的元件我們遇到的坑,當然還有很多沒有寫下來,本文只是作為紀錄重要的幾個點,也方便有遇到同樣的問題的同學檢視,能提供一點小小的思路也是很榮幸哈~