在react中实现一个简易版antdForm表单组件(react^17)

459 阅读4分钟

前言

在react中我们大多都使用过第三方Ui库,antdesign,其中表单组件更是高频使用,为了帮我们更好理解该组件,和如何在工作中利用这个组件库的设计思想和原理从而自定义自己的业务组件,这里我们一起来手写一下该表单组件。

设计思想

  1. 需要一个Form组件包裹整个表单内容。
  2. 需要一个Field组件包裹单个表单元素。
  3. 需要为每一个Form表单创建数据存储区域,并将数据一一对应到items上,且当数据改变时,只更新对应Field。

Form

  1. FieldContext 使用上下文进行数据响应式。
  2. useForm 为每个form表单创建一个独立的数据存储区域(核心思路使用react中的ref创建唯一性)。
  3. 将form组件中接受的回调函数,传入到数据管理中心内,以便将来提交时调用。
import React from "react";
import FieldContext from "../FieldContext/index";
import useForm  from "../useForm/index";

const Form = (props, ref) => {
  const { form, children, onFinish, onFinishFailed } = props;  
  const [formInstance] = useForm(form); // 获取当前组件对应的数据存储区域

  // 类组件中使用Form组件,需要手动绑定ref  子->父
  React.useImperativeHandle(
    ref,
    () => formInstance,
  )
  const { setCallBacks, submit } = formInstance
  // console.log("formInstance", formInstance);
  setCallBacks({
    onFinish,
    onFinishFailed
  })
  return  (
    <form onSubmit={e => {
      e.preventDefault();
      submit();
    }}>
      <FieldContext.Provider value={formInstance}>{ children }</FieldContext.Provider>
    </form>
  )
  
}

export default Form;

Field

  1. 获取context上下文中保存的表单实例,也就是上文中的(value={formInstance})。
  2. clone当前Field的子节点,并为该子节点添加统一的处理函数。
  3. 将当前Field实例传入数据存储区域内,以便将来数据改变时,更新当前组件。

import React from 'react';
import FieldContext from "../FieldContext/index";
class Field extends React.Component {
  static contextType = FieldContext; // 获取数据

  componentDidMount() {
    const { setFieldEntities } = this.context
    this.unRegistger = setFieldEntities(this); // 传入当前field实例
  }

  componentWillUnmount() {
    this.unRegistger && this.unRegistger(); // 卸载数据订阅,并移除field实例
  }

  onStoreChange = () => { // 在数据更新时调用 更新当前组件
    this.forceUpdate();
  }

  getControlled = () => {
    const { name } = this.props; // 每个field的唯一key。用于标识当前field到底是谁。
    // console.log("this.context", this.context);
    const { getFieldValue, setFieldsValue } = this.context; // 跨层级传递的数据
    return {
      value: getFieldValue(name), // 获取初始值,或者修改之后的值
      onChange: (e) => {
        const newVal = e.target.value;
        setFieldsValue({ [name]: newVal }); // 设置值到数据存储区域中,然后就能更新视图
      }
    }
  }
  render() {
   
    const { children } = this.props;
    console.log("renderrenderrender", children);
    const returnChildNode = React.cloneElement(children, this.getControlled()); // 克隆一个节点。用于添加props属性
    return (
      returnChildNode
    ) 
  }
}


export default Field;

FieldContext

创建一个上下文对象

import React from 'react';
const FieldContext = React.createContext(); // 创建上下文,用于跨层级数据传递
export default FieldContext;

useForm

useForm是整个表单的核心之处,其中为每个表单创建了独立的ref进行标识,收集对应Form下的所有Field实例,获取值,修改值更新dom,和校验等。

  1. 通过React.useRef创建唯一标识。
  2. 实例化FormStore对象,创建相应逻辑处理
  3. FormStore下提供表单收集的数据,修改函数,校验,提交等,是最核心之处。

import React from "react";
class FormStore { 
  constructor() {
    this.store = {}; // 开辟一个空间存储表单中的状态

    this.fieldEntities = []; // 存储fields组件的更新函数

    this.callBacks = {}; // 存储回调函数,成功失败等、
  }

  setCallBacks = (newCallBacks) => {
    this.callBacks = { ...this.callBacks, ...newCallBacks };
  }

  setFieldEntities = (entity) => { // 存储
    this.fieldEntities.push(entity);
    return () => { // 取消订阅
      this.fieldEntities = this.fieldEntities.filter(v => v !== entity);
      this.store[entity.props.name] = undefined;
    }
  }

  getFieldsValue = () => { // 返回所有存储的值
    return { ...this.store };
  }

  getFieldValue = (name) => { // 返回指定值
    return this.store[name];
  }

  setFieldsValue = (newStore) => { // 设置值,key: value 格式
    this.store = {
      ...this.store,
      ...newStore,
    }

    this.fieldEntities.forEach(v => { // 更新与newStore数据相关联的field
      Object.keys(newStore).forEach(k => {
        if (k === v.props.name) {
          v.onStoreChange()
        }
      })
    }); 
    console.log("newStore", newStore);
  }

  validate = () => { // 校验
    let err = []; // 错误信息
    this.fieldEntities.forEach(field => {
      const { name, rules } = field.props; // 拿到对应组件的校验函数和对应key下面的值进行匹配
      const value = this.getFieldValue(name);
      rules.forEach(rule => {
        if (rule.required && (value === undefined || value === "" || value === null)) { // 这里只做了简单的校验判断,主要体现思路
          err.push({ [name]: value, err: `请填写${name}字段!` });
        } 
      })
    })
    return err;
  }

  submit = () => {
    const { onFinish, onFinishFailed } = this.callBacks;
    let err = this.validate();
    
    if (err.length > 0) { // 错误
      onFinishFailed(err, this.getFieldsValue());
    } else {
      onFinish(null, this.getFieldsValue())
    }
  }

  getForm = () => { // 返回外部需要的事件,而不是返回整个stroe对象,因为有些东西可能是私有的。
    const { getFieldValue, getFieldsValue, setFieldsValue, setFieldEntities, setCallBacks, submit }  = this;
    return {
      getFieldValue,
      getFieldsValue,
      setFieldsValue,
      setFieldEntities,
      setCallBacks,
      submit
    }
  }
}

const useForm = (form) => {
  const formRef = React.useRef(); // 创建唯一值 ref,在组件生命周期中进行使用,组件卸载会自动销毁。
  
  if (!formRef.current) { // 没有值创建,有值,直接返回
    if (form) {
      formRef.current = form; // 如果其他地方传递了form回来 直接赋值
    } else {
      const formStore = new FormStore(); // 实例化存储对象
      formRef.current = formStore.getForm(); // 每个Form 都是一个独立的数据存储区域,防止数据混乱。
    }

  }
  
  return [formRef.current]
}

export default useForm;

BaseForm

将组件代码导出

import React from "react";
import _Form from "./Form";
import Field  from "./Field";
import useForm from "./useForm";

const Form = React.forwardRef(_Form); // ref转发
Form.Field = Field;
Form.useForm = useForm;

export { Field, useForm };
export default Form;

组件结构

image.png

组件使用

类组件

import React, { useEffect } from 'react';
import Form, { Field } from "../../components/BaseForm/index";
class Login extends React.Component{
  formRef = React.createRef();
  
  componentDidMount() {
    this.formRef.current.setFieldsValue({ password: 123 })
  }

  onFinish = (err, values) => {
    console.log("onFinish", err, values);
  }

  onFinishFailed = (err, values) => {
    console.log("onFinishFailed", err, values);
  }
  render() {
    const rules = [{ required: true }]; // 校验规则
    return (
      <div>
        <Form ref={this.formRef} onFinish={this.onFinish} onFinishFailed={this.onFinishFailed}>
          <Field name="account" rules={rules}>
            <input type="text" placeholder='请输入账号'/>
          </Field>
          <Field name="password" rules={rules}>
            <input type="password" placeholder='请输入密码' />
          </Field>
          <button type="submit">提交提交</button>
        </Form>
      </div>
    )
  }
}
export default Login

函数式组件

import React, { useEffect } from 'react';
import Form, { Field } from "../../components/BaseForm/index";
const Login = () => {

    const rules = [{ required: true }]; // 校验规则
    const [form] = Form.useForm();
    console.log("form", form);

    const onFinish = (err, values) => {
      console.log("onFinish", err, values);
    }

    const onFinishFailed = (err, values) => {
      console.log("onFinishFailed", err, values);
    }
    
    useEffect(() => {
      form.setFieldsValue({ account: '1184405532' })
    },[])

    return (
      <div>
        <Form form={form} onFinish={onFinish} onFinishFailed={onFinishFailed}>
          <Field name="account" rules={rules}>
            <input type="text" placeholder='请输入账号'/>
          </Field>
          <Field name="password" rules={rules}>
            <input type="password" placeholder='请输入密码' />
          </Field>
          <button type="submit">提交提交</button>
        </Form>
      </div>
    )
}

结语

以上便实现了一个简历的表单组件,其中核心思想就是额外开辟数据存储区域,进行数据存储和数据修改,通过指定的唯一key(name)进行一对一的定点更新,通过Ref为Form创建唯一的指向,避免有多个Form时数据紊乱。其中的各种技巧及思想很多都可以应用在业务组件中。

github地址