背景
写了篇文章不要再用json去封装element的表格了,用函数式组件去劫持它吧,文章里写了两个版本的代码,只是想记录一下自己的一些思路和封装历程,写完之后被喷复杂,那就给你们个精简版吧,再问问你们复杂在哪里,或者有没有更好的解决方案
回应为啥不用json配置
首先,本次整改的项目设计到需要整改的表格有100+个,全部去转json意味着大量的工作量,已经存在的风险性提高,而本方案只需要修改标签名,其余的不变。
其次为啥要使用json?难道你在template中写的属性在json中少了?难道你用for循环你的数组不用去构建吗?
再者,你的封装别人接手的时候是否要去读你的api才能使用?
还有,嵌套表头可以去试试,看json是否能做
实现效果
调用
和原本的el-table一样,只是换了个标签变成成自己的组件
原理
操作组件里的$vnode,将vnode下的子节点传给表格组件,使用render函数去渲染node数组。全部就以下两个核心方法。剩余的全是操作缓存数组的方法,你也可以不止我实现的这些功能,自己去增加缓存数组的属性就行。
一个获取缓存在localStorage里的缓存数据,并根据最新的node节点做刷新
initStorage() {
this.storageList = []
const storage = window.localStorage.getItem('tableStorage') ? JSON.parse(window.localStorage.getItem('tableStorage')) : {}
// 不管是否初次还是要做一下处理,万一页面有修改,做一下更新,以最新的node节点数组为准
let list = storage[this.storageName] ? storage[this.storageName] : []
this.$vnode.componentOptions.children.forEach(node => {
// 以label为准,因为可能会改文本
if (!(!node.componentOptions || node.componentOptions.propsData.type) && list.findIndex(i => i.label === node.componentOptions.propsData.label) < 0) {
// 非插槽且 不是特殊类型的 找不到就加上
const propsData = JSON.parse(JSON.stringify(node.componentOptions.propsData))
if (propsData.fixed === undefined || propsData.fixed === false) {
propsData.fixed = false
} else {
propsData.fixed = propsData.fixed ? propsData.fixed : 'left'
}
list.push({
fixed: false, // 默认新增的都是不固定
show: true, // 默认新增的都是显示的
...propsData
})
}
})
// 必须在节点数组存在的才有意义
list = list.filter(item => this.$vnode.componentOptions.children.find(n => {
return n.componentOptions && item.label === n.componentOptions.propsData.label
}))
this.storageList = list
},
一个根据当前的配置数据进行表格渲染
// 根据缓存的数组进行渲染表格
updateTable() {
// 特殊类型
const childrenNodes = this.$vnode.componentOptions.children.filter(node => node.componentOptions && node.componentOptions.propsData.type)
this.storageList.forEach(item => {
if (item.show) {
const node = this.$vnode.componentOptions.children.find(n => n.componentOptions && n.componentOptions.propsData.label === item.label)
if (node) {
node.componentOptions.propsData.fixed = item.fixed
childrenNodes.push(node)
}
}
})
this.config.children = childrenNodes
this.config.attrs = this.$attrs
this.config.listeners = this.$listeners
// 通过key值触发表格刷新,避免拖拽由于数组长度一致导致diff算法无法识别
this.config.key = Math.random() + ''
// 控制下表格重新刷新
// this.getInstance()?.doLayout()
const storage = window.localStorage.getItem('tableStorage') ? JSON.parse(window.localStorage.getItem('tableStorage')) : {}
storage[this.storageName] = this.storageList
window.localStorage.setItem('tableStorage', JSON.stringify(storage))
}
如果以上两个方法加上注释不到五十行都让你觉得无比复杂,无法阅读和维护的话,那下文代码就可以不用看了
HfTable组件代码
<template>
<div class="hf-table">
<el-popover
placement="bottom-end"
popper-class="table-cloumn-setting-popper"
trigger="click"
>
<div class="setting-row-content">
<div style="text-align:right">
<el-button @click="delAllStorage">恢复系统表格设置</el-button>
<el-button @click="delStorage">恢复当前表格设置</el-button>
</div>
<draggable v-model="storageList" handle=".el-icon-s-operation" @end="updateTable">
<div v-for="clo in storageList" :key="clo.label" class="setting-row">
<i class="el-icon-s-operation" />
<el-checkbox v-model="clo.show" class="label" @change="showOrHidden($event,clo)">{{ clo.label }}</el-checkbox>
<el-button
class="btn"
size="mini"
:type="clo.fixed === 'left' ? 'primary' : 'default'"
@click="setFixed('left',clo)"
>固定在左侧</el-button>
<el-button
class="btn"
size="mini"
:type="clo.fixed === 'right' ? 'primary' : 'default'"
@click="setFixed('right',clo)"
>固定在右侧</el-button>
</div>
</draggable>
</div>
<i slot="reference" class="el-icon-setting" />
</el-popover>
<!-- 按钮容器 -->
<div
class="table-operate-btn-content"
>
<!-- 插槽自定义表格上方操作栏 -->
<slot name="operateBtnContent">
<!-- 默认左右都有操作按钮,如果单纯想左或者想右,请在插入具名插槽 -->
<div class="operate-btn-content">
<!-- 流式左右布局 -->
<slot name="btnContentLeft">
<div />
</slot>
<slot name="btnContentRight">
<div />
</slot>
</div>
</slot>
</div>
<div :style="{height:`${tableHeight}px`}">
<new-table ref="table" :config="config" />
</div>
</div>
</template>
<script>
import draggable from 'vuedraggable'
import newTable from './myTable.jsx'
import setHeight from '@/mixins/setHeight'
const components = { newTable, draggable }
export default {
name: 'HfTable',
components,
mixins: [setHeight],
props: {
storageName: {
type: String,
required: true
}
},
data() {
return {
showTable: false,
storageList: [],
name: '',
config: {
children: [],
attrs: {},
listeners: {},
key: ''
}
}
},
watch: {
'$attrs': {
handler(newV) {
this.$set(this.config, 'attrs', newV)
},
deep: true,
immediate: true
}
},
mounted() {
this.initStorage()
this.updateTable()
},
methods: {
// 动态列渲染表格时,需要在列渲染完成后重新渲染一下表格
reload() {
this.$nextTick(() => {
this.initStorage()
this.updateTable()
})
},
getInstance() {
// const ref = this.$children.find(i => i.$options._componentTag === 'el-table')
// return ref
return this.$refs.table.$refs?.elTable
},
delStorage() {
this.$confirm('恢复当前表格设置将清除当前表格设置并刷新页面是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const storage = window.localStorage.getItem('tableStorage') ? JSON.parse(window.localStorage.getItem('tableStorage')) : {}
storage[this.storageName] = []
window.localStorage.setItem('tableStorage', JSON.stringify(storage))
location.reload()
})
},
delAllStorage() {
this.$confirm('恢复系统表格设置将清除当前表格设置并刷新页面是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
window.localStorage.removeItem('tableStorage')
location.reload()
})
},
showOrHidden(val, clo) {
if (!val && this.storageList.filter(i => i.show).length === 0) {
this.$message.warning('列表最少显示一列')
this.$nextTick(() => {
clo.show = true
})
return
}
this.updateTable()
},
setFixed(value, clo) {
if (clo.fixed === value) {
clo.fixed = false
} else {
clo.fixed = value
}
this.updateTable()
},
// 初始化缓存配置
initStorage() {
this.storageList = []
const storage = window.localStorage.getItem('tableStorage') ? JSON.parse(window.localStorage.getItem('tableStorage')) : {}
// 不管是否初次还是要做一下处理,万一页面有修改,做一下更新,以最新的node节点数组为准
let list = storage[this.storageName] ? storage[this.storageName] : []
this.$vnode.componentOptions.children.forEach(node => {
// 以label为准,因为可能会改文本
if (!(!node.componentOptions || node.componentOptions.propsData.type) && list.findIndex(i => i.label === node.componentOptions.propsData.label) < 0) {
// 非插槽且 不是特殊类型的 找不到就加上
const propsData = JSON.parse(JSON.stringify(node.componentOptions.propsData))
if (propsData.fixed === undefined || propsData.fixed === false) {
propsData.fixed = false
} else {
propsData.fixed = propsData.fixed ? propsData.fixed : 'left'
}
list.push({
fixed: false, // 默认新增的都是不固定
show: true, // 默认新增的都是显示的
...propsData
})
}
})
// 必须在节点数组存在的才有意义
list = list.filter(item => this.$vnode.componentOptions.children.find(n => {
return n.componentOptions && item.label === n.componentOptions.propsData.label
}))
this.storageList = list
},
// 根据缓存的数组进行渲染表格
updateTable() {
// 特殊类型
const childrenNodes = this.$vnode.componentOptions.children.filter(node => node.componentOptions && node.componentOptions.propsData.type)
this.storageList.forEach(item => {
if (item.show) {
const node = this.$vnode.componentOptions.children.find(n => n.componentOptions && n.componentOptions.propsData.label === item.label)
if (node) {
node.componentOptions.propsData.fixed = item.fixed
childrenNodes.push(node)
}
}
})
this.config.children = childrenNodes
this.config.attrs = this.$attrs
this.config.listeners = this.$listeners
// 通过key值触发表格刷新,避免拖拽由于数组长度一致导致diff算法无法识别
this.config.key = Math.random() + ''
// 控制下表格重新刷新
// this.getInstance()?.doLayout()
const storage = window.localStorage.getItem('tableStorage') ? JSON.parse(window.localStorage.getItem('tableStorage')) : {}
storage[this.storageName] = this.storageList
window.localStorage.setItem('tableStorage', JSON.stringify(storage))
}
}
}
</script>
<style lang="scss" scoped>
.table-cloumn-setting-popper{
.setting-row-content{
max-height: 600px;
overflow-y: auto;
.setting-row{
height: 40px;
line-height: 40px;
.el-icon-s-operation{
cursor: move;
font-size: 16px;
margin-right: 8px;
}
.label{
margin-right: 8px;
}
.btn{
padding: 4px!important;
}
}
}
}
.hf-table{
width:100%;
height:100%;
position: relative;
.el-icon-setting{
position: absolute;
right: 7px;
top: 5px;
cursor: pointer;
}
.table-operate-btn-content{
width: calc(100% - 40px);
min-height: 28px; // 给一个最低的高度,防止没有插槽,右侧icon被吞
.operate-btn-content {
display: flex;
justify-content: space-between;
align-items: center;
min-height: 28px;
padding-bottom: 6px;
}
}
}
</style>
table.jsx代码
export default {
name: 'MyTable',
props: {
config: {
type: Object,
default: () => ({})
}
},
render(h) {
const scopedSlots = {}
Object.keys(this.$parent.$scopedSlots).forEach(key => {
if (key !== 'default') {
scopedSlots[key] = this.$parent.$scopedSlots[key]
}
})
return h('el-table', {
ref: 'elTable',
key: this.config.key || '',
attrs: { ...this.config.attrs },
on: { ...this.config.listeners },
scopedSlots
}, this.config.children)
}
}
结语
我也不是要杠什么,也不是一定说自己的方案最好最牛逼,我也改了很多版,从设计到应用,到问题出现到重构,从一开始用函数式组件,到后面用jsx,都是一个探索的过程,但是烦的是那种看都不看就在说复杂说无法维护的,能不能给个切合实际的设计思路出来?是真特么口嗨不用成本