RC-Field-Form源码解析与实现

本文介绍了rc-field-form的使用方法,重点讲解了useForm、Form组件和Field组件的交互,以及如何实现表单验证和提交处理。源码分析深入浅出。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基本介绍

本文中涉及到的关键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我们可以总结出他的功能有如下几点:

  1. 通过自定义Hook useForm来获取表单的实例
  2. 可以通过form实例中的setFieldsValueAPI 来设置表单的初始值
  3. 每个Field组件都有name并且可以传递rules属性来验证当前的值是否符合规则
  4. 提交表单时,如果表单中的子组件都符合规则,则触发onFinish方法如果并获取表单内所有组件的值
  5. 如果验证失败,则触发onFinshFailed方法,并获取不满足规则的字段及其规则

源码分析

对应的源码地址rc-field-form源码

逻辑关系图

在分析源码及其使用的方式之后,站在使用者的视角逻辑关系图大概如下所示:
在这里插入图片描述

主要文件功能

我梳理了rc-field-form的内部主要逻辑关系图,其中几个关键性的文件有如下几个:

  1. index.tsx: 入口,导入/导出内部各个组件及自定义Hook比如Form、Field、useForm…
  2. FieldContext.tsx: 创建一个Context供Form子孙组件获取form实例
  3. Form.tsx:
    1. 封装的form组件并通过setCallBack来设置表达组件的回调函数
    2. 通过FieldContext.Provider来共享form实例给其子孙组件
    3. 表单提交时,阻止默认时间&触发传进来的自定义表单事件
  4. Field.tsx:
    1. 将传递进来的子组件改为受控组件
    2. 消费Form组件共享的form实例
    3. 通过form实例上的方法收集Field组件实例
    4. 监听/取消监听数据变化时,对应的 Field组件更新
  5. useForm.tsx:
    1. 开辟一个新的数据存储空间store来存储表单数据
    2. 收集各个Field的组件实例
    3. 收集 Form组件中的表单成功/失败的方法
    4. 收集各个字段的校验规则
    5. 提供getFieldsValue、getFieldValue、setFieldsValue...等API 来操作表单中的数据
    6. 通过useRef保存formStore的实例

内部逻辑关系图

整体的逻辑关系如下所示
在这里插入图片描述

代码实现

基于上面的分析,我们就可以自己实现一个精简版本的rc-field-form
可以新建一个文件夹为my-rc-field-form,其整个目录结构如下所示:
在这里插入图片描述

useForm

我们先看useForm.js这个文件内的代码,这个文件是最核心的,也是相对比较复杂的。
根据上面的源码分析,我们知道,在useForm内我们要实现下面几点主要功能

  1. 开辟一个新的数据存储空间store来存储表单数据
  2. 收集各个Field的组件实例
  3. 收集 Form组件中的表单成功/失败的方法
  4. 收集各个字段的校验规则
  5. 提供getFieldsValue、getFieldValue、setFieldsValue...等API 来操作表单中的数据
  6. 通过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的使用三步走的策略

  1. 创建Context对象
  2. 通过Provider传递数据给子孙组件
  3. 子孙组件消费传递下来的数据,而子孙组件消费的方式又有三个,分别为:
    1. useContext:这是一个hook,自然的只能在函数组件或者自定义hook中使用
    2. contextType: 是能在class组件中使用,并且只能订阅单一的context来源
    3. Consumer: 没有啥限制,函数组件和类组件都能使用,就是用起来比较烦

该文件关键代码如下:

import React from 'react';
// context三步走
const FieldContext = React.createContext()
export default FieldContext

Form

由上面的源码分析我们知道,在该文件内需要实现的功能如下:

  1. 封装的form组件并通过setCallBack来设置表达组件的回调函数
  2. 通过FieldContext.Provider来共享form实例给其子孙组件
  3. 表单提交时,阻止默认时间&触发传进来的自定义表单事件

在这里其实默认是使用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

通过源码分析我们知道,在该文件夹中实现的主要功能如下:

  1. 将传递进来的子组件改为受控组件
  2. 消费Form组件共享的form实例
  3. 通过form实例上的方法收集Field组件实例
  4. 监听/取消监听数据变化时,对应的 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

执行效果

请添加图片描述

线上实现demo

相关文档

rc-field-form源码地址
rc-field-form的基本使用
线上实现demo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

问白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值