背景
项目中,产品不想要分页查询,想要初始就查询出所有数据再用表格把所有数据渲染出来,并支持编辑。
因为表格中嵌套大量的输入框、下拉框等编辑组件,当返回数据有两千条时,进入页面加载特别慢,而且编辑时卡顿。原因就是页面的输入框、下拉框等组件数量太多,渲染需要花费很长时间,导致特别慢,性能问题严重
解决方案
为解决这个问题,
1)首先表格使用滚动分页加载,这样可以大大减少渲染编辑框的数量,降低渲染时间;当滚动条触底后加载下一页数据进行追加,让用户以为所有数据是已经加载出来的。
2)使用css模拟一个虚拟的输入框,简单使用css样式模拟编辑框,相比加载一个编辑框更简单轻便。
3)当点击单元格时,使用js将虚拟输入框进行隐藏,向单元格中插入一个真实的输入框;当真实输入框失去焦点时,再销毁真实输入框,将虚拟输入框展示出来。
这样以来,大大降低渲染时间,一个页面只会存在一个编辑框,也不会操作卡顿。
实现
下面是表格组件(后台返回全部数据后,将数据以allData传给此组件,默认显示10条,滚动时,当滚动条触底,加载下一页数据,在之前10条数据的后面进行累积,共20条,直至数据全部加载)
<template>
<!-- 表格--滚动分页加载 -->
<el-table class="my-table" ref="myTable" :data="data" @cell-click="cellClick" :row-key="rowKey">
<template v-for="col in columns">
<el-table-column :key="col.prop" :label="col.label" :formatter="col.formatter">
<template slot-scope="scope">
<!-- 模拟输入框样式 -->
<div v-if="col.type=='input'" class="v-input" :class="{
'v-required': showError(col, scope.row),
'gray-color': !scope.row[col.prop],
'v-disabled':col.disabled,
'is-error': showError(col, scope.row)
}">
{{scope.row[col.prop] || (disabledCell(col, scope.row) ? '' : col.placeholder)}}
</div>
<div v-else-if="col.type=='select'">
......
</div>
<div v-else class="text"
v-html="col.formatter ? col.formatter(scope.row,col) : scope.row[col.prop]">
</div>
</template>
</el-table-column>
</template>
</el-table>
</template>
<script>
import Vue from 'vue';
import VInput from './VInput.vue'
export default {
name: 'V-Table',
props: {
// 表格所有数据
allData: {
type: Array,
default: []
},
// 列配置参数
columns: {
type: Array,
default: []
},
// 每页加载数量
pageSize: {
type: Number,
default: 10
},
rowKey: {
type: String,
default: 'id'
},
},
data() {
return {
currentPage: 0,// 当前页码
total: 0,// 表格所有数据的总数
data: [], // 表格展示数据
}
},
computed: {
// 是否显示 红框提示
showError() {
return (col, row) => {
if (col.type == 'input' && col.required) {
return !row[col.prop]
} else if (col.type == 'select' && col.required) {
// ......
}
return false
}
},
},
watch: {
allData: {
deep: true,
immediate: true,
handler(newValue) {
const currentPage = this.currentPage || 1
const total = currentPage * this.pageSize
this.data = newValue.slice(0, total)
}
},
// 强制刷新变量
reload() {
this.total = this.allData.length
this.currentPage = 0
this.$refs.myTable.bodyWrapper.scrollTop = 0
this.fetchData()
this.loop()
},
},
mounted() {
// 表格添加滚动事件
this.$refs.myTable.bodyWrapper.addEventListener('scroll', this.handleScroll)
},
methods: {
// 滚动加载下一页,将下一页数据和之前数据进行累加
handleScroll(event) {
const { scrollTop, scrollHeight, clientHeight } = event.target
if (Math.ceil(scrollTop) + clientHeight >= scrollHeight) {
// 如果数据已全部加载,则跳出
if (this.data.length == this.total) {
return
}
this.fetchData()
}
},
fetchData() {
this.currentPage += 1
const start = (this.currentPage - 1) * this.pageSize
const end = start + this.pageSize
const newData = this.allData.slice(start, end)
this.data = this.currentPage == 1 ? newData : this.data.concat(newData)
},
// 如果滚动高度小于可视范围高度,则继续加载下一页,直至可视区域填充满
loop() {
this.$nextTick(() => {
const { scrollHeight, clientHeight } = this.$refs.myTable.bodyWrapper
if (scrollHeight && clientHeight && scrollHeight <= clientHeight) {
if (this.data.length == this.total) {
return
}
this.fetchData()
this.loop()
}
})
},
// 鼠标点击单元格
cellClick(row, column, cell, event) {
const col = this.columns.find(v => v.prop == column.prop)
if (!col || col.disabled) {
return
}
if (col.type == 'input') {
this.renderInput(cell, row, col)
} else if (col.type == 'select') {
// ......
}
},
// 隐藏文本div,使用js向单元格里追加el-input组件
renderInput(cell, row, col) {
if (cell.getElementByClassName('el-input')?.length) {
return
}
cell.getElementByClassName('cell')[0].style.display = 'none'
const myInput = Vue.extend(VInput)
const vm = new myInput({
propsData: {
modelValue: row[col.prop],
disabled: col.disabled,
required: col.required
}
}).$mount()
cell.appendChild(vm.$el)
vm.$on('on-input', (val) => {
// 输入事件
})
vm.$on('on-blur', (val) => {
// 失焦事件:销毁el-input,显示文本div
setTimeout(() => {
if (cell.getElementByClassName('el-input')?.length) {
cell.removeChild(cell.getElementByClassName('el-input')[0])
}
cell.getElementByClassName('cell')[0].style.display = 'block'
vm.$destory()
}, 100)
})
// 输入框获取焦点
vm.$refs.myInput.focus()
}
}
}
</script>
代码中的VInput也是个组件,如下:
<template>
<el-input ref="myInput" v-model="modelValue" @input="inputFn" @blur="blurFn"></el-input>
</template>
<script>
export default {
name: 'V-Input',
props: {
modelValue: {
type: String,
default: ''
}
},
data() {
return {
}
},
methods: {
inputFn(val) {
this.$emit('on-input', val)
},
blurFn() {
this.$emit('on-blur', this.modelValue)
}
}
}
</script>
至此,一个表格组件就完成封装了,使用如下
<template>
<myTable :allData="tableData" :columns="columns" :reload="reload" :pageSize="10" />
</template>
<script>
import myTable from './test.vue'
export default {
name: '',
components: { myTable },
data(){
return {
tableData: [], // 接口获取,此处省略
reload: 0,
columns: [
{
prop: 'name',
disabled: false,
label: '名称',
type: 'input',
placeholder: '请输入'
},
{
prop: 'age',
disabled: false,
label: '年龄',
type: 'select',
placeholder: '请选择'
}
]
}
}
}
</script>
同理,下拉框渲染和输入框一样的方法,大家可以自己去扩展一下