如何打造一款,平民化的、高性能的,类表单体系(原理篇)
什么叫高性能的表单
尤其是在写后台管理系统时,我们会经常和表单打交道。单纯的表单的概念没什么难的,就是一堆由不同字段组成的一个结构整体,对应的 UI
库中,字段就是比如,输入框、选择框、多选按钮呀等等表单控件,当提交时会通过某种方式把内部所有的字段收集整理成一个 json
给开发者使用
比较复杂的场景是动态表单,它和一般的表单使用使用有如下区别
- 普通
- 写法上 cv 文档代码,再填充自己的数据即可
- 属于是,由静态组件,生成数据,是一个正向的过程
- 动态
- 写法上是一坨子结构数据,比如从
json
还原回使用表单组件的效果 - 属于是,由最终结构,还原
ui
,是一个逆向或者双向的转换过程
- 写法上是一坨子结构数据,比如从
表单类别的不同,其性能往往取决于使用的 UI
控件是否高效,因为它们是承载每个字段数据的主体
而我这里说的高性能则体现在精准更新 上,即哪个字段变了就只更新那个字段,拿国内常用的组件库 antd/element
举例,我们为了做到更好的逻辑控制往往以受控表单的方式使用,这里大家可以尝试安装 vue/react
它们的开发者工具,然后修改某个字段,会发现多个组件都发生了更新
React
是全量更新
Vue
一般是父组件和对应控件更新
难点分析
如果是非受控组件性能会更好,但组件是为开发业务服务的,当受控时我们可以尽快的拿到输入的内容做些其他事,根据不同框架的更新颗粒度,最少最少也会触发对应字段的控件 + 父组件 更新
框架的更新原理是,如果依赖某个组件的,使用响应式系统创建的变量变化,则发生改变 ,而 React
会更新所有子组件的行为,可以看作是,人家的响应式规则就是如此,等同于所有子组件会被动依赖父组件的响应式更新 (setState 函数的调用)
这里实则我们需要解决两件事
- 根据更新原理把范围圈定到各自字段中,该字段中多少组件更新不关心,只要不是局部影响整体即可
- 提供能在任意表单层级中的局部监听但不触发框架级别的响应式更新能力。我们需要表单受控的原因是,我们需要当某个字段更新时,可能需要关联更新其他字段,或者是干些其他事,比如接口请求
解决方案 ------ 01,精确更新
这个其实很好解决,组件本身就能做到,我们只要不要在顶层使用组件的监听事件即可
取值方式往下看 ->
解决方案 ------ 02,任意层级局部监听
这个东西有些组件库能做到,但是文档是不会给标出来的,因为是人家内部的东西
这个解决起来会麻烦点,想要省心需要摆脱组件库提供管理方法,自建一套规则
我们可以通过在表单组件外层套一个上下文,把全局配置和数据依赖注入进所有子组件中,记得这个数据必须是非响应式的
在每个子组件中,拿到接收到的上下文,当数据改变时,手动把上下文中的数据改掉
取值时,在上下文中就能直接拿了
监听使用订阅发布者模式,可以把每个字段都做成对象,里边挂一个数组,当值改变时,修改上下文中当前字段的内容时,手动调用一遍
以上的东西,逻辑部分可以封装成 hook
,组件封装出来大概 2-3 个就够用了
伪代码说明
VUE
vue
<script setup>
const form = createForm({
config: {}/*初始数据*/
components: { ElButton, ElInput, ElSelect, ElItem, ElSpace }
})
form.getFormData()
form.reset()
</script>
<template>
<FormProvide :form="form">
<Field name="username" component="ElInput" layout="ElItem"></Field>
<Field name="pwd" component="ElInput" layout="ElItem"></Field>
<Field group-name="capture" layout="ElSpace">
<Field name="value" component="ElInput"></Field>
<Field name="img" component="ElImage"></Field>
</Field>
</FormProvide>
</template>
REACT
jsx
const form = createForm({
config: {}/*初始数据*/
components: { Button, Input, Select, FormItem, Space }
})
function App() {
form.getFormData()
form.reset()
return <form.Provider>
<Field name="username" component="Input" layout="FormItem"></Field>
<Field name="pwd" component="Input" layout="FormItem"></Field>
<Field group-name="capture" layout="ElSpace">
<Field name="value" component="Input"></Field>
<Field name="img" component="Image"></Field>
</Field>
</form.Provider>
}
封装逻辑参考
-
createForm
用于创建管理器,主要职责是生成上下文,和批量操作getFormData
获取此刻内部所有的数据reset
比如请求完接口需要清空,调用rest
实则会把每个字段的清空方法批量调用一遍
-
Field
自己封装自定义组件,主要用于给组件库的组件包一层,内部会把组件的修改重置等等方法统一挂到上下文中name
字段名称component
用什么组件填充layout
布局用的包裹组件
-
之所以内容组件通过动态传参的方式放进去,是为了更好的支持动态表单
市场已有的方案
目前 ,相关产品我知道做的最好的应该是阿里的 formily
,同时支持 vue
和 react
还有常见的组件库,就是上手难度真的很高
它确实是个很优秀的产品,但里边存在多少 KPI
成份咱也不清楚,真的是对表单需求很强烈的硬着头皮用还是很推荐的
扩展
表单的难点和后台管理系统的页面还是挺像的,正常写没问题,但要做到高性能就会有同样的问题
后台页面绝大多数都是由表单+表格+分页+其他构成,它们的逻辑是强耦合关系,表单查询和分页更新都会引发表格更新,表格更新又需要用到很多其他地方的数据
如果把它们几个都看成是表单的字段,是否不是感觉也查不到哪里去了。不同的是,对于页面我们能通过状态管理库共享数据,因为页面没有循环的需求,任何时间都是唯一的,而表单不行,每个表单的数据得是唯一的
对于这类问题的一个很通用的解法就是状态管理库,有了它既可以让所有层级数据共享和访问,同时还能兼顾性能
要注意的是:此状态管理库非彼状态管理库,状态管理库是一个概念,它不等用于 pinia/vuex/mobx/redux/zustand/...
,如果想对状态管理库有更深入的理解,可以看我另篇文章