工作中或多或少都会遇到一些很复杂的表单,通常的特点是每个表单项自身都有一大坨逻辑,好几百行代码。如果将所有表单项都放到一个文件里,那么没人能看得懂它,维护起来是一个噩梦。为了解决这个问题,小组内之前做过一些封装和拆分:
- 将每个表单项封装成独立组件
- 在业务上将表单项分类,功能内聚的一组表单项放到一个
form group
下,这样整个表单被拆分为多个form group
,同时每个form group
也封装成组件:
form
汇集下属所有form group
的互操作和业务逻辑,同时form group
汇集下属所有form item
的互操作和业务逻辑。例如收集表单提交时的接口数据,表单项显隐的控制- 统一数据流管理,放到
vuex
中,但不够彻底,组件内部仍然有维护小部分数据
上面这样做在表单复杂度不是那么高时可以很好应对,但随着业务越来越复杂,遇到了新的问题:
form
和form group
由于需要汇集下属所有组件的互操作和业务逻辑,导致组件变的非常大,代码可以很轻易的增长到上千行- 每个表单项的逻辑散落在多处,难以整理出其具体是怎么工作的
- 维护困难,虽然可能单独一个表单项的逻辑只有几百行,但是混杂在了父组件上千行的代码中,轻易不敢乱动
思路
仔细思考了困境,发现其实form
和form group
并不需要维护具体的业务逻辑,它们应当只是做一些简单的汇总工作。如果将业务逻辑全部内聚到单独的表单项,那么维护起来将会非常方便。表单项的核心逻辑有:
- 为了能够工作,需要从完整的
form data
中获取哪些数据 - 当表单提交时,自身需要贡献哪些数据到
form data
- 是否展示、隐藏的控制逻辑,一个表单项的显隐通常是受到某些其他表单项、上层
form
、form group
组件的状态影响 - 所有数据完全由
vuex
托管,不再散落多处 - 每个表单项独立的
module
,form group
也有独立module
,与组件树一一对应形成module tree
表单项完全自治内聚,form
只用在初始化时将后端拉取的完整form data
传递给表单项,由表单项自行关心的字段;在表单提交时,由于表单项内部已经准备好需要贡献哪些数据,form
只用直接拿过来即可。
基于以上思路,势必有很多通用逻辑,提取出来后就可以方便的复用了, demo
代码放在最后,下面具体说说实现细节。
总体架构
如上,组件树与vuex module
树一一对应,form
、form group
、form item
都有各自的module
。
vuex
中涉及多层次module
时上下层通信不是很方便,必须知道namespace
才行, 蛋疼的是vuex
并没有提供很好的机制方便上层module
了解其下属module
的情况。
为此参考组件注册的做法做了一个约定,子module
在state
中以_moduleKey
作为注册时的namespace
,父module
将所有子module
的namespace
放入自身的state
属性中。例如:
1 | // form item module |
1 | // form group module |
1 | // form module |
1 | // vuex store |
通过这种约定,父module
就能非常方便的与子module
通信了,例如:
1 | state._formItems.forEach( ( formItemModuleKey ) => { |
但是受制于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
是空串的话,
1 | formItemData4Hide () { |
那么最终的form data
就是:
form model
此数据通常用于表单校验,表单校验情况比较复杂,因为校验时可能不仅仅需要用表单控件自身的值,也可能获取组件内部数据,父组件数据等等,最好的办法是form model
获取一份非常全的数据。
为此form model
是收集各个表单项的state
,同时避免为state
内同名属性的影响,最终不打平数据而是以子module
的namespace
作为key
:
一个示范:
那么表单项校验时如何获取数据呢? 通常需要两步
form
组件声明model
属性为formModel
1
2
3<el-form :model="formModel">
xxx
</el-form>表单项组件声明
prop
为对应的namespace
,也就是上面定义的_moduleKey
1
2
3<el-form-item prop="form-item-id" label="id">
xxx
</el-form-item>
这样在校验函数里,拿到的就是formModel
中对应namespace
的数据
数据初始化与同步
module
数据初始化
上面也有提到所有具体的业务数据都分散在每个表单项module
里,那么当在编辑已有表单时势必有一个拉取后端数据并回填页面的过程。数据获取是由顶层表单组件来做的,需要在vuex
中与所有下层表单项module
通信传递数据。借助上面的namespace
,这一步简单了很多:
1 | // form module |
1 | // form group module |
1 | // 某个form item module |
如上,每个表单项组件只需取自己关心的数据字段,对于可维护性有大大提高,因为只要看data2State
函数就能明白此表单项的数据依赖。
最后只需在顶层表单组件中触发fillForm
即可,如:
1 | { |
module
数据同步
因为各个表单项的module state
是在初始化时通过data2State
一次性值拷贝过来的,所以当全局性的formData
有变更时,表单项的module state
不能响应式同步。这会带来一些问题,比如表单项A
改变了formData
中表单项B
关心的某个属性,表单项B
是不知道的。
需要有一种机制在数据改变时将最新数据同步到各个module
,借助vuex
提供的plugin
机制可以做到这点:
1 | // 同步表单项module。 namespace是form module的_moduleKey |
上面的逻辑可以用下图表示:
注意传给data2State
的是formData4View
而不是formData
,二者很类似,只不过前者是所有表单项的formItemData4Show
合集。
组件渲染
由于vuex
掌控着数据,组件显隐的判断最好也放在vuex
中,如:
1 | // form item module |
那么上层组件在template
中渲染时,就需要知道下层组件是否展示,得益于组件树与module
树的一一对应关系,只需获取下层module
里的isVisible
属性即可。如:
1 | // form group module |
这样我们在vue template
里只要调用isFormItemVisible
并传入表单项module
的_moduleKey
:
1 | <!-- form group template --> |
如果约定组件的name
和_moduleKey
一致,那么直接用v-for
就能完成渲染逻辑:
1 | <!-- form group template --> |
module固定属性
以上就是整个表单模块化的思路了,module
层涉及到很多特有属性,在此做个总结:
加粗的部分属性值需要各module
自行设置,其余部分均可以抽取成公共逻辑,这样每个表单就可以很方便复用了。
最后附上demo
仓库:modulize-form