背景
- 项目中涉及层级嵌套较深的父子表单组件,且表单的入参、出参复杂,且部分入参还涉及后端返回的动态的正则校验。
- 原项目的vue文件中,业务逻辑、页面交互逻辑、入参出参等状态管理和数据格式的转换,混杂不清。
- 导致各方面问题:
- 数据转换与状态管理:由于各个业务逻辑、第三方库等对数据的格式要求不一样,同一个业务内,从后端拿到的原始数据被重复遍历、映射,极其影响性能。对于整个业务的状态管理不清晰,同一个数据在
vuex里被多次声明。 - 业务逻辑与
vue和element-ui等其他第三方库的界面交互高度耦合。
- 数据转换与状态管理:由于各个业务逻辑、第三方库等对数据的格式要求不一样,同一个业务内,从后端拿到的原始数据被重复遍历、映射,极其影响性能。对于整个业务的状态管理不清晰,同一个数据在
实现过程与思考
业务逻辑与UI的解耦
- 在「vue」下,“基础数据、数据转换(映射)、业务逻辑、UI” 之间的关系
- 对于UI界面,无论是在样式层面还是交互层面,其本身变动性大;
- 对于同样的业务逻辑,落实到具体UI框架上,往往有多种实现。例如在VUE中,层级嵌套的表单组件之间的数据传递有多种实现方式,但业务逻辑层面并不关心这些实现细节。如果业务逻辑分散在VUE框架本身的多个组件、多个方法中,会造成代码的可阅读性变差,而且也更容易产生bug。
- 解耦之后的业务逻辑易于覆盖单元测试。虽然有适配VUE的vue-test-utils,可以很方便地操纵地获取到
.vue文件中的data、props、子组件,甚至vuex等。但是,要在一个业务逻辑与VUE代码耦合的组件甚至多组件中去覆盖单元测试,意味着用于测试的代码也与前者高度耦合。 - hooks式的业务逻辑抽离
// usecase/setform.js
/**
* 某项业务逻辑
* @param inputData 传入的业务数据依赖
* @param props 业务的配置项等
* @returns 返回给Vue组件的可以直接使用的数据
*/
export const useSetSomeForm = (inputData, props) => {
let finalOutput;
// 具体业务逻辑...
// if () TODO1
// if () TODO2
return { finalOutput1, finalOutput2 };
}
// component/SomeForm.vue
import { useSetSomeForm } from "..."
const inputData = this.$store.getters.mapInfo;
const props = {};
const { finalOutput1, finalOutput2 } = useSetSomeForm(inputData, props);
this.data1 = finalOutput1;
this.data2 = finalOutput2;
- 以上的方法完全可以直接在组件中一套连招实现 —— 请求后端数据 => 处理后端数据 => 赋值给vue组件的data下的某个属性。那为什么还要在store中进行这么一套操作呢?
- 笔者认为当我们在考虑如何解耦或者优化代码的时候,一定要结合实际的场景考虑,不能强塞硬套地搬用某种模式,否则只会徒增代码的可阅读性。
“传统”的处理方式
- 场景一:请求后端接口的数据不需要进行处理,或者说只需要处理一次,且这个数据也不会被多次复用: 那么这种情况下 “直接在组件中写一整套的获取数据、处理数据、挂载数据到vue实例上的操作” 真无可厚非。例如某个页面是纯粹的表格列表展示,甚至没有需要响应式修改数据的需求。
method:{ initForm() { const { data } = requestFormdata(); this.formdata = data.map(callback); } } - 场景二:请求后端接口的数据需要被多组件复用,且各组件内复用时,都需要把数据进行各不相同映射转换处理,再挂载到vue实例上:
// 举例后端返回类似如下的数据 // dataInfo:表示保存着用户上次填好的数据,用于表单的数据回填; // paramInfo 表格内的选项参数。选项参数非固定,例如selectBox的可选项不固定,依靠后端动态的返回; "formData":{ "baseData":{"dataInfo":{}, "paramsInfo":[]}, // 主表单用到的数据, "subData1":{"dataInfo":{}, "paramsInfo":[]}, // 嵌套的小表单组件部分的数据 "subData2":{"dataInfo":{}, "paramsInfo":[]}, // 嵌套的小表单组件部分的数据 ... }
假如我们还是在MainForm组件中,通过props传递数据给子组件SubForm1,SubForm2(父子通信,采用props传值,而props是异步获取的,需要在子组件中用watch监听props的变化,动态地赋值给子组件);// MainForm.vue async created() { // 发送请求,获取数据 const { data } = requestFormdata(); this.initMainform(); }, methods: { initMainform() { this.formdata = data.baseData.dataInfo; this.selectionParams = data.baseData.paramInfo.map(callback); }, }
从上面可以看到,主组件中获取到后端数据// SubForm1.vue(SubForm2.vue类似) data(){ return{ subData: {}, subSelections: [] } }, props: [‘subform1Data’], watched: { subform1Data(val) { this.subData = val.dataInfo; this.subSelections = val.paramInfo.map(callback) }, },formData之后,再将其中的formData.subData1、formData.subData2通过props传值给子组件。数据处理方法分散在各个组件中。当我们想去查看整个表单的数据处理方法,就必须去到每个组件内部观察;其次,如果子组件内还需要父组件中的其他异步获取的数据,就得继续重复props传值 => watch监视的操作 => watch监测到变化之后 => 触发内部回调,对数据进行操作后更新数据。
在VUEX中集成 entity & adapter
还是以上面场景二为例子,首先给出基本的实现介绍:
配置VUEX
// ./store/index.js
export default {
// Entity 用于存放各种基本的 "原始" 数据
state: {
handledInfo: {},
},
// 由相应的actions触发,对数据进行适当处理后,赋值给state
mutations: {
setInfo(state, rawData) {
state.handledInfo = dataHandleFn(rawData);
},
},
// 由页面中调用触发,实现ajax请求等异步操作
actions: {
async getInfo({ commit }, params) {
const { data } = await someRequest(params);
commit('setInfo', data);
// 是否return根据自己需要选择
return data;
},
},
// adapter,负责将state中的数据映射为 外界组件 中能直接使用的结构、形式
getters: {
mapMainform1Info(state) {
return state.handledInfo.map(callback1);
},
mapSubform1Info(state) {
return state.handledInfo.map(callback2);
},
mapSubform2Info(state) {
return state.handledInfo.map(callback3);
},
},
}
}
调用VUEX
// MainForm.vue
computed: {
...mapGetters(['mapMainform1Info']),
}
methods: {
...mapActions('getInfo'),
initMainform() {
this.formdata = this.mapMainform1Info;
},
async mounted() {
// 组件内的数据获取依靠vuex,所以需要保证mounted函数内部先执行完 vuex内的一套调用操作(actions => muations => state更新 => getters更新)后,再执行获取state或getters的操作
await getInfo();
// MainForm组件自己更新数据
this.initMainform();
// SubForm1组件更新数据, 在父组件中用`$refs`去调用子组件中的方法
this.$refs.subForm1.initSubForm1();
// SubForm2组件更新数据, 在父组件中用`$refs`去调用子组件中的方法
this.$refs.subForm2.initSubForm2();
}
MainForm.vue中,从mounted中可以清晰的看到表单初始化需要干的事情(请求数据,主表单回填,子组件表单回填...)。而具体的数据处理逻辑被统一交给VUEX中的mutations和getters完成。
子组件中也通过vuex获取数据
// SubForm1.vue
computed: {
...mapGetters(['mapSubform1Info']),
},
methods: {
initSubForm1() {
this.subForm1Data = this.mapSubform1Info;
}
}
总结
- 在VUEX中集成 entity & adapter,可以对多组件都需要访问的数据进行集中管理,并且通过getters将数据映射为组件需要的结构,然后暴露给所需要的组件。
- 组件内部的逻辑更加清晰,让组件内部的方法更多地关注UI交互本身。
- 组件内涉及到数据的初始化、增删改查等操作时,组件内只负责调用外部引入的VUEX、抽象后的业务函数。