记录一次功能模块的重构过程
模块介绍
该模块共计7张表,105个字段(排除重复项实际41个字段),字段之间可存在联动逻辑,且不同表的同一字段可能存在不同的联动逻辑。
表中的字段根据所在表不同,可拥有不同的的字段值column
、操作符号 conditionType
以及不同格式的值 condition
,当点击查询的时候,需要将这些筛选条件的值都收集起来,传递给后端用于SQL字段拼接。
重构原因及期望
-
代码冗余度过高,7张表的代码都在同一个文件中,总代码行数5k+;
-
代码基本无注释,变量均为拼音缩写,难以理解代码逻辑;
-
代码中很多地方使用了JQuery,不符合vue的使用思想;
目前需求需要在前面7张表的基础上加入第8张表,但是基于上面的因素,很难做到在不影响原有逻辑的情况下实现需求,甚至第8张表怎么融入原有7张表之间都是一个问题,原先表与表之间的耦合度太高了,而且有第8张表,未来就会有第9第10张表的出现,终究会出现崩溃的一天。
因此选择重构此模块,重构后期望:
- 每张表独立为一个模块,拥有自己的逻辑;
- 每张表中的筛选条件通过组合而成,相同字段可复用;
- 每个筛选条件拥有自己的逻辑;
重构思路
根据对该模块原有功能的分析,可以将该模块的内容拆分为3种子模块,分别是查询表模块(Tables)、筛选条件模块(Conditions)、基础表单模块(Components)
3种子模块的关系如下图:
目录结构
将该模块的全部子模块都放在同一个目录中,这样其他项目需要使用该模块的时候,直接把这个目录复制过去就可以了。
index.vue
目录下的 index.vue
文件中引入所需要的表模块,各表模块之间的切换逻辑,点击查询时执行的筛选条件收集逻辑以及重置筛选条件和请求查询/导出接口等逻辑都在该文件中实现。
表模块之间的切换
表模块之间的切换采用了element的tabs组件,一开始是把各表模块通过 component
组件动态加载到 el-tab-pane
中的,但是发现,假设筛选条件A的数据从接口B中获取,且7张表都存在筛选条件A,那么这种写法就会导致,每次切换表,接口B都会被请求7次。
后来发现 el-tab-pane
组件存在一个 lazy
属性可以在用到的时候才进行渲染,但是又出现了一个问题,当我点击第一张表的时候,请求了1次接口B,当我切换到第二长表的时候,请求了2次接口B,7张表都切换过一次后,再任意切换一张表都会请求7次接口B。哪怕不使用 component
采用 v-if
来控制表模块的显示也会出现这个问题。
最后,将表模块都放在了 el-tab
组件外面,通过 v-if
来控制显示,可以做到每次只请求一次接口B。
重置筛选条件
当点击筛选按钮的时候,会将页面上的筛选条件都重置为默认值,并且如果已经有查询结果了也会将查询结果清除掉。
从上面的代码中可以看到我给表模块的父容器加了一个 key
属性,当点击重置按钮的时候,会为这个属性绑定的值重新赋值,进而就会重新渲染当前的表,相当于用了一种比较暴力的方法将筛选条件的值都重置为默认值了。
上面这种重置的方法是不好的(进度赶,无奈采用这种方法实现),因为存在一些筛选条件的选项数据是通过接口请求得来的,这样的做法无疑会多次发送请求,浪费了资源。
除此之外,可能会存在切换表单时保留筛选条件的值的需求,因此直接绑定个 key
让表单强制刷新的这种做法只是权宜之计。
其实有为每个筛选条件暴露一个 setCondition
方法,就是为了预防回显和重置这种情况,后面有时间了才去具体实现逻辑。
/**
* 点击重置按钮执行的操作
* 1. 下方结果表格隐藏
* 2. 上方筛选条件的值全部重置
* 3. 参数数据清空
*/
reset() {
this.showTable = false; // 隐藏结果表格
this.tableContainer = Date.now(); // 不该这样暴力的
this.params = []; // 清除已经收集的筛选条件
},
筛选条件收集
点击查询按钮的时候,会调用当前查询表的 getConditions
方法,然后就会返回收集到的所有筛选条件的值,然后可以在这里对收集到的值进行再次加工,比如说有一些参数是写死且每个表都需要的又或者说有些参数页面上没有显示,但是实际上接口就是需要的,就可以在这里进行处理。
getConditionParams() {
const table = this.$refs[this.activeTab]; // 获取当前表的实例
this.currentTableInstance = table; // 这里在请求查询接口的时候有用
if (table.getConditions) {
const params = table.getConditions();
// 每个表中默认添加年的参数
params.unshift({
column: "t1.year",
conditionType: "=",
condition: localStorage.getItem("chooseTheYear"),
});
// console.table(params); // 调试时打开
this.params = params;
}
}
逻辑代码结构
综上所述, index.vue
文件逻辑相关代码结构应该如下:
methods: {
/**
* 初始化标签栏
* 对标签栏进行配置
*/
initTabPanes() {},
/**
* Tab被点击时切换表
* @param {VueComponent} tab elTabPane组件实例
* @param {PointerEvent} event 事件对象
*/
handleTabClick(tab, event) {},
/**
* 点击查询按钮执行的事件
* 1. 获取参数
* 2. 请求接口
*/
search() {},
/**
* 点击重置按钮执行的操作
* 1. 下方结果表格隐藏
* 2. 上方筛选条件的值全部重置
* 3. 参数数据清空
*/
reset() {},
/**
* 获取当前表格的筛选条件的参数
*/
getConditionParams() {},
/**
* 点击查询按钮后,请求接口,获取表格数据
* 注意,不用表要请求不同的接口
*/
getTable(pageNum) {},
/**
* 表格导出
* 不同表对应不同的导出接口
*/
exportTable() {},
/**
* 查询结果的页码改变时触发
* @param {number} pageNum 页码
*/
pageChange(pageNum) {},
},
查询表模块
指的是 Tables
目录,该目录下的每一个单文件组件代表一张表,每张表都应该具有下面3个方法(接口)
- conditionChange 表中的筛选条件的值改变时就会通过该方法得知
- getConditions 获取当前表中全部筛选条件的值
- resetConditions 重置当前表中的全部筛选条件为默认值
conditionChange
该方法接收一个 condition
对象做参数,参数格式如下
{
label:'筛选条件的名称',
value:any // 筛选条件的值
}
在这个方法里面可以通过判断 label
的值来进行一些联动的操作,比如说A条件的值改变时,会重新发起请求获取B/C/D条件的选项数据,就可以如下书写代码
conditionChange(condition) {
const { label, value: data } = condition;
if (["A"].includes(label)) {
// 做一些联动操作
return;
}
}
getConditions
该方法会通过 $children
属性获取到该表中的所有筛选条件,然后通过遍历每个筛选条件同时调用筛选条件提供的 getCondition
接口获取到筛选条件的值,将这些值汇总到一个数组中(汇集应该按照自己的需求来定),因为需求中是:当某个筛选条件的值为“不限”的时候,无需收集该条件的值,这个时候该方法会通过 Array.isArray
来判断返回值是对象还是数组,如果是数组则通过扩展运算符 ...
push进数组中,如果是对象则直接push进数组,这样的话,如果不需要将该条件push进数组,只需要筛选条件的 getCondition
返回一个空数组即可。
/**
* 获取当前表中筛选条件的值
*/
getConditions() {
const conditionsList = this.$children;
const params = []; // 参数集合
// 遍历表中的筛选条件
conditionsList.forEach((c /*condition*/) => {
if (c.getCondition) {
const condition = c.getCondition(0);
if (Array.isArray(condition)) {
params.push(...condition);
} else {
params.push(condition);
}
}
});
// 参数过滤-这里是因为生产条线联动产生的问题
// this.paramsFilter(params);
return params;
}
这里拿到的是最原始的参数集合,可以根据需要自己进行一些操作,比如我这里因为有两个条件之间的联动做的不够完美,就多加了一个参数过滤方法 paramsFilter
来专门把这两个条件给过滤掉。
resetConditions
重置表中的筛选条件,待实现
筛选条件模块
指的是 Condotions
目录,该目录下的每一个单文件组件都代表一个筛选条件,每个筛选条件需要有以下属性
-
label 条件名称,这样就可以更容易看出某个查询表中存在哪些筛选条件了。
-
d 数据,这个属性是可选的,当某个筛选条件的选项数据需要外部传进来的时候,可以通过这个属性接收,但是出现这个属性的原因是因为,在我这个项目中,后端通过一个接口将好几个筛选条件的选项数据传递给我了,为了减少请求次数,就加入了这个属性。可以通过watch来监控该属性,然后转换成需要的数据格式(意思就是,父组件只负责提供数据,数据格式的问题应该交由组件本身处理)
watch: { /** * 监控属性d,当d发生改变时,说明接口请求到了数据 * 在这里会对数据结构进行修改 * @param {array} data 接口返回的数据 */ d(data) { const key = Date.now(); const list = data.map((item, index) => { item.$key = `${key}${index}`; item.label = item.name; return item; }); this.$refs.collapse.setState(list); }, }
conditionChange
当筛选条件的值改变的时候,会触发这个方法,同时,这个方法也应该触发父组件(Table)的 change
方法。
该方法接收一个 data
参数,data是一个对象,格式如下
{
label: string, // 筛选条件的名称
value: any, // 筛选条件的值-可以是字符串/数组/对象各种各样
}
筛选条件的值也是在这个方法中改变的
conditionChange(data) {
this.value = data.value;
this.$emit("change", data);
},
每个筛选条件都会有一个 value
状态,用于存储当前筛选条件的值。
getCondition
该方法接收一个number类型的参数 tabIndex
,用于区分是哪个表调用的,这个参数可以自定义,这里用数字只是因为方便。
当父组件调用该方法的时候,该方法内部会进行参数拼装,如下
getCondition(tabIndex) {
if (!this.value) return [];
let param = {};
param.conditionType = "=";
param.condition = `'${this.value}'`;
if ("0".includes(tabIndex)) {
param.column = "t1.id";
}
if ("12".includes(tabIndex)) {
param.column = "t1.workflow_id";
}
return param;
},
如果不需要拼接该参数,就直接返回一个空数组回去即可,原因上面有说过。
上面的代码的意思就是,当表的index为0的时候,返回下面的参数
{
column:'t1.id',
conditionType:'=',
condition:`'${this.value}'`
}
如果表的index为1或则2的时候,则返回下面的参数
{
column:'t1.workflow_id',
conditionType:'=',
condition:`'${this.value}'`
}
所以,当参数出现问题的时候,我们就可以直接来到这个方法中debugger,更快的定位到问题。
setCondition
设置当前参数的值,保留位置,暂未实现
setCondition(value) {
this.value = value;
}
基础表单模块
指的是 components
目录,该目录下的每一个单文件组件代表一种基础表单,这是筛选条件的基础,一般而言,一个筛选条件应该只会有一种基础表单类型,比如说 审批人 这个筛选条件,它的基础表单类型就是一个输入框。
经过统计,这次需求中,一共有7种表单类型,分别是
-
文本输入框
-
单选按钮组
-
复选按钮组
-
日期选择(相互独立)
需求变了,开始日期和结束日期直接相互独立,不存在必填(但是逻辑不能乱,比如说结束日期不能在开始日期之前)
-
范围输入框
其实就是两个输入框,但是样式不一样,还要有单位属性
-
折叠复选框
这个真是一言难尽
-
级联选择器
每个基础表单类型都应该存在 label
属性,用于显示筛选条件的名称,然后也可以加上更多自定义的属性,比如说我为了防止标签换行,加了一个 labelWidth
用于控制筛选条件的 width
样式,然后因为有时候希望不限制该筛选条件,所以我还加了一个 unlimited
属性,用于控制是否显示 不限 这个按钮。
因为存在不限选项的筛选条件比较多,所以该属性的默认值为true。
每个基础表单组件应该存在 setState
方法,用于设置该表单组件的值(基本上只有复选框,单选框,级联这些需要从后台获取选项数据才会用到这个),还要存在一个 valueChange
方法,这些表单组件的基础是 element-ui
的表单组件,因此都存在一个 change
事件,当表单组件的 change
事件被触发时,就会执行 valueChange
,然后在这个方法中执行一些逻辑,但是,该方法的最后一定要触发父组件(condition)的 change
事件,将 label
和 value
传递出去。
setState
setState(value) {
this.value = value;
this.valueChange(this.value); // 触发一次
},
这里要注意的是,因为 setState
方法更多是用来设置选项的值的,所以基本上只会在拿到选项的值之后调用一次之后就不会再调用了,如果某个基础表单存在默认值的话,要在这个方法里面调用一次 valueChange
来告诉父组件,默认值改变了。
valueChange
值改变之后,可以在这里对值进行格式化,然后传递出去,如果说值的格式比较多样的话,可以把格式化这一步放到筛选条件组件中做。
valueChange(value) {
this.$emit("change", {
label: this.label,
value,
});
}
重构实现
结合上面的思路,最后完成7张表的重构外加1张新表的搭建。
部分页面
参数获取
结果显示
上面可以看到,参数都已经可以正常的拿到了,剩下的事情就是请求接口,然后把获取到数据显示出来就可以了,可以多写一个表格组件,因为不同的查询表要显示的字段也不一样,可以在这个表格组件中进行配置。
转载自:https://juejin.cn/post/7207692689591992376