模仿antd4从零到一实现rc-field-form

·  阅读 2517

后台开发中,表单的需求非常的频繁。面对大量的表单需求,antd提供的Form组件提供的功能足够强大,学习成本也非常的低。 那这个Form组件内部帮我们做了什么事情呢?
Form.useForm()如何生成form实例?
onFinish是怎么被执行的?
Form.Itemname是怎么被利用起来的,rules配置的验证规则又是怎么触发的?
请带着以上这些问题,跟我一起对Form组件的核心依赖rc-field-form进行了源码学习与仿造。 我将带着你实现以下功能:

  1. 表单存取值
  2. 表单初始值
  3. 表单验证
  4. 表单提交
  5. 重置表单
  6. 值改变监听

食用方式:跟着文章动手实现属于你自己的表单组件,同时比对rc-field-form源码加深理解。
文章考虑到可读性,没有在代码块中加入类型,可以在项目源码中获取。

rc-field-form使用案例

import Form, { Field } from 'rc-field-form';
 
<Form
  onFinish={values => {
    console.log('Finish:', values);
  }}
>
  <Field name="username">
    <Input placeholder="Username" />
  </Field>
  <Field name="password">
    <Input placeholder="Password" />
  </Field>
 
  <button>Submit</button>
</Form>;
复制代码

项目初始化

做为一款组件,选择使用dumi快速生成项目。

mkdir base-field-form && cd base-field-form

yarn create @umijs/dumi-lib

如果你不了解dumi可以点击了解

生成项目后目录结构如下:

docs是dumi的文档目录,src是开发目录。

组件搭建

首先我们来看一下Form内部流程图:

大家第一次看到这个图一定是一头雾水的,没有关系,我们来逐个讲一下这些模块的作用以及调用关系。
首先搭出组件的结构,在src目录下新建FieldForm目录:

FieldForm
  - index.tsx
  - Form.tsx
  - Field.tsx
  - useForm.js
复制代码

Form.tsx内容如下:

const Form: React.FC<any> = props => {
  const { children } = props;
  return <form>{children}</form>;
};

export default Form;
复制代码

再来看Field.tsx

class Field extends Component<any> {

  getControled = () => {
    const { name } = this.props;
    return {
      value: '', // TODO
      onChange: (e: any) => {
        // TODO
      },
    };
  };
  
  render() {
    const { children } = this.props;
    const returnChildNode = React.cloneElement(
      children as React.ReactElement,
      this.getControled(),
    );
    return returnChildNode;
  }
}

export default Field;
复制代码

接着就是核心的useForm

export default function useForm() {
  const formRef = useRef();
  return [formRef.current];
}

复制代码

在使用antd Form开发的时候,利用useForm生成的Form实例做验证、提交、重置表单等功能,这些功能的基础是它先要有一个内部的仓库来集中管理数据。

数据仓库

class FormStore {
  // 用来保存表单数据
  private store: Store = {};
  
  getFieldValue = (name: string) => this.store[name];

  getFieldsValue = () => this.store;

  setFieldsValue = (newStore: any) => {
    this.store = {
      ...this.store,
      ...newStore,
    };
  };

  getForm = () => {
    return {
      getFieldValue: this.getFieldValue,
      getFieldsValue: this.getFieldsValue,
      setFieldsValue: this.setFieldsValue,
    };
  };
}
复制代码

我们创建FormStore来作为数据仓库,并提供相应的函数来操作、获取数据。下一步让useForm提供FormStore,使其能拥有对应的能力:

const useForm = (form?: FormInstance) => {
  const formRef = useRef<FormInstance>();
  if (!formRef.current) {
    if (form) {
      formRef.current = form;
    } else {
      const formStore = new FormStore();
      formRef.current = formStore.getForm();
    }
  }
  return [formRef.current];
};

export default useForm;
复制代码

存储数据的地方有了,Field.tsx同时对值的改变、获取进行了拦截。那下一步需要解决的问题就是怎么让Field.tsx中值的改变、获取与useForm绑定起来了。

rc-field-form上交的答案是React.createContext

如果你对React.createContext不了解,建议先学习掌握该知识点。

创建文件FieldContext.tsx:

import * as React from 'react';

const warningFunc: any = () => {
  console.warn('Can not find FormContext. Please make sure you wrap Field under Form.');
};

const Context = React.createContext<FormInstance>({
  getFieldValue: warningFunc,
  getFieldsValue: warningFunc,
  setFieldsValue: warningFunc,
});

export default Context;
复制代码

OK,开始改造Form,引入FieldContextuseForm。这里完成的是把useForm提供的实例作为FieldContext的值进行绑定:

import React from 'react';
import FieldContext from './FieldContext';
import useForm from './useForm';

const Form: React.FC<any> = props => {
  const { children, form, ...restProps } = props;
  const [formInstance] = useForm(form);
  return (
    <form {...restProps}>
      <FieldContext.Provider value={formInstance}>
        {children}
      </FieldContext.Provider>
    </form>
  );
};

export default Form;
复制代码

Form.tsx中提供了 Context,在Field.tsx中对它进行消费了:

import React, { Component } from 'react';
import FieldContext from './FieldContext';

class Field extends Component<any> {
  public static contextType = FieldContext;

  getControled = () => {
    const { name } = this.props;
    const { getFieldValue, setFieldsValue } = this.context;
    return {
      value: getFieldValue(name),
      onChange: (...args: EventArgs) => {
        const event = args[0];
        if (event && event.target && name) {
          setFieldsValue({
            [name]: (event.target as HTMLInputElement).value,
          });
        }
      },
    };
  };
  
  render() {
    const { children } = this.props;
    const returnChildNode = React.cloneElement(
      children as React.ReactElement,
      this.getControled(),
    );
    return returnChildNode;
  }
}

export default Field;
复制代码

现在已经完成了Form提供ContextField中消费了Context以完成值的获取和更新。

来查看一下目前的成果:

上面动图中打印的值是在setFieldsValue输出的。很明显存在一个问题,store成功赋值,组件却没有更新。

组件更新

值更新了,组件没有更新,是因为setFieldsValue‌对store的改变,没有去触发组件的重新渲染,所以我们需要在setFieldsValue改变store的同时,重新渲染与之对应的组件。

明白了其中的缘由,再来看rc-field-form的解决步骤:

  1. useForm提供「内部」的注册方法, 注册Field组件到「内部变量」fieldEntities中。
// useForm.tsx新增

// 用于存储Field
private fieldEntities: FieldEntity[] = [];

private registerField = (entity: FieldEntity) => {
  this.fieldEntities.push(entity);

  // un-register field callback
  return () => {
    this.fieldEntities = this.fieldEntities.filter((item) => item !== entity);
  };
};
复制代码
  1. Field初始化的时候,把自己注册功能到fieldEntities中去。同时Field提供组件刷新的功能函数(onStoreChange)
// Field.tsx

import React, { Component } from 'react';
import FieldContext from './FieldContext';

class Field extends Component<any> {
  public static contextType = FieldContext;

  private cancelRegister: any;

  componentDidMount() {
    const { registerField } = this.context;
    this.cancelRegister = registerField(this);
  }

  componentWillUnmount() {
    this.cancelRegister && this.cancelRegister();
  }
  
  public onStoreChange = () => {
    this.forceUpdate();
  };

  getControled = () => {
    const { name } = this.props;
    const { getFieldValue, setFieldsValue } = this.context;
    return {
      value: getFieldValue(name),
      onChange: (...args: EventArgs) => {
        const event = args[0];
        if (event && event.target && name) {
          setFieldsValue({
            [name]: (event.target as HTMLInputElement).value,
          });
        }
      },
    };
  };
  render() {
    const { children } = this.props;
    const returnChildNode = React.cloneElement(
      children as React.ReactElement,
      this.getControled(),
    );
    return returnChildNode;
  }
}

export default Field;
复制代码
  1. setFieldsValue的时候,调用对应FieldonStoreChange完成组件的更新。
// 更新useForm.tsx中setFieldsValue方法

setFieldsValue = (newStore: any) => {
  this.store = {
    ...this.store,
    ...newStore,
  };
  this.getFieldEntities(true).forEach(({ props, onStoreChange }) => {
    const name = props.name as string;
    Object.keys(newStore).forEach((key) => {
      if (name === key) {
        onStoreChange();
      }
    });
  });
};
复制代码

看一下更新后的效果:

表单验证与表单提交

有了数据仓库,实现valdatesubmit方法也水到渠成了。

表单验证

useForm中增加validateFields

private validateFields = () => {
  const promiseList: Promise<{
    name: string;
    errors: string[];
  }>[] = [];

  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>;
};
复制代码

梳理一下validateFields的流程:

  1. 声明异步列表promiseList用来存储需要验证的规则。
  2. 遍历this.fieldEntities以获取每一个Field实例的验证方法,并将其放入promiseList
  3. 初始化验证结果标记hasError、验证结果results、需要验证的规则总数count
  4. 利用summaryPromise返回PromisesummaryPromise内部遍历promiseList,遇到验证失败时把验证结果设置为错误,并且自减count,直到count为0时返回验证结果。

上面看到了FieldvalidateRules方法,在Field.tsx中创建validateRules方法:

public validateRules = () => {
  const { rules, name } = this.props;
  if (!name || !rules || !rules.length) return [];
  const cloneRule = [...rules];
  const { getFieldValue } = this.context;
  const value = getFieldValue(name);

  const promise = validateRules(name, value, cloneRule);

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

  return promise;
};
复制代码

最终验证功能交给了validateRules方法,validateRules其实是用async-validator做了验证,有兴趣的小伙伴可以看一下项目源码下的utils/validateUtil.ts

表单提交

useForm.tsx中新增:

private 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);
      }
    });
};
复制代码

解答一下this.callbackscallbacks顾名思义,存储了在Form中传递的回调函数,例如onFinishonFinishFailed。对应的,useForm拥有一个setCallbacks方法。在初始化Form的时候,把所需的事件存储,在正确的时机触发对应的回调。

// Form.tsx
import React from 'react';
import {
  Store,
  FormInstance,
  Callbacks,
  InternalFormInstance,
} from './interface';
import FieldContext, { HOOK_MARK } from './FieldContext';
import useForm from './useForm';

type BaseFormProps = Omit<
  React.FormHTMLAttributes<HTMLFormElement>,
  'onSubmit'
>;

type RenderProps = (
  values: Store,
  form: FormInstance,
) => JSX.Element | React.ReactNode;

const Form: React.FC<FormProps> = ({
  name,
  initialValues,
  form,
  children,
  onValuesChange,
  onFinish,
  onFinishFailed,
  ...restProps
}: FormProps) => {
  const [formInstance] = useForm(form);
  const { setCallbacks } = formInstance as FormInstance;
  setCallbacks({
    onFinish,
    onFinishFailed,
  });

  const mountRef = React.useRef<boolean>(true);
  if (!mountRef.current) {
    mountRef.current = false;
  }

  const WrapperNode = (
    <FieldContext.Provider value={formInstance}>
      {children}
    </FieldContext.Provider>
  );

  return (
    <form
      {...restProps}
      onSubmit={(event: React.FormEvent<HTMLFormElement>) => {
        event.preventDefault();
        event.stopPropagation();
        formInstance.submit();
      }}
    >
      {WrapperNode}
    </form>
  );
};

export default Form;
复制代码

目前的成果演示:

初始化值

// useForm
private initialValues: Store = {};

private setInitialValues = (initialValues: Store, init: boolean) => {
  if (init) return; // 初始化组件的时候,才能有效设置初始值
  this.initialValues = initialValues || {};
  this.setFieldsValue(initialValues);
};

// 更新registerField方法,增加设置
private registerField = (entity: FieldEntity) => {
  this.fieldEntities.push(entity);

  const { name, initialValue } = entity.props;
  // 设置Field的初始值
  if (initialValue !== undefined && name) {
    this.initialValues = {
      ...this.initialValues,
      [name]: initialValue,
    };
    this.setFieldsValue({
      ...this.store,
      [name]: initialValue,
    });
  }
  // un-register field callback
  return () => {
    this.fieldEntities = this.fieldEntities.filter(item => item !== entity);
    // this.store = setValue(this.store, namePath, undefined); // 删除移除字段的值
  };
};
复制代码

以上useForm的更新使其提供了设置表单initialValues的函数,并且完成了FieldinitialValues的功能。在Form.tsx中来使用一下setInitialValues

// Form.tsx新增
const { setCallbacks, setInitialValues } = formInstance as FormInstance;
const mountRef = React.useRef<boolean>(true);
setInitialValues(initialValues || {}, !mountRef.current);
if (!mountRef.current) {
  mountRef.current = false;
}
复制代码

重置表单

useForm.tsx新增方法:

private resetFields = (nameList?: string[]) => {
  if (!nameList) {
    this.store = { ...this.initialValues };
    this.setFieldsValue(this.store, true);
  }
};

// 更新setFieldsValue方法
// 增加reset参数,由重置方法触发时,触发所有组件的重新渲染
setFieldsValue = (newStore: any, reset?: boolean) => {
  this.store = {
    ...this.store,
    ...newStore,
  };
  this.getFieldEntities(true).forEach(({ props, onStoreChange }) => {
    const name = props.name as string;
    Object.keys(newStore).forEach(key => {
      if (name === key || reset) {
        onStoreChange();
      }
    });
  });
};
复制代码

值改变监听

对改变值的监听,在setFieldsValue的时候可以比对获取改变的值和改变后的store

// useForm.tsx
private setFieldsValue = (values: any, reset?: boolean) => {
  const nextStore = {
    ...this.store,
    ...values,
  };
  this.store = nextStore;
  this.getFieldEntities(true).forEach(({ props, onStoreChange }) => {
    const name = props.name as string;
    Object.keys(values).forEach((key) => {
      if (name === key || reset) {
        onStoreChange();
      }
    });
  });

  // 从callbacks中获取到onValuesChange
  const { onValuesChange } = this.callbacks;

  if (onValuesChange) {
    onValuesChange(values, nextStore);
  }
};

// 记得要在Form.tsx中获取onValuesChange,并放入callbacks中
// Form.tsx
setCallbacks({
  onFinish,
  onFinishFailed,
  onValuesChange,
});
复制代码

乞丐版表单到这里就完成了,来验收一下成果:

最后

本文参照rc-field-form源码实现了一个简易版本的表单组件。阅读完本文之后,对于文章开头提出的4个问题,相信在你心里已经有了确定的答案,对rc-field-form的核心原理也有一个清晰的思路。也感谢你能花时间阅读本文。

源码地址
rc-field-form

“难”绝对是生命中幸福的开始,“容易”绝对不是该庆幸的事。

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改