思想:封装表格渲染动态组件🥰(万num)
前言
学会了用旅游的心态看待自己生活的城市! 果然是,心态好,哪里都是巴厘岛啊。(喜欢这段话)
你说你看过日照金山,而我珍惜眼前的棉花糖也很不错啊!
一、想要的效果
请允许我用最简单朴素的话描述。
在我们封装的表格组件中,能根据JSON配置可渲染出switch开关(基于elementui),并且能够交互。
为什么我要用配置传入JSON对象的形式封装表格
ElementUI 列数据必须使用插槽的问题
如果我有很多个字段,那el-table-column就要写很多个。
有点小累。
我想要的组件是:
仅需要做简单的 columns 配置并传入数据就能够出现一个完整的表格,在 template 模板中根本不需要写过多的列数据代码
所以才想要封装一个表格组件,基本的功能都支持了:自定义组件、分页等等。
我觉得这是我们在开发中,遇到不顺心的组件应该要去思考的,沉淀出属于自己的东西。
每个页面都写着差不多的模板,是不是可以不再做重复性工作?
如何去封装组件,怎么样才能够到达可观性、复用性,降低团队的开发成本。
你说是吧,差不多的先生~
好了,接下来正文开始。
二、回忆原本功能
前篇相关可移步:
2.1 原生
我们知道,在之前我们已经实现了可显示元素渲染。
如i标签,icon,这个关闭的×
代入如下:
{
prop: "aaa",
label: "使用时间",
align: "center",
"header-align": "center",
fixed: "right",
width: 150,
event: () => {
return "";
},
// 配置表格内容
tempBody: [
// 勾
{
show: true,
position: "right",
name: "div",
ele: '<i class="el-icon-check" style="color: green; font-size: 22px;" ></i>',
// 显示
interface: val => {
return val.row.aaa;
},
attribute: {}
},
// ×
{
show: true,
position: "right",
name: "div",
ele: '<i class="el-icon-close" style="color: red;font-size: 22px;" ></i>',
// 隐藏
interface: val => {
return !val.row.aaa;
},
attribute: {}
}
]
}
2.2 tooltip
这种效果我们也实现成功
代码如下:
{
prop: "aaa",
label: "使用时间",
align: "center",
"header-align": "center",
fixed: "right",
width: 150,
event: () => {
return "";
},
// 配置表格内容
tempBody: [
{
show: true,
position: "left",
name: "el-tooltip",
ele: '<i class="el-icon-warning" style="cursor: pointer;margin-right: 5px;color: #E6A23C;"></i>',
attribute: {
placement: "top",
effect: "dark",
contentSlot: (scope, coloumnHeader) => {
return `提示: 该于 ${scope.row.aaa} 被体情况请点击【
<span class="detailed-link" style="cursor: pointer;color: #409EFF;">详情</span>】`
},
},
},
]
}
三、分析
3.1 现有功能无法交互
那么,既然能渲染元素,那现在的问题是什么?
现在的问题是:原生元素可渲染,el-switch
第三方组件也能渲染。
但无法进行交互,如开关能触发change事件,做一些业务的事情。
可能还是有点难理解,不着急,我们先实现下代码的配置。
我们可以很轻松的写出这段配置。
页面中el-switch也能显示。
{
prop: "aaa",
label: "启用状态",
align: "center",
"header-align": "center",
fixed: "right",
width: 150,
event: () => {
return ""; // 为什么这里要设置value值为空,因为他是布尔类型,会显示true/false;但此处只需要展示开关
},
// 配置表格内容
tempBody: [
{
show: true,
position: "left",
name: "el-switch",
// 是否显示,条件判断
interface:(val) =>{
return val.row.aaa
},
attribute: {
value: true, // true则开,因为el-switch是用v-model绑定的,底层就是:value和@change
},
},
]
},
看下table中的子组件DynamicComponents源码:
tips:相信大家看到新写的v-on,原本没有的,先忽略。
看完源码后,发现el-switch想要触发开/关change事件,根本没有实现,因为之前需求对表格的定位基本只是展示。
3.2 扩展v-on绑定事件
那么,我们现在需要扩展功能🙃。
相信大家已经看到答案了,用v-on绑定事件。
补充一点知识:
绑定多个事件:
<button
v-on="{
mouseover: onMouseOverHandler,
mouseout: onMouseOutHandler
}"
>
Hover me
</button>
于是配置,传入对象事件:
{
show: true,
position: "left",
name: "el-switch",
interface:(val) =>{
return val.row.aaa
},
attribute: {
value: true,
},
attrEvent:{
change: (e) =>{
this.$emit('attrEvent', e, 'aaa'); // 暴露给父组件
}
}
},
我们重点看这段配置
attrEvent:{
change: (e) =>{
this.$emit('attrEvent', e, 'aaa'); // 暴露给父组件
}
}
这里的this.emit是不可以的,我们这是写在js文件中,无vue实例的emit是不可以的,我们这是写在js文件中,无vue实例的emit是不可以的,我们这是写在js文件中,无vue实例的emit,尽管我可以import导入vue。
但由于内嵌了很多层,(封装得比较细),这里用事件总线bus解决。
import bus from '@/util/bus.js';
attrEvent:{
change: (e) =>{
bus.$emit('attrEvent', e, 'aaa'); // 暴露给父组件
}
}
bus.js
import Vue from 'vue';
export default new Vue();
在使用组件可监听事件
use.vue
bus.$on("attrEvent", (scope, coloumnHeader) => {
}); // 监听事件
当我们页面触发switch开关时,use组件确实可以监听到事件。
假如现在是开的状态下,点击之后,变成关闭状态。
我们需要手动在use组件中去改变他的布尔值。
但我们发现,写不下去了,因为只接收到
我们回顾一下子组件的配置传什么给父组件use
e:true/false(开关状态)
第二个参数:当前的表头字段名
bus.$emit('attrEvent', e, 'aaa'); // 暴露给父组件
只有这两个字段,我们无法判断当前点击的是哪一行数据。
但问题是JSON配置页拿不到是哪一行数据。
3.3 表格DynamicComponents需传参给外面的use使用
继续看,你将能明白讲述的意思。
所以,我们table的DynamicComponents组件要传参给配置,配置才能$emit暴露给父组件。
<template>
<span>
<!-- v-bind绑定属性(传入对象) -->
<component :is="item.name" v-bind="item.attribute" v-on="item.attrEvent">
<template slot="content">
<span v-if="item.attribute.contentSlot" v-html="item.attribute.contentSlot(scope, coloumnHeader)" @click="$emit('componentClick')"></span>
</template>
<span v-html="item.ele"></span>
</component>
</span>
</template>
这里的v-on="item.attrEvent"
,我们做下解构
v-on="{...item.attrEvent}"
好像也不行。没办法传。
tips: 我会把当时思考的想法都一五一十的记录下来,所以会看到很多过程。
v-on要写成方法,我们在方法中来传递。
v-on="attrEvent()"
attrEvent() {
if (!this.item.attrEvent) return {};
let obj = {};
Object.keys(this.item.attrEvent).forEach((property) => {
obj[property] = this.item.attrEvent[property](this.scope, this.coloumnHeader, this.item); // 在表格我们可以拿到哪一行的数据
});
return {};
},
但是,方法一旦加上括号(),就会立即执行,这里又没返回值,就变成
v-on = "{
change: undefined
}"
v-on即会报错。
那我们想,配置就不要去处理change里面到底执行什么,我们放在table组件中写。
即:
配置:写事件名就好
attrEvent: ['change']
DynamicComponents组件
v-on="attrEvent()"
attrEvent() {
if (!this.item.attrEvent) return {};
return this.item.attrEvent.reduce((pre, next) =>{
pre[next] = bus.$emit('attrEvent', this.scope, this.coloumnHeader, this.item); // 暴露给父组件
return pre
}, {})
},
那么,我们在use使用组件
bus.$on("attrEvent", (scope, coloumnHeader) => {
scope.row[coloumnHeader.prop] = !scope.row[coloumnHeader.prop]
}); // 监听事件
拿到哪一行数据,状态给他取反即可。
发现页面的switch状态没变。
不要忘记了,在配置那里需要写两个状态,一个开,一个关的配置。
{
prop: "aaa",
label: "启用状态",
align: "center",
"header-align": "center",
fixed: "right",
width: 150,
event: () => {
return ""; // 为什么这里要设置value值为空,因为他是布尔类型,会显示true/false;但此处只需要展示开关
},
// 配置表格内容
tempBody: [
// 开
{
show: true,
position: "left",
name: "el-switch",
interface:(val) =>{
return val.row.aaa
},
attribute: {
value: true,
},
attrEvent: ['change']
},
// 关
{
show: true,
position: "left",
name: "el-switch",
interface:(val) =>{
return !val.row.aaa
},
attribute: {
value: false,
},
attrEvent: ['change']
},
]
},
分析一下:
页面一开始进入,假设当前状态true,进入配置tempBody数组的interface为true的条件,渲染el-switch组件,开的状态。
绑定了change事件。
当点击switch从开到关,状态变化,触发change,在·DynamicComponents
组件的v-on事件,走进attrEvent
方法,触发bus.$emit
,在use组件监听attrEvent
取反了数据的状态,会进入配置的tempBody数组的interface为false的条件,状态改为false渲染成功。
状态变了,又触发change事件,上面的流程又不断触发。
3.4 防抖方多次触发
经验丰富的我们,很快就想到了防抖。
改造一下DynamicComponents
组件。
v-on="attrEvent()"
attrEvent() {
if (!this.item.attrEvent) return {};
return this.item.attrEvent.reduce((pre, next) =>{
pre[next] = debounce(this.emitAttrEvent, 300);
return pre
}, {})
},
emitAttrEvent() {
bus.$emit("attrEvent", this.scope, this.coloumnHeader, this.item); // 暴露给父组件
},
/**
* 防抖
* @param {Function} func
* @param {Number} delay
* @returns
*/
export const debounce = (func, delay) =>{
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
这样子就可以了。尽管会多次触发change,但我们防抖300毫秒内有触发,即取消,重新等待300毫秒,这样子可避免不断触发。
至此撒花~
后记
这种类似的场景一般用于可视化低代码平台中。
会根据配置来动态渲染。
做好扩展性,遵守开闭原则。
当然,我们还要考虑安全性防XSS注入,防止外部嵌入,导致安全问题。
如果有其他更好的方法也欢迎评论区见,这里提供的只是诸多方法之一。
最后,祝君能拿下满意的offer。
👍 如果对您有帮助,您的点赞是我前进的润滑剂。
以往推荐
原文链接
转载自:https://juejin.cn/post/7386501850042662946