前言
Form
是项目中经常使用的组件,为了能熟练驾驭它,深入 Form
源码还是有所必要的。我们知道,在 Ant Design 4
之后,Form
的底层使用的是 rc-field-form
,因此在了解源码时,我们也得对其底层的依赖深挖一番,这样才能了解的更透彻。
rc-field-form
💻 仓库地址:【传送门】(建议动手运行一下,会了解的更快)
⚠️ 注:表单的所有操作都离不开顶层的
FormStore
,所有的变动信息最终都会汇聚在FormStore
内。
整体结构
在了解源码之前,我们先大致了解一下 rc-field-form
的结构,因为 Ant Design Form
是基于此封装的,因此熟悉 rc-field-form
对我们解读源码来说很重要。
页面渲染的结构树如下:
基于此,我们使用 ReactDom
的方式用代码描述大致为:
1 | /** |
结构解析
根据 rc-field-form
源码的渲染逻辑,这里我花了一张结构图:
其渲染的步骤大致就是:
- 通过
useForm
创建顶层的FormStore
存储库,然后通过FieldContext
向下传递; Field
获取到由FieldContext
传下的顶层FormStore
,获取需要的name
和一些操作方法,对DomeElement
做克隆和属性封装;- 根据最终传入的
Dom
类型,做相应的格式化处理和呈现;(有可能传入的为函数)
能力拆解
整个 rc-field-form
的能力可简单拆解为:
数据存储
:在Form
顶层维护了一个Context
存储库,对Form
内注册的任何子表单项的变动,都通过注册好的get
、set
方法来存入store
内;表单对象
:顶层通过useRef
生成一个不随组件更新的formInstance
对象,然后通过useForm
生成,当初次渲染时创建出FormStore
绑定即可,后续如果需要刷新则触发初始化绑定的forceUpdate
方法即可刷新(我们通过form
拿到的表单对象就是这个formInstance
);表单校验
:使用async-validate
插件库校验,拼接格式则是通过name
+rules
字段来生成(组件注册阶段时生成组件描述数组即能方便的获取结构);变更事件
:表单项的变更事件是在注册Field
时绑定的,将传入的children
进行cloneElement
操作,生成目标子元素并改写props
(通过getControlled
方法去改写props
以及增加校验等操作)。命名获取
:表单项有个name
字段是用于定义表单名的,层级越深的表单要设置其name
时,需要通过getNamePath
方法来拼接成最终的name
,最终 join 生成的namePath
大致为prefixPath_currentName
其中,formInstance
暴露出来的能力为(顶层 FormStore
暴露的能力):
而这也可以算是 rc-field-form
对外暴露的所有能力了(内部拼接 name
的方法是 getNamePath
,建议留心一下)。
深入 AntD Form
AntD Form
组件及能力
图中有 AntD
在 4.20
支持的新特性:useWatch
、useFormInstance
:
useFormInstance
:返回useForm
获取的form
对象,这对于子组件拿顶层form
对象来说很方便;useWatch
:监听字段变化返回更新后的值,仅当改字段变化时重新渲染(内部用useEffect
做初始化赋值)。
⏰ 暴露的
Form.Provider
一般自己封装,相当于就是使用rc-field-form
自己手写Provider
层一样,一般用不着。
Form
结构
AntD Form
整体基于 rc-field-form
封装了一层,其拓展了对于 AntD
其余组件使用的能力和样式的填装。我们看个简单例子:
这里大致可分为四层:
Form Provider
:此部分实现了rc-field-form
需要手写的部分;Custom Provider
:此部分主要为AntD
需要的样式控制的Context
;Field Provider
:沿用rc-field-form
的FieldContext
,添加了一些“错误呈现”、“dependencies 依赖”等内容;Content
:主要做值绑定和一些校验结果、样式等的呈现,内部有自定义的关联Context
;
InternalForm
相关内容
A. 内部调用的 useForm
封装了什么?
useForm
额外增添的内容不多,一个是 __INTERNAL__
,它的作用就是为 React.forwardRef
暴露通过 useForm
生成的对象(可以了解一下 useImperativeHandle
)。
B. 它在 Form
的基础上又做了什么?
- 初始化了很多关于样式控制相关的内容(这里将 disable、layout、size 等配置均归属于样式配置);
- 向外暴露 ref 对象;
- 添加
scroll
错误定位; - 自动绑定
rc-field-form
需要用户写的Provider
;
FormItem
相关内容
A. FormItem
主要作用
- 决定表单的呈现方式:
函数式
、组件式
、数组
; - 组件更新判断,局部更新处理;
- 校验相关的绑定;
总而言之,就是将
Field
组件做AntD
的本地化处理(通过函数子组件获取Field
解析好的所有配置)。
B. FormList
的对 FormItem
的封装有什么?
FormList
其实就是调用 rc-field-form
的列表操作,然后让前端自己去对应封装 FormItem
。
rc-field-form
的 List
组件则是对外暴露了一个函数(我们用官网 List
组件常用的那个函数),它最终操作的就是 Field
组件暴露出来的能力。
可动手的点
从 AntD
对外暴露的能力来看,我们基本没法基于它包装内容,因为它基本上就是包装 rc-field-form
,却又没有对外面暴露 rc-field-form
支持的能力。因此通过锁定版本,根据其依赖的 rc-field-form
版本来扩充一些能力,而这就是 @alife/form-tools
封装出的原因。目前业务上使用表单比较难受的点有:
form
对象的深层传入,深层次组件的namePath
不好监听(4.20.0 版本已支持)。- 对于兄弟组件间的事件消息处理的很难受,特别是值变更事件(4.20.0 版本已支持);
- 对象类表单数据的
数组命名法
很难受,无法复用表单组件; - 复杂表单的初始化逻辑均收敛在上层,一些需要根据值做接口查询呈现的组件开发起来很困难;
那么基于这些难点,我们根据 rc-field-form
内部暴露的一些能力可做一些封装。
form
对象跨层级传递
4.20
版本解决了跨层级传递 form
对象的问题,直接使用 Form.useFormInstance
获取到 form
对象,不用为之前需要通过 props
传值而烦恼了。
🌰 使用示例:示例传送门
但是它也有缺陷:
- 获取
form
对象就得用到全量的NamePath
,难做到表单组件复用。
☕️ 如果需要将表单组件抽离出来做公共组件(它们仅包裹层的
name
不同),那么内部使用form
的一些方法就会很难受(因为没法知道它的prefixName
到底是什么)。
因此可基于 rc-field-form
做一层优化:直接获取当前路径下的 form
对象,即“自动前缀补全”。
1 | import { useCallback, useContext, useMemo } from 'react'; |
这样即使组件拆分出来,也可以不用关注 Form
命名前缀的问题,直接使用即可(数组在套用了 FormAccess
之后也适用此方法)。
兄弟组件事件处理
我们知道,如果要获取到兄弟组件间值的变更,设置此类事件监听方法很是头疼,不过在 4.20
版本提供的 useWatch
方法能够比较方便的解决这个问题,同时其获取值的能力是通过 useEffect
获取的,初始化时也能监听到变更。
🌰 使用示例:示例传送门
useWatch
的缺陷(同 4.1 所述问题)
- 一旦需要复用组件,限定死的
NamePath
会导致组件最终耦合太大,无法拆分。
因此同样可按照 4.1 中的处理方式,注册个虚拟的 Field
,然后通过它来做事件监听,从而模拟 dependencies
的能力:
1 | useEffect(() => { |
更为舒适的命名方式
先前提到过,FormItem
内的表单 name
值的获取其实是用 Field
的函数方式来获取到 prefixName
,最终拼接好 props
上传入的 name
来设置全名的,整体的使用很被动,这就导致:
获取值、监听事件等都需要拼接全名,对于复杂表单来说很麻烦;
数组形式的命名,会使得表单难以解耦,难以做到表单模块的复用;
rc-field-form
拼接名称的方式:
WrapperField
获取到当前fieldContext
的prefixName
值,再传入Field
组件根据传入的name
来拼接;
既然它的 prefixName
是来自 fieldContext
的,而 fieldContext
的 value
又来自 formContextValue
,且 fieldContext
内部并没包裹额外逻辑,那么我们为何在包裹表单对象的时候再包一层 FieldContext
,直接敲定它的 prefixName
,这样不就能优化掉 Form.Item
的数组命名方式吗?
@alife/form-tools
的功能拓展参考
1 | /** |
使用方法示例
1 | // 值为: { obj: { test: xxx } } |