「Vue」基于clean架构的思路,在嵌套表单业务上的重构尝试

414 阅读3分钟

背景

  • 项目中涉及层级嵌套较深的父子表单组件,且表单的入参、出参复杂,且部分入参还涉及后端返回的动态的正则校验。
  • 原项目的vue文件中,业务逻辑、页面交互逻辑、入参出参等状态管理和数据格式的转换,混杂不清。
  • 导致各方面问题:
    • 数据转换与状态管理:由于各个业务逻辑、第三方库等对数据的格式要求不一样,同一个业务内,从后端拿到的原始数据被重复遍历、映射,极其影响性能。对于整个业务的状态管理不清晰,同一个数据在vuex里被多次声明。
    • 业务逻辑与vueelement-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.vue
    async created() {
        // 发送请求,获取数据
        const { data } = requestFormdata();
        this.initMainform();
    },
    methods: {
        initMainform() {
          this.formdata = data.baseData.dataInfo; 
          this.selectionParams = data.baseData.paramInfo.map(callback);
        },
    }
    
    假如我们还是在MainForm组件中,通过props传递数据给子组件SubForm1,SubForm2(父子通信,采用props传值,而props是异步获取的,需要在子组件中用watch监听props的变化,动态地赋值给子组件);
    // 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.subData1formData.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中的mutationsgetters完成。 子组件中也通过vuex获取数据

// SubForm1.vue

computed: {
    ...mapGetters(['mapSubform1Info']),
},
methods: {
    initSubForm1() {
        this.subForm1Data = this.mapSubform1Info;
    }
}

总结

  • 在VUEX中集成 entity & adapter,可以对多组件都需要访问的数据进行集中管理,并且通过getters将数据映射为组件需要的结构,然后暴露给所需要的组件。
  • 组件内部的逻辑更加清晰,让组件内部的方法更多地关注UI交互本身。
  • 组件内涉及到数据的初始化、增删改查等操作时,组件内只负责调用外部引入的VUEX、抽象后的业务函数。