基本介绍
本文中涉及到的关键npm包的版本信息如下:
react 的版本为18.2.0
rc-field-form的版本为1.37.0
rc-field-form
是一个非常受欢迎的前端表单组件库,AntDesign中的Form
表单就是基于rc-field-form
实现的。今天就来解析一下rc-field-form
的源码部分,并且会自己实现一个相同功能的rc-field-form
基本使用
先看一下rc-field-form
的基本使用, 关键代码如下:
const Demo = () => {
const [form] = Form.useForm()
const onFinish = (values) => {
console.log('onFinish', values);
}
const onFinshFailed = (errorInfo, values) => {
console.log('onFinishFailed', errorInfo, values);
}
useEffect(() => {
form.setFieldsValue({
username: 'default'
})
}, [])
return (
<Form
form={form}
onFinish={onFinish}
onFinishFailed={onFinshFailed}
>
<Field name="username" rules={[nameRules]}>
<Input placeholder="Username" />
</Field>
<Field name="password" rules={[passwwordRules]}>
<Input placeholder="Password" />
</Field>
<button>Submit</button>
</Form>
);
};
附上线上链接rc-field-form
的基本使用
通过上面的基本使用的Demo我们可以总结出他的功能有如下几点:
- 通过自定义Hook useForm来获取表单的实例
- 可以通过form实例中的
setFieldsValue
API 来设置表单的初始值 - 每个
Field
组件都有name
并且可以传递rules
属性来验证当前的值是否符合规则 - 提交表单时,如果表单中的子组件都符合规则,则触发
onFinish
方法如果并获取表单内所有组件的值 - 如果验证失败,则触发
onFinshFailed
方法,并获取不满足规则的字段及其规则
源码分析
对应的源码地址rc-field-form源码
逻辑关系图
在分析源码及其使用的方式之后,站在使用者的视角逻辑关系图大概如下所示:
主要文件功能
我梳理了rc-field-form
的内部主要逻辑关系图,其中几个关键性的文件有如下几个:
- index.tsx: 入口,导入/导出内部各个组件及自定义Hook比如Form、Field、useForm…
- FieldContext.tsx: 创建一个Context供
Form
子孙组件获取form
实例 - Form.tsx:
- 封装的
form
组件并通过setCallBack来设置表达组件的回调函数 - 通过FieldContext.Provider来共享
form
实例给其子孙组件 - 表单提交时,阻止默认时间&触发传进来的自定义表单事件
- 封装的
- Field.tsx:
- 将传递进来的子组件改为受控组件
- 消费
Form
组件共享的form
实例 - 通过
form
实例上的方法收集Field
组件实例 - 监听/取消监听数据变化时,对应的
Field
组件更新
- useForm.tsx:
- 开辟一个新的数据存储空间store来存储表单数据
- 收集各个
Field
的组件实例 - 收集
Form
组件中的表单成功/失败的方法 - 收集各个字段的校验规则
- 提供
getFieldsValue、getFieldValue、setFieldsValue...
等API 来操作表单中的数据 - 通过
useRef
保存formStore
的实例
内部逻辑关系图
整体的逻辑关系如下所示
代码实现
基于上面的分析,我们就可以自己实现一个精简版本的rc-field-form
了
可以新建一个文件夹为my-rc-field-form
,其整个目录结构如下所示:
useForm
我们先看useForm.js
这个文件内的代码,这个文件是最核心的,也是相对比较复杂的。
根据上面的源码分析,我们知道,在useForm
内我们要实现下面几点主要功能
- 开辟一个新的数据存储空间store来存储表单数据
- 收集各个
Field
的组件实例 - 收集
Form
组件中的表单成功/失败的方法 - 收集各个字段的校验规则
- 提供
getFieldsValue、getFieldValue、setFieldsValue...
等API 来操作表单中的数据 - 通过
useRef
保存formStore
的实例
针对第1~5功能我们可以新建一个class
在这个class
内实现对应的各种API来实现我们想要的功能,这里的验证我们就先实现简单的校验,如果有值则通过校验,如果是空值则校验失败,具体代码如下:
// 数据仓库,所有Form中的数据和校验都可以在这里
class FormStore {
constructor() {
// key : value
this.store = {}
// 用于收集各个Field的组件
this.fieldEntity = []
// 存储Form组件的成功/失败的调用方法
this.callBack = {}
// 存储各个字段校验的规则
this.validateRule = []
}
setCallBack = (newCallBack) => {
this.callBack = {
...this.callBack,
...newCallBack
}
}
// 新增规则
setValidateRule = (newValidateRule) => {
const exitFlag = this.validateRule.some(rule => rule.name === newValidateRule.name)
// 如果对应的name规则已存,则不在存储了
if (exitFlag) return
this.validateRule.push(newValidateRule)
}
getFieldsValue = () => {
return {...this.store}
}
getFieldValue = (name) => {
return this.store[name]
}
setFieldsValue = (newStore) => {
// 1. 更新store中的值
this.store = { ...this.store, ...newStore}
// 2. 更新组件field
// 需要拿到拿到各个Field的实体,数据更新之后,更新对应的Field组件,这样UI界面上的数据也就自然更新了
// 需要先收集到各个Field的组件
// 根据传递进来的newStore中的key来判断需要更新那个Field组件
// field组件可以收集实例
this.fieldEntities.forEach((entity) => {
// 只更新目标组件Field
Object.keys(newStore).forEach((k) => {
// 更新的k就是Field中的name
if (k === entity.props.name) {
entity.onStoreChange();
}
});
});
}
// 收集各个Field的组件
setFieldEntities = entity => {
this.fieldEntity.push(entity)
// 返回一个函数,用以移除收集到的Field组件
return () => {
this.fieldEntity = this.fieldEntity.filter(item => item !== entity)
}
}
// 校验各个Field组件是否满足规则
validate = () => {
let errorList = []
const store = this.getFieldsValue()
this.fieldEntity.forEach(fieldEntity => {
const { name, rules } = fieldEntity.props
if (rules) {
rules.forEach(ruleItem => {
// 校驗
const { required, message } = ruleItem
if (required && !store[name]) {
// 必须参数,但是没有值
errorList.push({
error: message
})
}
})
}
})
return errorList
}
针对第6点功能,需要注意,这里应该是单例模式,即整个表单只应该有一个FormStore
的实例。并且要保证表单的及其子孙组件在任何声明周期内访问的都是同一个。很自然的我们想到可以使用React
中的useRef
来保存上面的formStore
实例。这样就满足我们的要求了。对应的关键代码如下:
export default function useForm(form) {
// 需要一个值,能够在组件的任何一个生命周期里都指向同一个对象
// ref 返回的对象可以保证我们在组件的任何一个生命周期中,指向的都是同一个对象
const formRef = useRef()
if (!formRef.current) {
if (form) {
// 如果接受了form则直接把接受的form挂载到ref上
formRef.current = form
} else {
// 如果没有接受
// 首次ref上没有值,则把对象挂载上去
const formStore = new FormStore()
formRef.current = formStore.getForm()
}
}
return [formRef.current]
}
FieldContext
由上面的源码分析我们知道,该文件主要需要实现功能为:创建一个Context供Form
子孙组件获取form
实例
很自然的,我们想到使用React
中的useContext()
这个API 来创建一个context
对象来保存数据
这里就自然涉及到Context
的使用三步走的策略
- 创建
Context
对象 - 通过
Provider
传递数据给子孙组件 - 子孙组件消费传递下来的数据,而子孙组件消费的方式又有三个,分别为:
- useContext:这是一个hook,自然的只能在函数组件或者自定义hook中使用
- contextType: 是能在class组件中使用,并且只能订阅单一的
context
来源 - Consumer: 没有啥限制,函数组件和类组件都能使用,就是用起来比较烦
该文件关键代码如下:
import React from 'react';
// context三步走
const FieldContext = React.createContext()
export default FieldContext
Form
由上面的源码分析我们知道,在该文件内需要实现的功能如下:
- 封装的
form
组件并通过setCallBack来设置表达组件的回调函数 - 通过FieldContext.Provider来共享
form
实例给其子孙组件 - 表单提交时,阻止默认时间&触发传进来的自定义表单事件
在这里其实默认是使用form
组件来包装了一层。其关键代码如下:
import FieldContext from "./FieldContext"
import useForm from "./useForm"
// form 这里的form就是业务组件在使用Form时使用useForm实例出来的useForm实例,将其作为共享数据放在context中,让子孙组件可以操作共享数据
function Form({children, form, onFinish, onFinishFailed}) {
// 在这里获取的useForm返回的数一个数组,传递给子孙组件的应该是一个对象
// 这里就给函数组件,class组件使用Form的话,不能像函数组件一样使用useForm,所以当业务组件是class组件时,需要重新实例化一个组件
// const [formInstance] = useForm(form)
form.setCallBack({
onFinish,
onFinishFailed
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
form.submit()
}}
>
{/* formInstance 就在Form中传递给子孙组件了,子孙组件就可以使用了 */}
<FieldContext.Provider value={form}>
{children}
</FieldContext.Provider>
</form>
)
}
export default Form
Field
通过源码分析我们知道,在该文件夹中实现的主要功能如下:
- 将传递进来的子组件改为受控组件
- 消费
Form
组件共享的form
实例 - 通过
form
实例上的方法收集Field
组件实例 - 监听/取消监听数据变化时,对应的
Field
组件更新
在第一点功能中,我们可以使用React.cloneElement()
这个API来给原来的组件添加value属性 & onChange方法
改变原本组件为受控组件。
第二点功能中,我们在class组件中可以使用static contextType = FieldContext;
方式来指定context
的来源
第三,第四功能可以通过Form
组件共享的formStore
实例中的API来操作
关键代码如下:
import React, {Component} from "react";
import FieldContext from "./FieldContext";
class Field extends Component {
// 类组件消费caontext的方式,使用静态属性 contextType来指定是那个context(有时可能同时存在多个context)
// 指定之后,在class组件中就可以使用this.context消这一context了
static contextType = FieldContext;
componentDidMount() {
// 将当前的Field组件收集
// console.log('this.context', this.context);
this.unregister = this.context.setFieldEntities(this);
}
// 该Field组件卸载时,自然的需要将cmd收集的Field组件删除掉
componentWillUnmount() {
this.unregister();
}
// 让组件更新
onStoreChange = () => {
this.forceUpdate();
};
getControlled = () => {
// console.log('this.context', this.context);
const {name} = this.props;
const {getFieldValue, setFieldsValue} = this.context;
return {
// value: "omg", //"omg", //get(name) store
value: getFieldValue(name), //"omg", //get(name) store
onChange: (e) => {
const newVal = e.target.value;
// console.log("newVal", newVal);
// store set(name)
setFieldsValue({[name]: newVal});
},
};
};
render() {
const {children} = this.props;
// 将接受到的非受控组件input改为受控组件,第二个参数是任何在clone元素中你需要加的属性,这里一个是value,一个是onChange受控组件
const returnChildNode = React.cloneElement(children, this.getControlled());
return returnChildNode;
}
}
export default Field;
index
在该文件中,主要是导入导出内部的各个组件,并把Field & useForm
挂载在Form
组件上。关键代码如下:
import Field from "./Field";
import _Form from "./Form";
import useForm from "./useForm";
const Form = _Form;
Form.Field = Field
Form.useForm = useForm
export { useForm, Field }
export default Form