复杂表单的模块化处理

栏目: 编程语言 · 发布时间: 4年前

内容简介:工作中或多或少都会遇到一些很复杂的表单,通常的特点是每个表单项自身都有一大坨逻辑,好几百行代码。如果将所有表单项都放到一个文件里,那么没人能看得懂它,维护起来是一个噩梦。为了解决这个问题,小组内之前做过一些封装和拆分:上面这样做在表单复杂度不是那么高时可以很好应对,但随着业务越来越复杂,遇到了新的问题:

工作中或多或少都会遇到一些很复杂的表单,通常的特点是每个表单项自身都有一大坨逻辑,好几百行代码。如果将所有表单项都放到一个文件里,那么没人能看得懂它,维护起来是一个噩梦。为了解决这个问题,小组内之前做过一些封装和拆分:

  • 将每个表单项封装成独立组件
  • 在业务上将表单项分类,功能内聚的一组表单项放到一个 form group 下,这样整个表单被拆分为多个 form group ,同时每个 form group 也封装成组件:

复杂表单的模块化处理

  • form 汇集下属所有 form group 的互操作和业务逻辑,同时 form group 汇集下属所有 form item 的互操作和业务逻辑。例如收集表单提交时的接口数据,表单项显隐的控制
  • 统一数据流管理,放到 vuex 中,但不够彻底,组件内部仍然有维护小部分数据

上面这样做在表单复杂度不是那么高时可以很好应对,但随着业务越来越复杂,遇到了新的问题:

  • formform group 由于需要汇集下属所有组件的互操作和业务逻辑,导致组件变的非常大,代码可以很轻易的增长到上千行
  • 每个表单项的逻辑散落在多处,难以整理出其具体是怎么工作的
  • 维护困难,虽然可能单独一个表单项的逻辑只有几百行,但是混杂在了父组件上千行的代码中,轻易不敢乱动

思路

仔细思考了困境,发现其实 formform group 并不需要维护具体的业务逻辑,它们应当只是做一些简单的汇总工作。如果将业务逻辑全部内聚到单独的表单项,那么维护起来将会非常方便。表单项的核心逻辑有:

  • 为了能够工作,需要从完整的 form data 中获取哪些数据
  • 当表单提交时,自身需要贡献哪些数据到 form data
  • 是否展示、隐藏的控制逻辑,一个表单项的显隐通常是受到某些其他表单项、上层 formform group 组件的状态影响
  • 所有数据完全由 vuex 托管,不再散落多处
  • 每个表单项独立的 moduleform group 也有独立 module ,与组件树一一对应形成 module tree

表单项完全自治内聚, form 只用在初始化时将后端拉取的完整 form data 传递给表单项,由表单项自行关心的字段;在表单提交时,由于表单项内部已经准备好需要贡献哪些数据, form 只用直接拿过来即可。

基于以上思路,势必有很多通用逻辑,提取出来后就可以方便的复用了, demo 代码放在最后,下面具体说说实现细节。

总体架构

复杂表单的模块化处理

如上,组件树与 vuex module 树一一对应, formform groupform item 都有各自的 module

vuex 中涉及多层次 module 时上下层通信不是很方便,必须知道 namespace 才行, 蛋疼的是 vuex 并没有提供很好的机制方便上层 module 了解其下属 module 的情况。

为此参考组件注册的做法做了一个约定, modulestate 中以 _moduleKey 作为注册时的 namespace ,父 module 将所有子 modulenamespace 放入自身的 state 属性中 。例如:

// form item module
export default {
  state: {
    _moduleKey: 'form-item-id',
  },
}
// form group module
import formItemId from './form-item-id';

export default {
  state () {
    return {
      _moduleKey: 'form-group-1',
      _formItems: [formItemId.state._moduleKey], // 记录所有子module的namespace
    }
  },
  modules: {
    [formItemId.state._moduleKey]: formItemId, // 注册module时的key设置为子module的namespace
  },
}
// form module
import formGroup1 from './form-group-1';

export default {
  state () {
    return {
      _moduleKey: 'form-demo',
      _formGroups: [formGroup1.state._moduleKey], // 记录所有子module的namespace
    }
  },
  modules: {
    [formGroup1.state._moduleKey]: formGroup1, // 注册module时的key设置为子module的namespace
  },
}
// vuex store
import formDemo from './form-demo';

new Vuex.Store( {
  modules: {
    [formDemo.state._moduleKey]: formDemo, // 注册module时的key设置为子module的namespace
  },
} );

通过这种约定,父 module 就能非常方便的与子 module 通信了,例如:

state._formItems.forEach( ( formItemModuleKey ) => {
  commit( `${ formItemModuleKey }/mutation1` );
  dispatch( `${ formItemModuleKey }/action1` );
} );

但是受制于 vuex ,子 module 仍然无法与父 module 通信,因为此时 vuex 需要提供完整的 namespace 路径才能定位父 module 具体的 mutaion/action

上层数据收集

通过上述架构,所有具体的业务数据都分散在每个表单项 module 里,一些“宏观”数据就需要一个收集过程,例如表单提交时传递给后端接口的数据、表单校验时传递给 form 组件的 model 属性。借助上面的架构,这一项工作比较简单。

form data

这是表单提交时传递给后端接口的数据,大体思路是每个表单项都会贡献自己的一小份数据,最后汇总到一起。

复杂表单的模块化处理

每个表单项都有自己的 formItemData ,最后打平汇总到一起。

一个示范:

复杂表单的模块化处理

表单项、 form group 显隐的影响

在很多时候,如果某个表单项或者 form group 是隐藏的,那么即使其内部状态已经发生变化,也只能贡献初始状态的数据到 form data 。因此 表单项内部的 formItemData 实际上需要区分成两份,一份 formItemData4Show 用于在展示时贡献给 form data ,另一份 formItemData4Hide 用于在隐藏时贡献。

复杂表单的模块化处理

在上面的例子里,如果贡献 id 属性的表单项在被隐藏时贡献的 id 是空串的话,

formItemData4Hide () {
  return {
    id: ''
  }
},
formItemData4Show ( state ) {
  return {
    id: +state.id
  };
},

那么最终的 form data 就是:

复杂表单的模块化处理

form model

此数据通常用于表单校验,表单校验情况比较复杂,因为校验时可能不仅仅需要用表单控件自身的值,也可能获取组件内部数据,父组件数据等等,最好的办法是 form model 获取一份非常全的数据。

为此 form model 是收集各个表单项的 state ,同时避免为 state 内同名属性的影响,最终不打平数据而是以子 modulenamespace 作为 key

复杂表单的模块化处理

一个示范:

复杂表单的模块化处理

那么表单项校验时如何获取数据呢? 通常需要两步

  1. form 组件声明 model 属性为 formModel

    <el-form :model="formModel">
      xxx
    </el-form>
    
  2. 表单项组件声明 prop 为对应的 namespace ,也就是上面定义的 _moduleKey

    <el-form-item prop="form-item-id" label="id">
      xxx
    </el-form-item>
    

这样在校验函数里,拿到的就是 formModel 中对应 namespace 的数据

复杂表单的模块化处理

数据初始化与同步

module 数据初始化

上面也有提到所有具体的业务数据都分散在每个表单项 module 里,那么当在编辑已有表单时势必有一个拉取后端数据并回填页面的过程。数据获取是由顶层表单组件来做的,需要在 vuex 中与所有下层表单项 module 通信传递数据。借助上面的 namespace ,这一步简单了很多:

// form module

fillForm ( { dispatch, getters }, backendData ) {
  // 利用后端数据回填表单,分发到每个form group来做
  getters.formGroups.forEach( ( formGroupModuleKey ) =>
    dispatch( `${ formGroupModuleKey }/fillFormGroup`, backendData
  ) )
},
// form group module

fillFormGroup ( { dispatch, getters }, formData ) {
  // 每个form item自身决定取哪些数据
  getters.formItems.forEach( ( formItemModuleKey ) => {
    dispatch( `${ formItemModuleKey }/data2State`, formData );
  } );
},
// 某个form item module
{
  mutations: {
    update ( state, newState ) {
      Object.keys( newState ).forEach( key => {
        state[ key ] = newState[ key ];
      } )
    },
  },
  actions: {
    data2State ( { commit }, formData ) {
      commit( 'update', { // 只取自己关心的数据字段
        id: formData.id,
        name: formData.name,
      } )
    }
  }
}

如上,每个表单项组件只需取自己关心的数据字段,对于可维护性有大大提高,因为 只要看 data2State 函数就能明白此表单项的数据依赖

最后只需在顶层表单组件中触发 fillForm 即可,如:

{
  mounted() {
    // 模拟数据拉取
    setTimeout(() => {
      this.fillForm({
        id: 1,
        name: 2,
        desc: 3,
        text: 4
      });
    }, 2000);
  },
  methods: {
    ...mapActions("demo", ["fillForm"]),
  }
}

module 数据同步

因为各个表单项的 module state 是在初始化时通过 data2State 一次性值拷贝过来的,所以当全局性的 formData 有变更时,表单项的 module state 不能响应式同步。这会带来一些问题,比如表单项 A 改变了 formData 中表单项 B 关心的某个属性,表单项 B 是不知道的。

需要有一种机制在数据改变时将最新数据同步到各个 module ,借助 vuex 提供的 plugin 机制可以做到这点:

// 同步表单项module。 namespace是form module的_moduleKey
export default function createSyncPlugin ( namespace ) {
  return store => {
    let dispatched = false; // 避免无限循环

    // formItemModulePaths: 各表单项module的namespace路径
    const formItemModulePaths = store.getters[ `${ namespace }/formItemModulePaths` ];
    const moniteTypes = formItemModulePaths.map( key => `/${ key }/` );

    // 监听所有表单项module的mutation, 更新所有module到最新状态
    store.subscribe( ( mutation ) => {
      const { type = '' } = mutation; // 触发的mutation名
      const index = moniteTypes.findIndex( moniteType => type.includes( moniteType ) );

      // 每次有某个module触发update的mutation时,联动其他module的state更新到最新,因为可能有互相依赖
      if ( index > -1 && !dispatched )
      {
        dispatched = true;
        moniteTypes.forEach( ( _, curIndex ) => {
          if ( curIndex !== index )
          {
            store.dispatch( `${ namespace }/${ formItemModulePaths[ curIndex ] }/data2State`, store.getters[ `${ namespace }/formData4View` ] )
          }
        } )
        dispatched = false;
      }
    } );
  };
}

new Vuex.Store( {
  modules,
  plugins: [ createSyncPlugin( 'demo' ) ],
} );

上面的逻辑可以用下图表示:

复杂表单的模块化处理

注意传给 data2State 的是 formData4View 而不是 formData ,二者很类似,只不过前者是所有表单项的 formItemData4Show 合集。

组件渲染

由于 vuex 掌控着数据,组件显隐的判断最好也放在 vuex 中,如:

// form item module
{
  getters: {
    isVisible ( state ) {
      return state.id !== 10;
    },
  }
}

那么上层组件在 template 中渲染时,就需要知道下层组件是否展示,得益于组件树与 module 树的一一对应关系,只需获取下层 module 里的 isVisible 属性即可。如:

// form group module

{
  getters: {
    // 下属某个表单项是否可见
    isFormItemVisible ( state, getters ) {
      return formItemName => getters[ `${ formItemName }/isVisible` ];
    },
  }
}

这样我们在 vue template 里只要调用 isFormItemVisible 并传入表单项 module_moduleKey :

<!-- form group template -->

<form-item-text v-show="isFormItemVisible('form-item-text')"/>

如果约定组件的name和_moduleKey一致,那么直接用v-for就能完成渲染逻辑:

<!-- form group template -->

<template>
  <!-- formItem是_moduleKey,同时与组件name相同 -->
  <component
    v-for="formItem in formItems"
    v-show="isFormItemVisible(formItem)"
    :key="formItem"
    :is="formItem"
  />
</template>
<script>
export default {
  name: "form-group-2",
  computed: {
    ...mapGetters("demo/form-group-2", ["formItems", "isFormItemVisible"])
  }
};
</script>

module固定属性

以上就是整个表单模块化的思路了, module 层涉及到很多特有属性,在此做个总结:

复杂表单的模块化处理

加粗的部分属性值需要各 module 自行设置,其余部分均可以抽取成公共逻辑,这样每个表单就可以很方便复用了。

最后附上 demo 仓库: modulize-form


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

重构

重构

[美]马丁•福勒(Martin Fowler) / 熊节 / 人民邮电出版社 / 2015-8 / 69.00

本书清晰揭示了重构的过程,解释了重构的原理和最佳实践方式,并给出了何时以及何地应该开始挖掘代码以求改善。书中给出了70 多个可行的重构,每个重构都介绍了一种经过验证的代码变换手法的动机和技术。本书提出的重构准则将帮助你一次一小步地修改你的代码,从而减少了开发过程中的风险。一起来看看 《重构》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器