函數(shù)式組件劫持替代json封裝element表格
背景
系統(tǒng)有個整改需求,要求系統(tǒng)內(nèi)的所有表格支持本地動態(tài)列顯隱,拖拽排序列位置,固定列功能,涉及的頁面很多
上效果圖:

思路
其實最開始想的肯定是json配置表單的形式,再由循環(huán)出來的列去控制對應(yīng)的位置和屬性 但是!很多頁面?。∶總€頁面都要去轉(zhuǎn)json配置意味著大量的工作量和極高的風(fēng)險
能不能我就寫個自己的組件來包一層,這樣我就能實現(xiàn)最小改動的情況下只需要替換組件標(biāo)簽來實現(xiàn)這個功能

與實際的不同只是我將原來的el-table換成了hf-table,同時支持原本el-table的所有功能
想法與實踐
el-table-column獲取
我們不可能去自己實現(xiàn)一個el-table的組件,所以無非我們的組件就是在el-table的基礎(chǔ)上套一層殼,給他加上一個設(shè)置按鈕,同時設(shè)置的內(nèi)容能夠去影響整個表格的渲染。
那既然我們不自己實現(xiàn)el-table則意味著原先代碼中的el-table-column我們要拿到,并且要傳給el-table,這樣我們才能去渲染出來原先的那個表格
在一個組件的實例中,我們能夠通過vnode去獲取到當(dāng)前的一個虛擬dom,vnode去獲取到當(dāng)前的一個虛擬dom,vnode去獲取到當(dāng)前的一個虛擬dom,vnode有一個componentOptions組件配置項的屬性,通過他的children就能獲取到所有的el-table-column 虛擬dom數(shù)組
如何渲染表格
上一步我們已經(jīng)拿到了所有的el-table-column虛擬dom,那怎么將虛擬dom去渲染成對應(yīng)的表格組件呢?
這不render就該登場了嗎??!

這個children就是我們拿到的el-table-column的數(shù)組,我們只需要將該虛擬dom的數(shù)組以組件屬性的形式傳傳進(jìn)來了,再創(chuàng)建一個el-table,將對應(yīng)的children傳給他!臥槽,這不就又和原本<el-table>xxx</el-table>的效果一毛一樣嗎,是的 ,我做的就是掛羊頭賣狗肉。
也就是說,實際上我的hf-table只是劫持了el-table,他的作用只是拿到原本寫的el-table-colunm的虛擬dom,去渲染成一個表格
操作表格
此時我們的任務(wù)已經(jīng)完成大半了,就是我原本el-table的標(biāo)簽已經(jīng)可以被替換了,那我們要做的就只剩下操作表格了。 實際我做的很簡單,既然我已經(jīng)拿到了所有的子節(jié)點,那我就在hf-table組件中去操作成我想要的數(shù)組,再丟給render函數(shù)去渲染就好了
組件代碼
整個組件的代碼,代碼量除掉樣式也就不到100行
<template>
<div class="hf-table">
<el-popover
placement="bottom-end"
width="400"
popper-class="table-cloumn-setting-popper"
trigger="click"
>
<div class="setting-row-content">
<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)"
>固定在左側(cè)</el-button>
<el-button
class="btn"
size="mini"
:type="clo.fixed === 'right' ? 'primary' : 'default'"
@click="setFixed('right',clo)"
>固定在右側(cè)</el-button>
</div>
</draggable>
</div>
<i slot="reference" class="el-icon-setting" />
</el-popover>
<new-table v-if="showTable" :config="config" />
</div>
</template>
<script>
import draggable from 'vuedraggable'
import newTable from './table.js'
const components = { newTable, draggable }
export default {
components,
props: {
storageName: {
type: String,
default: 'hfTable'
}
},
data() {
return {
showTable: false,
storageList: [],
name: '',
config: {
children: [],
attrs: {},
listeners: {}
}
}
},
watch: {
'$attrs': {
handler(newV) {
this.$set(this.config, 'attrs', newV)
},
deep: true,
immediate: true
}
},
mounted() {
this.initStorage()
this.updateTable()
},
methods: {
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(this.storageName)
// 不管是否初次還是要做一下處理,萬一頁面有修改,做一下更新,以最新的node節(jié)點數(shù)組為準(zhǔn)
let list = storage ? JSON.parse(storage) : []
this.$vnode.componentOptions.children.forEach(node => {
// 以label為準(zhǔn),因為可能會改文本
if (!node.componentOptions.propsData.type && list.findIndex(i => i.label === node.componentOptions.propsData.label) < 0) {
// 不是特殊類型的 找不到就加上
const propsData = JSON.parse(JSON.stringify(node.componentOptions.propsData))
propsData.fixed = propsData.fixed !== undefined ? 'left' : false
list.push({
fixed: false, // 默認(rèn)新增的都是不固定
show: true, // 默認(rèn)新增的都是顯示的
...propsData
})
}
})
// 必須在節(jié)點數(shù)組存在的才有意義
list = list.filter(item => this.$vnode.componentOptions.children.find(n => {
return item.label === n.componentOptions.propsData.label
}))
this.storageList = list
},
// 根據(jù)緩存的數(shù)組進(jìn)行渲染表格
updateTable() {
const childrenNodes = this.$vnode.componentOptions.children.filter(node => node.componentOptions.propsData.type)
this.storageList.forEach(item => {
if (item.show) {
const node = this.$vnode.componentOptions.children.find(n => 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
this.showTable = false
this.$nextTick(() => {
this.showTable = true
})
window.localStorage.setItem(this.storageName, JSON.stringify(this.storageList))
}
}
}
</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: 20px;
top:-20px;
cursor: pointer;
}
}
</style>
表格函數(shù)式組件
import Vue from 'vue'
export default Vue.component('newtable', {
functional: true,
props: {},
listeners: {},
render: function(h, context) {
return h(
'el-table',
{
props: context.data.attrs.config.attrs,
on: context.data.attrs.config.listeners
},
context.data.attrs.config.children
)
}
})
問題點與優(yōu)化
當(dāng)真的推行到項目中時,發(fā)現(xiàn)了以上代碼存在了幾個問題:
1.函數(shù)式組件沒有生命周期和實例,也就是table.js幫我們渲染了el-table,我們卻沒辦法拿到el-table 的實例,也就沒辦法去調(diào)用table原生的方法,例如clearSelection等
2.忘了做插槽傳遞。例如空數(shù)據(jù)自定義插槽等
hf-table.vue
<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">恢復(fù)系統(tǒng)表格設(shè)置</el-button>
<el-button @click="delStorage">恢復(fù)當(dāng)前表格設(shè)置</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)"
>固定在左側(cè)</el-button>
<el-button
class="btn"
size="mini"
:type="clo.fixed === 'right' ? 'primary' : 'default'"
@click="setFixed('right',clo)"
>固定在右側(cè)</el-button>
</div>
</draggable>
</div>
<i slot="reference" class="el-icon-setting" />
</el-popover>
<!-- 按鈕容器 -->
<div
class="table-operate-btn-content"
>
<!-- 插槽自定義表格上方操作欄 -->
<slot name="operateBtnContent">
<!-- 默認(rèn)左右都有操作按鈕,如果單純想左或者想右,請在插入具名插槽 -->
<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 v-if="showTable" :config="config" />
</div>
</div>
</template>
<script>
import draggable from 'vuedraggable'
import newTable from './table.js'
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: {}
}
}
},
watch: {
'$attrs': {
handler(newV) {
this.$set(this.config, 'attrs', newV)
},
deep: true,
immediate: true
}
},
mounted() {
this.initStorage()
this.updateTable()
},
methods: {
getInstance() {
const ref = this.$children.find(i => i.$options._componentTag === 'el-table')
return ref
},
delStorage() {
this.$confirm('恢復(fù)當(dāng)前表格設(shè)置將清除當(dāng)前表格設(shè)置并刷新頁面是否繼續(xù)?', '提示', {
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('恢復(fù)系統(tǒng)表格設(shè)置將清除當(dāng)前表格設(shè)置并刷新頁面是否繼續(xù)?', '提示', {
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節(jié)點數(shù)組為準(zhǔn)
let list = storage[this.storageName] ? storage[this.storageName] : []
this.$vnode.componentOptions.children.forEach(node => {
// 以label為準(zhǔn),因為可能會改文本
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, // 默認(rèn)新增的都是不固定
show: true, // 默認(rèn)新增的都是顯示的
...propsData
})
}
})
// 必須在節(jié)點數(shù)組存在的才有意義
list = list.filter(item => this.$vnode.componentOptions.children.find(n => {
return n.componentOptions && item.label === n.componentOptions.propsData.label
}))
this.storageList = list
},
// 根據(jù)緩存的數(shù)組進(jìn)行渲染表格
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
this.showTable = false
this.$nextTick(() => {
this.showTable = true
})
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: 10px;
top:16px;
cursor: pointer;
}
.table-operate-btn-content{
width: calc(100% - 40px);
.operate-btn-content {
height: 40px;
display: flex;
justify-content: space-between;
align-items: center;
}
}
}
</style>
針對插槽的處理主要是根據(jù)插槽沒有componentOption屬性,然后把它和帶有type的這類vnode直接丟給el-table,而其他的再去做顯隱的處理。
table.js
import Vue from 'vue'
export default Vue.component('newtable', {
functional: true,
props: {},
listeners: {},
render: function(h, context) {
const scopedSlots = {}
Object.keys(context.parent.$scopedSlots).forEach(key => {
if (key !== 'default') {
scopedSlots[key] = context.parent.$scopedSlots[key]
}
})
return context.parent.$createElement(
'el-table',
{
props: { ...context.data.attrs.config.attrs, ref: 'newtable' },
on: context.data.attrs.config.listeners,
attrs: { ref: 'newtable' },
scopedSlots
},
context.data.attrs.config.children
)
}
})
針對函數(shù)式組件沒有實例的問題,這里我直接調(diào)用了父級組件的$createElement方法去創(chuàng)建el-table,再利用父級組件的children中‘children中`children中‘options._componentTag === 'el-table'`的vnode,來拿到對應(yīng)的實例
有點奇怪的是我在創(chuàng)建的時候給生成的組件配置attrs的ref,在父組件中$refs無法拿到
還有一點要注意!我在控制組件重新渲染的時候,使用了$nexttick,所以不要在鉤子函數(shù)中使用getInstance()方法獲取表格組件實例,如果一定要,那就用鏈?zhǔn)脚幸幌驴赵儆?code>this.$refs.hftable.getInstance()?.xxx()
后話
其實這是不是的合適方案也未定,但是最主要是過程中的一個探索吧。包括評論說的$slots去繼承所有的vnode節(jié)點。其實在拿不到表格實例的時候我想換方案的時候嘗試過這個辦法,我直接去掉了函數(shù)式組件。直接寫了一個el-table,然后具名插槽去接收。但是因為要操作vnode,也就是我要隱藏某列,這意味著我需要去修改hf-table中$slots中的default數(shù)組,就總覺得不太合適,共同進(jìn)步吧~更多關(guān)于表格函數(shù)式組件封裝element的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于Vue3+TypeScript的全局對象的注入和使用詳解
這篇文章主要介紹了基于Vue3+TypeScript的全局對象的注入和使用,本篇隨筆主要介紹一下基于Vue3+TypeScript的全局對象的注入和使用,需要的朋友可以參考下2022-09-09
vue+導(dǎo)航錨點聯(lián)動-滾動監(jiān)聽和點擊平滑滾動跳轉(zhuǎn)實例
今天小編就為大家分享一篇vue+導(dǎo)航錨點聯(lián)動-滾動監(jiān)聽和點擊平滑滾動跳轉(zhuǎn)實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-11-11
vue實現(xiàn)動態(tài)表格提交參數(shù)動態(tài)生成控件的操作
這篇文章主要介紹了vue實現(xiàn)動態(tài)表格提交參數(shù)動態(tài)生成控件的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11
vue中el-autocomplete與el-select的異同
本文主要介紹了vue中el-autocomplete與el-select的異同,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05

