实现一个最简antd-form

239 阅读6分钟

源码地址:github.com/gezhicui/mi…

实现一个 antd-form

在前端学习的入门阶段,使用组件库的过程中,我总是觉得能写出这种东西的人真的太厉害了,但是随着深入学习,我发现之所以觉得厉害其实是因为你不知道他是怎么实现的,人总是对未知的东西感到神秘,当了解了他的实现方式时,你就会茅塞顿开

antd-form是基于rc-field-form实现的,所以本文从 0 实现一个最简单的rc-field-form,是为了了解核心原理

antd-form表单的开发有三个最重要的组成部分,分别是

  • useForm
  • Form
  • Form.Item

其中,Form.Item是由rc-field-form中的Field封装而来,所以下文中我提到的Field其实就是Form.Item

useForm

先来写一个表单的最简使用:

const [form] = useForm();

return (
  <Form
    form={form}
    onFinish={(values: any) => {
      console.log('onFinish执行,值为:', values);
    }}
  >
    <Field name="nickname">
      <Input placeholder="请输入昵称" />
    </Field>
    <Field name="doing">
      <Input placeholder="请输入在做的事" />
    </Field>
  </Form>
);

我们可以看到,表单在使用之前通过useForm拿到了form这个东西,form被传入到Form组件中,那这个form到底是个啥?

我们把这个form打印出来:

发现他是一个挂载了许多方法的对象实例,useForm的实现如下:

const useForm = (form) => {
  // 创建一个ref保存表单
  const formRef = useRef();
  // 防止表单重复创建
  if (!formRef.current) {
    // 如果有传参,ref为传进来的表单,否则创建表单实例对象
    if (form) {
      formRef.current = form;
    } else {
      const formStore = new FormStore();
      formRef.current = formStore.getForm();
    }
  }
  // 返回数组 方便扩展
  return [formRef.current];
};

useForm的实现中,其实暴露出去的form就是FormStore这个类的实例上的getForm方法,getForm方法获取了类的所有可访问属性,这个FormStore是表单的核心所在,是保存表单所有状态和处理表单操作的中心。他的基本结构是这样的:

class FormStore {
  // 用来保存表单数据
  private store: Store = {};

  //...各种方法,主要是对表单数据的操作

  getForm = () => {
    return {
     // 暴露出去供外界使用的各种方法
    };
  };
}

总结一下useForm中的操作就是 new 了一个FormStore对象,获取到了FormStore对象实例中所有能供外界访问的数据和方法。

Form

Form就是表单组件了,form实例被传到Form中,进行了以下操作:

// 由于可能没有传入form,所以这里useForm执行一下
const [formInstance] = useForm(form);
// 从form实例中拿到设置用户自定义方法和设置初始值的方法
const { setCallbacks, setInitialValues } = formInstance;

setCallbacks, setInitialValues这两个方法在FormStore中实现如下:

class FormStore {
// 使用callbacks保存用户自定义方法
+  private callbacks = {};
// 使用initialValues保存初始值,后面做表单重置会用到
+  private initialValues = {};


+  setCallbacks = (callbacks) => {
+    this.callbacks = callbacks;
+  };

+  setInitialValues = (initialValues) => {
+    this.initialValues = initialValues || {};
+    this.setFieldsValue(initialValues);
+  };

  getForm = () => {
    return {
+     setCallbacks,
+     setInitialValues
    };
  };
}

拿到了setCallbackssetInitialValues,就要使用这两个函数了

// 获取到传入Form的表单事件处理方法
const { onFinish, onFinishFailed, onValuesChange, initialValues } = props;

// setCallbacks保存用户自定义回调函数
setCallbacks({
  onFinish,
  onFinishFailed,
  onValuesChange,
});

// 设置表单初始值
setInitialValues(initialValues || {});

这时候 就在Form实例中初始化了数据和用户自定义方法,接下来返回一个组件,通过contextform实例透传下去,这样Field组件中也可以获取form实例

const FieldContext = React.createContext();

return (
  <form
    {...restProps}
    onSubmit={(event) => {
      event.preventDefault();
      event.stopPropagation();
      formInstance.submit();
    }}
  >
    {/* 把form对象实例透传下去 */}
    <FieldContext.Provider value={formInstance}>
      {children}
    </FieldContext.Provider>
  </form>
);

这样,最简单的Form组件就完成了

Field

FieldForm的子组件,在antd中是Form.Item,它的基本使用如下:

<Form form={form}>
  <Field
    name="nickname"
    rules={[
      {
        required: true,
        message: '昵称必填',
      },
    ]}
  >
    <Input placeholder="请输入昵称" />
  </Field>
</Form>

基本使用一看就会,和 antd 的 form.item 一模一样,直接来看Field的实现

组件加载的时候,做了以下三件事:

  • 组件获取了context的内容(即 form 实例),同时从 form 实例上拿到了registerField方法,在componentDidMount执行了该方法,并把当前组件传给registerField当做实参,返回的是组件卸载的处理方法,在组件卸载时调用该方法来删除form实例中该控件绑定的状态
  • 定义了一个onStoreChange方法,可以在控件值发生改变的时候使用,来刷新数据
  • 定义了一个validateRules方法,用来做该控件的字段校验
componentDidMount() {
  const { registerField } = this.context;
  this.cancelRegister = registerField(this);
}

componentWillUnmount() {
  this.cancelRegister && this.cancelRegister();
}

onStoreChange = () => {
  //值改变调用react的forceUpdate重新render,因为数据不是响应式的
  this.forceUpdate();
};

/*
  该方法提供给FormStore消费,当前组件先被存入fieldEntities这个保存所有Field的数组中,
  等表单校验时,循环数组拿出组件,执行组件的validateRules进行校验
*/
validateRules = () => {
    const { rules, name } = this.props;
    if (!name || !rules || !rules.length) return [];
    const cloneRule = [...rules];
    const { getFieldValue } = this.context;
    const value = getFieldValue(name);

    // validateRules是表单的校验方法,具体看源码
    const promise = validateRules(name, value, cloneRule);

    promise
      .catch(e => e)
      .then(() => {
        if (this.validatePromise === promise) {
          this.validatePromise = null;
          this.onStoreChange();
        }
      });

    return promise;
  };

registerField方法主要是初始化处理当前Field组件的内容,并把当前组件存进fieldEntities数组中,方法的实现如下:

class FormStore {
+  private fieldEntities = [];

+  registerField = (entity) => {
     // 用fieldEntities保存页面上的所有Field组件
     this.fieldEntities.push(entity);
     const { name, initialValue } = entity.props;
     // 初始化Field传入的initialValue
     if (initialValue !== undefined && name) {
       this.initialValues = {
         ...this.initialValues,
         [name]: initialValue,
       };
       // store中添加控件的初始化值
       this.setFieldsValue({
         ...this.store,
         [name]: initialValue,
       });
     }
     // 返回一个函数,当组件卸载时调用该函数,移除改组件的所有状态
     return () => {
       this.fieldEntities = this.fieldEntities.filter(item => item !== entity);
       // this.store = setValue(this.store, namePath, undefined); // 删除移除字段的值
     };
   };

  getForm = () => {
    return {
+     registerField,
    };
  };
}

Field 返回的 jsx 是这样的:

render() {
  const { children } = this.props;
  // 为form.item附加上form中的属性
  const returnChildNode = React.cloneElement(
    children,
    this.getControled(),
  );
  return returnChildNode;
}

可以看到,Field主要干的事情就是把他的子组件,即真正的输入控件使用React.cloneElement()拿来拓展

React.cloneElement接收三个参数

  • type (ReactElement)
  • props (object)
  • children (ReactElement)

他的作用是克隆并返回一个新的 ReactElement (内部子元素也会跟着克隆),新返回的元素会保留有旧元素的 props、ref、key,也会集成新的 props(只要在第二个参数中有定义),第三个参数为添加的新的子元素

从上面的代码里可以看到,子组件扩展了getControled方法里返回的东西,getControled方法的实现如下:

getControled = () => {
  // 指定this.context 等于FieldContext.Provider传递过来的form实例对象
  const contextType = FieldContext;

  // 拿到 Field的name,就是表单的label
  const { name } = this.props;
  // this.context
  const { getFieldValue, setFieldsValue } = this.context;
  return {
    value: getFieldValue(name),
    onChange: (...args) => {
      const event = args[0];
      if (event && event.target && name) {
        setFieldsValue({
          [name]: event.target.value,
        });
      }
    },
  };
};

从代码可以看出,Field为空间扩展了valueonChange,其中,value是通过getFieldValue拿到form实例对应的值来进行绑定,onChange触发了setFieldsValue方法来对表单数据进行修改。这两个方法也要在 FormStore对象中定义一下

class FormStore {

+  private initialValues = {};

+  getFieldValue = (name) => this.store[name];

+  setFieldsValue = (values, reset) => {
     const nextStore = {
       ...this.store,
       ...values,
     };
     this.store = nextStore;
     this.fieldEntities.forEach(({ props, onStoreChange }) => {
       const name = props.name;
       Object.keys(values).forEach(key => {
         if (name === key || reset) {
           onStoreChange();
         }
       });
     });
     const { onValuesChange } = this.callbacks;
     if (onValuesChange) {
       onValuesChange(values, nextStore);
     }
   };

  getForm = () => {
    return {
+     getFieldValue,
+     setFieldsValue
    };
  };
}

到此为止,FormFielduseForm的核心就完成了,表单可以正常使用,补上一个表单提交的逻辑就行了

表单提交

form实例提供了一个submit方法,调用form.submit()就能够实现对表单的提交,submit方法中从fieldEntities中拿到了所有表单控件,调用了表单控件组件自身上的校验方法

class FormStore {

+ validateFields = () => {
    const promiseList: Promise<{
      name: string;
      errors: string[];
    }>[] = [];
    // 获取到所有在field初始化时保存进来的组件
    this.getFieldEntities(true).forEach(entity => {
      const promise = entity.validateRules();
      const { name } = entity.props;
      promiseList.push(
        promise
          .then(() => ({ name, errors: [] }))
          .catch((errors: any) =>
            Promise.reject({
              name,
              errors,
            }),
          ),
      );
    });

    let hasError = false;
    let count = promiseList.length;
    const results: FieldError[] = [];

    const summaryPromise = new Promise((resolve, reject) => {
      promiseList.forEach((promise, index) => {
        promise
          .catch(e => {
            hasError = true;
            return e;
          })
          .then(result => {
            count -= 1;
            results[index] = result;
            if (count > 0) {
              return;
            }
            if (hasError) {
              reject(results);
            }
            resolve(this.getFieldsValue());
          });
      });
    });

    return summaryPromise as Promise<Store>;
  };

+ submit = async () => {
    this.validateFields()
      .then(values => {
        const { onFinish } = this.callbacks;
        if (onFinish) {
          try {
            onFinish(values);
          } catch (err) {
            console.error(err);
          }
        }
      })
      .catch(e => {
        const { onFinishFailed } = this.callbacks;
        if (onFinishFailed) {
          onFinishFailed(e);
        }
      });
  };
  getForm = () => {
    return {
+    submit,
    };
  };
}

这样 一个简单可用的Form组件就封装完了,建议配合源码食用