函數(shù)式組件劫持替代json封裝element表格
背景
系統(tǒng)有個整改需求,要求系統(tǒng)內(nèi)的所有表格支持本地動態(tài)列顯隱,拖拽排序列位置,固定列功能,涉及的頁面很多
上效果圖:
思路
其實(shí)最開始想的肯定是json配置表單的形式,再由循環(huán)出來的列去控制對應(yīng)的位置和屬性 但是!很多頁面?。∶總€頁面都要去轉(zhuǎn)json配置意味著大量的工作量和極高的風(fēng)險
能不能我就寫個自己的組件來包一層,這樣我就能實(shí)現(xiàn)最小改動的情況下只需要替換組件標(biāo)簽來實(shí)現(xiàn)這個功能
與實(shí)際的不同只是我將原來的el-table換成了hf-table,同時支持原本el-table的所有功能
想法與實(shí)踐
el-table-column獲取
我們不可能去自己實(shí)現(xiàn)一個el-table的組件,所以無非我們的組件就是在el-table的基礎(chǔ)上套一層殼,給他加上一個設(shè)置按鈕,同時設(shè)置的內(nèi)容能夠去影響整個表格的渲染。
那既然我們不自己實(shí)現(xiàn)el-table則意味著原先代碼中的el-table-column我們要拿到,并且要傳給el-table,這樣我們才能去渲染出來原先的那個表格
在一個組件的實(shí)例中,我們能夠通過vnode去獲取到當(dāng)前的一個虛擬dom,vnode去獲取到當(dāng)前的一個虛擬dom,vnode去獲取到當(dāng)前的一個虛擬dom,vnode有一個componentOptions組件配置項(xiàng)的屬性,通過他的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>
的效果一毛一樣嗎,是的 ,我做的就是掛羊頭賣狗肉。
也就是說,實(shí)際上我的hf-table只是劫持了el-table,他的作用只是拿到原本寫的el-table-colunm的虛擬dom,去渲染成一個表格
操作表格
此時我們的任務(wù)已經(jīng)完成大半了,就是我原本el-table的標(biāo)簽已經(jīng)可以被替換了,那我們要做的就只剩下操作表格了。 實(shí)際我做的很簡單,既然我已經(jīng)拿到了所有的子節(jié)點(diǎn),那我就在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é)點(diǎn)數(shù)組為準(zhǔn) let list = storage ? JSON.parse(storage) : [] this.$vnode.componentOptions.children.forEach(node => { // 以label為準(zhǔn),因?yà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é)點(diǎn)數(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 ) } })
問題點(diǎn)與優(yōu)化
當(dāng)真的推行到項(xiàng)目中時,發(fā)現(xiàn)了以上代碼存在了幾個問題:
1.函數(shù)式組件沒有生命周期和實(shí)例,也就是table.js幫我們渲染了el-table,我們卻沒辦法拿到el-table 的實(shí)例,也就沒辦法去調(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é)點(diǎn)數(shù)組為準(zhǔn) let list = storage[this.storageName] ? storage[this.storageName] : [] this.$vnode.componentOptions.children.forEach(node => { // 以label為準(zhǔn),因?yà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é)點(diǎn)數(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ù)式組件沒有實(shí)例的問題,這里我直接調(diào)用了父級組件的$createElement
方法去創(chuàng)建el-table,再利用父級組件的children中‘children中`children中‘options._componentTag === 'el-table'`的vnode,來拿到對應(yīng)的實(shí)例
有點(diǎn)奇怪的是我在創(chuàng)建的時候給生成的組件配置attrs的ref,在父組件中$refs無法拿到
還有一點(diǎn)要注意!我在控制組件重新渲染的時候,使用了$nexttick
,所以不要在鉤子函數(shù)中使用getInstance()
方法獲取表格組件實(shí)例,如果一定要,那就用鏈?zhǔn)脚幸幌驴赵儆?code>this.$refs.hftable.getInstance()?.xxx()
后話
其實(shí)這是不是的合適方案也未定,但是最主要是過程中的一個探索吧。包括評論說的$slots
去繼承所有的vnode節(jié)點(diǎn)。其實(shí)在拿不到表格實(shí)例的時候我想換方案的時候嘗試過這個辦法,我直接去掉了函數(shù)式組件。直接寫了一個el-table,然后具名插槽去接收。但是因?yàn)橐僮鱲node,也就是我要隱藏某列,這意味著我需要去修改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-09vue+導(dǎo)航錨點(diǎn)聯(lián)動-滾動監(jiān)聽和點(diǎn)擊平滑滾動跳轉(zhuǎn)實(shí)例
今天小編就為大家分享一篇vue+導(dǎo)航錨點(diǎn)聯(lián)動-滾動監(jiān)聽和點(diǎn)擊平滑滾動跳轉(zhuǎn)實(shí)例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-11-11vue實(shí)現(xiàn)動態(tài)表格提交參數(shù)動態(tài)生成控件的操作
這篇文章主要介紹了vue實(shí)現(xiàn)動態(tài)表格提交參數(shù)動態(tài)生成控件的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11一篇帶你搞懂Vue項(xiàng)目里的權(quán)限控制
這篇文章主要為大家介紹了vue項(xiàng)目里的權(quán)限控制,文中有詳細(xì)的代碼示例供大家參考,有需要的朋友可以借鑒參考下,希望能夠有所幫助2023-06-06vue項(xiàng)目首次打開時加載速度很慢的優(yōu)化過程
這篇文章主要介紹了vue項(xiàng)目首次打開時加載速度很慢的優(yōu)化過程,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08vue中el-autocomplete與el-select的異同
本文主要介紹了vue中el-autocomplete與el-select的異同,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05