本文主要是介绍el-tree组件展示节点过多时造成页面卡顿、奔溃的解决办法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
解决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一致,可根据自己的业务需求更改,我做的需求只是进行文件导出。
组件使用示例:
<virtualNodeTreeref="dirTree":data.sync="treeData":load="loadDir":keeps="50":check-strictly="false":props="{isLeaf: 'leaf'}"lazyshow-checkboxnode-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-buttonclass="ame-button"size="mini"type="primary"@click="exportHandle":loading="exportLoading":disabled="exportLoading">导出</el-button></div><virtualNodeTreev-loading="loading"ref="dirTree":data.sync="treeData":load="loadDir":keeps="50":check-strictly="false":props="{isLeaf: 'leaf'}"lazyshow-checkboxnode-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-treeref="dirTree":data.sync="treeData":load="loadDir":check-strictly="false":props="{isLeaf: 'leaf'}"lazyshow-checkboxnode-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.rfidconst res = await this.getFilesNumFromFolder(params)data.allFileNum = resthis.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 = nullif (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, dataPathswitch (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.addInforesolve('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) : 0const 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.fileNumparams.dirInfos[i].curSelFileNum = params.fileNumthis.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 = 0if (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.curSelFileNumreturn total + tmp}, 0)resolve(fileTotal)break// 当父文件夹勾选后,将所有已知文件总数量的子文件夹的curSelFileNum设置为allSelFileNumcase 'setAllChildrenChecked':if (Array.isArray(params.children) && params.children.length !== 0) {for (const i in params.children) {const allFileNum = params.children[i].allFileNumparams.children[i].curSelFileNum = allFileNum === 'unknown' ? 'unknown' : allFileNumif (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]) continueout: for (let j = i + 1; j < paths.length; j++) {if (!paths[j]) continue outfor (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);// returnslotFileExport(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>
这篇关于el-tree组件展示节点过多时造成页面卡顿、奔溃的解决办法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!