el-tree组件展示节点过多时造成页面卡顿、奔溃的解决办法
解决el-tree组件展示节点过多时造成页面卡顿、奔溃前几天测试提了个BUG,文件列表展示5w个文件页面会卡顿甚至奔溃。项目用的是vue+element-ui框架,我是使用el-tree进行渲染文件列表的。参考网上使用virtual-scroll-list插件与el-tree源码写成一个新组件。virtual-scroll-list可以只渲染页面呈现部分的节点,这样就不会造成卡顿了,源el-tre
·
解决el-tree组件展示节点过多时造成页面卡顿、奔溃
前几天测试提了个BUG,文件列表展示5w个文件页面会卡顿甚至奔溃。
项目用的是vue+element-ui框架,我是使用el-tree进行渲染文件列表的。
参考网上使用virtual-scroll-list插件与el-tree源码写成一个新组件。virtual-scroll-list可以只渲染页面呈现部分的节点,这样就不会造成卡顿了,源el-tree是直接将5w个节点直接渲染到页面,导致页面奔溃。
这是使用virtual-scroll-list插件与el-tree源码结合后的组件:github组件下载 、gitee组件下载
组件使用方法(传入的属性)与el-tree一致,可根据自己的业务需求更改,我做的需求只是进行文件导出。
组件使用示例:
<virtualNodeTree
ref="dirTree"
:data.sync="treeData"
:load="loadDir"
:keeps="50"
:check-strictly="false"
:props="{
isLeaf: 'leaf'
}"
lazy
show-checkbox
node-key="path"
class="treeWrap"
@check-change="handleCheckChange">
<span slot-scope="{ data }">
<svg-icon v-if="data.ftype === '1'" style="color: #fdd300;" icon-class="faFolder"/>
<span v-else>
<svg-icon :icon-class="fileInputHandle(data).icon" :style="{color: fileInputHandle(data).color}"/>
</span>
<span>{{ data.fname }}</span>
</span>
</virtualNodeTree>
组件引入:
效果图:
more文件下面有5w个文件,实际页面渲染50个文件,根据组件传入的keeps展示文件数,默认30个;
注意:
1.搜索只能搜到已渲染的节点,可以自己做递归搜索源数据,不过这样的话数据一多会很卡,建议后端写个搜索api
2.该组件的父容器一定要确定高度,不能以整个body作为父容器,这样有可能滚动时渲染不出下面的文件。
补充:
我使用的完整代码
<template>
<div class="app-container">
<div>
<el-button
class="ame-button"
size="mini"
type="primary"
@click="exportHandle"
:loading="exportLoading"
:disabled="exportLoading"
>导出
</el-button>
</div>
<virtualNodeTree
v-loading="loading"
ref="dirTree"
:data.sync="treeData"
:load="loadDir"
:keeps="50"
:check-strictly="false"
:props="{
isLeaf: 'leaf'
}"
lazy
show-checkbox
node-key="path"
class="treeWrap"
@check="handleNodeCheck"
>
<span slot-scope="{ data }">
<svg-icon v-if="data.ftype === '1'" style="color: #fdd300;" icon-class="faFolder"/>
<span v-else>
<svg-icon :icon-class="fileInputHandle(data).icon" :style="{color: fileInputHandle(data).color}"/>
</span>
<span>{{ data.fname }}</span>
</span>
</virtualNodeTree>
<!--
<el-tree
ref="dirTree"
:data.sync="treeData"
:load="loadDir"
:check-strictly="false"
:props="{
isLeaf: 'leaf'
}"
lazy
show-checkbox
node-key="path"
class="treeWrap"
@check-change="handleCheckChange"
>
<span slot-scope="{ data }">
<svg-icon v-if="data.ftype === '1'" style="color: #fdd300;" icon-class="faFolder"/>
<span v-else>
<svg-icon :icon-class="fileInputHandle(data).icon" :style="{color: fileInputHandle(data).color}"/>
</span>
<span>{{ data.fname }}</span>
</span>
</el-tree>
-->
</div>
</template>
<script>
import { slotFileList, slotFileExport } from '@/api/disc.js'
import { downloadFile } from '@/utils/index'
import getFileIcon from '@/utils/getFileIcon'
import { getFilesNumFromFolder } from '@/api/mtoptical'
import { Message } from 'element-ui'
import virtualNodeTree from '@/components/virtualNodeTree/tree'
export default {
name: 'DiscFileDetail',
components: { virtualNodeTree },
data() {
return {
treeData: [],
dirInfos: [],
lock: false,
loading:false,
exportLoading:false,
exportMaxNum: 10000 // 允许导出文件的最大数量
}
},
mounted() {},
methods: {
async handleNodeCheck(data, selctedInfo) {
const checked = selctedInfo.checkedKeys.includes(data.path)
if (data.ftype == '1' && checked) {
if (typeof data.allFileNum === 'number') {
// 已获悉该文件夹数量的不再查询
this.exportUtils('fileNumChange', { dirInfos: this.dirInfos, data, fileNum: parseInt(data.allFileNum), checked })
return
}
const params = {
nodeId: this.$route.query.nodeId || '',
libId: this.$route.query.libId || '',
grooveId: this.$route.query.grooveId || '',
path: data.path || '/'
}
if (this.$route.query.rfid) params.rfid = this.$route.query.rfid
const res = await this.getFilesNumFromFolder(params)
data.allFileNum = res
this.exportUtils('fileNumChange', { dirInfos: this.dirInfos, data: data || '/', fileNum: parseInt(res), checked })
// if (res > this.exportMaxNum) {
// this.$message.error(`导出文件数量不能超过${this.exportMaxNum}`)
// // 取消勾选
// this.$refs.dirTree.setChecked(data, false, true)
// } else {
// data.allFileNum = res
// this.exportUtils('fileNumChange', { dirInfos: this.dirInfos, data: data || '/', fileNum: parseInt(res), checked })
// console.log('添加数量完成', this.dirInfos[0]);
// }
} else if (data.ftype != '1' && checked) {
// 勾选文件
this.exportUtils('fileNumChange', { dirInfos: this.dirInfos, data, fileNum: null, checked })
} else if (data.ftype != '1') {
// 取消勾选文件
this.exportUtils('fileNumChange', { dirInfos: this.dirInfos, data, fileNum: null, checked })
} else {
// 取消勾选文件夹
this.exportUtils('fileNumChange', { dirInfos: this.dirInfos, data, fileNum: null, checked })
}
},
getFilesNumFromFolder(params) {
return new Promise((resolve, reject) => {
const loaderTip = this.$loading({
lock: true,
text: '请稍等......',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
})
getFilesNumFromFolder(params).then(res => {
// console.log('获取到的文件数量', res);
loaderTip.close()
resolve(typeof res === 'number' ? parseInt(res) : 0)
}).catch(() => {
loaderTip.close()
reject(0)
})
})
},
fileInputHandle(file) {
const nameSplit = file.fname.split('.')
let iconInfo = null
if (nameSplit.length > 1) {
iconInfo = getFileIcon(nameSplit[nameSplit.length - 1])
} else {
iconInfo = getFileIcon('其他')
}
return iconInfo
},
async exportUtils(fn, params) {
return new Promise(async(resolve, reject) => {
let fileTotal, dataPath
switch (fn) {
// 添加文件夹信息到文件夹信息集合内
case 'addDirInfo':
for (let i = 0; i < params.dirInfos.length; i++) {
if (params.node.data.path === params.dirInfos[i].path) {
params.dirInfos[i].children = params.addInfo
resolve('finished')
}
if (params.dirInfos[i].children) {
const status = this.exportUtils('addDirInfo', { ...params, dirInfos: params.dirInfos[i].children })
if (status === 'finished') resolve()
}
}
break
// 添加文件夹数量到文件夹信息集合内
case 'fileNumChange':
// 勾选的是文件比对时需要删除path后面的文件名后再比对
if (params.data.ftype != '1') {
const tmp = params.data.path.split('/')
dataPath = tmp.slice(0, tmp.length - 1).join('/')
} else {
dataPath = params.data.path || '/'
}
for (let i = 0; i < params.dirInfos.length; i++) {
// 找到该文件夹信息
if (dataPath === params.dirInfos[i].path) {
// console.log('已找到该信息', params);
if (params.checked) {
const curDirSelFileNum = params.data.ftype == '1' ? (typeof params.dirInfos[i].curSelFileNum === 'number' ? params.dirInfos[i].curSelFileNum : 0) : 0
const curAllCheckedFileNum = await this.exportUtils('getFileTotal') + (params.fileNum || 1) - curDirSelFileNum
// 添加前检查是否超出最大导出数量
if (curAllCheckedFileNum > this.exportMaxNum) {
this.$message.error(`导出文件数量不能超过${this.exportMaxNum}`)
// 取消勾选
this.$refs.dirTree.setChecked(params.data, false, true)
if (params.data.ftype == '1') {
params.dirInfos[i].curSelFileNum = 0
}
resolve(params.data.ftype == '1' ? -curDirSelFileNum : 0)
return
}
// console.log('当前勾选文件数', curAllCheckedFileNum);
// 勾选的是文件夹
if (params.data.ftype == '1') {
params.dirInfos[i].allFileNum = params.fileNum
params.dirInfos[i].curSelFileNum = params.fileNum
this.exportUtils('setAllChildrenChecked', { children: params.dirInfos[i].children })
resolve(params.fileNum - curDirSelFileNum)
} else {
if (params.dirInfos[i].curSelFileNum === 'unknown') {
params.dirInfos[i].curSelFileNum = 1
} else {
params.dirInfos[i].curSelFileNum += 1
}
resolve(1)
}
} else {
// 取消勾选
if (params.data.ftype == '1') {
params.dirInfos[i].curSelFileNum = 0
if (params.dirInfos[i].allFileNum === 'unknown') {
const queryParams = {
nodeId: this.$route.query.nodeId || '',
libId: this.$route.query.libId || '',
grooveId: this.$route.query.grooveId || '',
path: params.data.path || '/'
}
resolve(await this.getFilesNumFromFolder(queryParams))
}
resolve(params.dirInfos[i].allFileNum)
} else {
if (params.dirInfos[i].curSelFileNum !== 'unknown') {
params.dirInfos[i].curSelFileNum -= 1
}
resolve(1)
}
}
}
if (params.dirInfos[i].children && params.dirInfos[i].children.length !== 0) {
const num = await this.exportUtils('fileNumChange', { ...params, dirInfos: params.dirInfos[i].children })
if (typeof num === 'number') {
if (params.dirInfos[i].curSelFileNum === 'unknown') {
params.dirInfos[i].curSelFileNum = num
} else {
params.dirInfos[i].curSelFileNum += params.checked ? num : -num
}
resolve(num)
}
}
if (i === params.dirInfos.length - 1) resolve('continue')
}
break
// 获取已勾选的文件总数
case 'getFileTotal':
fileTotal = this.dirInfos.reduce((total, item) => {
const tmp = item.curSelFileNum === 'unknown' ? 0 : item.curSelFileNum
return total + tmp
}, 0)
resolve(fileTotal)
break
// 当父文件夹勾选后,将所有已知文件总数量的子文件夹的curSelFileNum设置为allSelFileNum
case 'setAllChildrenChecked':
if (Array.isArray(params.children) && params.children.length !== 0) {
for (const i in params.children) {
const allFileNum = params.children[i].allFileNum
params.children[i].curSelFileNum = allFileNum === 'unknown' ? 'unknown' : allFileNum
if (params.children[i].children && params.children[i].children.length !== 0) {
this.exportUtils('setAllChildrenChecked', params.children[i].children)
}
}
}
break
}
})
},
loadDir(node, resolve) {
const temp = {
nodeId: this.$route.query.nodeId || '',
libId: this.$route.query.libId || '',
oid: this.$route.query.oid || '',
path: node.data.path || '/'
}
if (this.$route.query.src === 'warehouseTask' &&
this.$route.query.rfid
) {
temp.rfid = this.$route.query.rfid
} else {
temp.grooveId = this.$route.query.grooveId || ''
}
this.loading = true;
slotFileList(temp).then(res => {
this.loading = false;
if (res && res instanceof Array) {
const addInfo = []
for (const i in res) {
if (res[i].ftype == '1') {
addInfo.push({
path: (node.data.path || '') + '/' + res[i].fname,
level: node.level + 1,
allFileNum: 'unknown',
curSelFileNum: 'unknown',
children: null
})
}
}
if (this.dirInfos.length === 0) {
this.dirInfos.push({
path: node.data.path || '/',
allFileNum: 'unknown',
curSelFileNum: 'unknown',
level: node.level,
children: addInfo
})
} else {
this.exportUtils('addDirInfo', { dirInfos: this.dirInfos, node, addInfo })
}
const list = res.map(item => {
return {
...item,
path: (node.data.path || '') + '/' + item.fname,
leaf: item.ftype != '1'
// disabled: item.ftype == '1'
}
})
resolve(list)
} else {
resolve([])
}
}).catch(() => {
this.loading = false;
resolve([])
})
},
exportHandle() {
const checkeNodes = this.$refs.dirTree.getCheckedNodes()
if (checkeNodes.length < 1) {
this.$message.error('请选择需要导出的数据')
return
}
this.exportLoading = true;
setTimeout(() => {
try {
// const paths = checkeNodes.map(item => {
// return { fileName: item.path, fileType: item.ftype === '1' ? '1' : '2' }
// })
let paths = checkeNodes.map(item => {
return { pathArr: item.path.split('/'), fileType: item.ftype === '1' ? '1' : '2' }
})
// 如果文件夹与子文件件都勾选了,只保留顶级文件夹
for (let i = 0; i < paths.length; i++) {
if (!paths[i]) continue
out: for (let j = i + 1; j < paths.length; j++) {
if (!paths[j]) continue out
for (let k = 0; k < paths[i].pathArr.length; k++) {
if (paths[i].pathArr[k] !== paths[j].pathArr[k]) {
continue out
}
}
paths[j] = null
}
}
// 过滤掉null的元素
paths = paths.filter(item => item)
// const paths = checkeNodes.map(item => item.path)
const temp = {
nodeId: this.$route.query.nodeId || '',
libId: this.$route.query.libId || '',
grooveId: this.$route.query.grooveId || '',
oid: this.$route.query.oid || '',
exportFiles: paths.map(item => {
return { fileName: item.pathArr.join('/'), fileType: item.fileType === '1' ? '1' : '2' }
})
}
if (this.$route.query.rfid) temp.rfid = this.$route.query.rfid
// console.log('提交参数', temp);
// return
slotFileExport(temp).then(res => {
downloadFile(res, '导出', 'xlsx')
this.exportLoading = false;
}).catch(error => {
const fileReader = new FileReader()
fileReader.onload = function(e) {
Message.error(this.result)
}
fileReader.readAsText(error.response.data)
this.exportLoading = false;
})
} catch (e) {
this.$message.error(e)
this.exportLoading = false;
}
}, 20)
}
}
}
</script>
<style lang="scss" scoped>
.el-tree {
height: calc(100vh - 136px - 90px);
}
.app-container {
background: #fff;
padding: 20px;
margin: 0px 10px 0 10px;
/* height: 800px; */
overflow-y: auto;
border-radius: 5px;
}
.treeWrap {
margin-top: 20px;
border-radius: 4px;
border: 1px solid #9e9e9e;
}
</style>
更多推荐
已为社区贡献1条内容
所有评论(0)