写点什么

Form 表单在数栈的应用 (下):深入篇

作者:数栈DTinsight
  • 2022 年 1 月 19 日
  • 本文字数:3660 字

    阅读完需:约 12 分钟

这篇文章的主题为我们对 Form 表单在数栈产品中使用之后理解消化的一个过程,通过介绍一些 Form 表单中常用到的方法,来理解部分设计思想,加深我们对技术的追求。主要介绍 Form 表单的创建和 Form 表单双向绑定(getFieldDecorator)。


后文中所提到的 Form 表单均为 Antd 3.x 中的 Form 组件,以下简称为 Form 表单。在 Form 表单在数栈的应用(上): 校验篇 中提到,我们生在一个最好的时代,其实是别人造好轮子帮我们做了一些事情,那我们今天看一看,别人的轮子是怎么造的,我们自己能不能实现。留心过 Antd 的同学可能有印象,Antd 是基于 react-component 组件进行了 UI 封装,文章会以 react-component/form 的代码为主。

一、别人的 Form

1.1 From.create

先查看 createForm.js 文件,该文件主要是对 createBaseForm.js 文件进行了一层封装,并加上一些常用的方法。


import createBaseForm from './createBaseForm';export const mixin = {  getForm() {    return {      getFieldsValue: this.fieldsStore.getFieldsValue,      getFieldValue: this.fieldsStore.getFieldValue,      ...      validateFields: this.validateFields,      resetFields: this.resetFields,    };  },};function createForm(options) {  return createBaseForm(options, [mixin]);}export default createForm;
复制代码


接下来查看一下 createBaseForm.js 文件,主要查看该文件中的 createBaseForm 方法,这个方法起到装饰器的作用,在 props 中包装了一个默认为 form 的变量,在这个变量中完成 Form 的所有功能。createBaseForm 的作用是拷贝当前传递来的组件,也就是调用函数将当前组件传递下去作为被包装组件,最终返回一个被包装过的具备新属性的组件。


render() {  const { wrappedComponentRef, ...restProps } = this.props; // eslint-disable-line  const formProps = {    // getForm 方法来自 createForm.js,在 props 中包装一个 formPropName 变量,默认为 form    [formPropName]: this.getForm(),  };  // 获取 form 的实例  if (withRef) {    formProps.ref = 'wrappedComponent';  } else if (wrappedComponentRef) {    formProps.ref = wrappedComponentRef;  }  const props = mapProps.call(this, {    ...formProps,    ...restProps,  });  return <WrappedComponent {...props} />;}
复制代码


装饰器(decorator): 是一种与 相关的语法,主要用来修改类和类方法(类属性),大部分面向对象的编程语言都支持这种语法,比如 Java、Python。装饰器可以简单理解为:能对一些 对象 进行修改,然后返回一个被包装过的 对象


综合来看,Form.create(options) 实际上是对我们的业务组件进行了一次封装,进行了 Form 相关属性的初始化,挂载了一些需要使用的方法,并将这些方法添加到 props.form 下。

1.2 getFieldDecorator

<FormItem {...formItemLayout} label="姓 名" >  {getFieldDecorator('name', {    initialValue: userInfo.name,    rules: [      { required: true, message: '姓名不可为空!' }    ]  })(    <Input placeholder="请输入姓名" />  )}</FormItem>
复制代码


从上述使用代码和下方实现方法可以看出,getFieldDecorator 是一个柯里化的函数,通过 id 和参数的输入,返回以输入组件为入参加上新属性的一个 Dom 节点,把 option 的 valuePropName、getValueProps、initialValue、rules 等各种 props 挂载到输入组件上。


getFieldDecorator(name, fieldOption) {  const props = this.getFieldProps(name, fieldOption);  return fieldElem => {    // We should put field in record if it is rendered    this.renderFields[name] = true;    const fieldMeta = this.fieldsStore.getFieldMeta(name);    const originalProps = fieldElem.props;    fieldMeta.originalProps = originalProps;    fieldMeta.ref = fieldElem.ref;    const decoratedFieldElem = React.cloneElement(fieldElem, {      ...props,      // 没有 initialValue 时为 undefined,有则是 initialValue 的值      ...this.fieldsStore.getFieldValuePropValue(fieldMeta),    });    return supportRef(fieldElem) ? (      decoratedFieldElem    ) : (      <FieldElemWrapper name={name} form={this}>        {decoratedFieldElem}      </FieldElemWrapper>    );  };}
复制代码


getFieldDecorator 有以下两个作用,可在 createBaseForm.js 文件的 getFieldPropsgetFieldValuePropValue 方法中分别验证:


  • 在初始化数据字段时将数据字段放到 fieldsStore 中;

  • 挂载 props 到输入组件上时会从 fieldsStore中读取数据字段。

1.3 validateFields

通常使用 validateFields 方法对我们的表单数据进行校验,查看 createBaseForm.js 文件中 validateFields 方法的实现后,发现 validateFields 方法返回一个 Promise 并且拼装 validateFieldsInternal 方法需要的参数。


validateFields(ns, opt, cb) {  const pending = new Promise((resolve, reject) => {    ...    this.validateFieldsInternal(..., params, callback);  });  ...  return pending;}
复制代码


再看 validateFieldsInternal 方法的代码,它会从 fieldsStore 中获取 rules 和数据 fields 的值,校验后将错误信息分别存储到对应的 fieldsStore 中。


import AsyncValidator from 'async-validator';validateFieldsInternal(  fields,  { fieldNames, action, options = {} },  callback,) {  const fieldMeta = this.fieldsStore.getFieldMeta(name);  ...  const validator = new AsyncValidator(allRules);  validator.validate(allValues, options, errors => {    if (errors && errors.length) {      errors.forEach(e => {        ...        const fieldErrors = get(errorsGroup, fieldName.concat('.errors'));        fieldErrors.push(e);      });    }  });  ...  this.setFields(nowAllFields);  ...}
复制代码


总得来说,Form 表单从初始化到表单收集校验经过了以下几个步骤:1、通过 Form.create 方法初始了一些属性到 props.form 中,供开发者调用;2、通过 getFieldDecorator 初始化表单的属性和值,达到双向绑定的效果;3、校验通过,把数据存到 fieldsStore 中;校验不通过,把 error 存到 fieldsStore 中,渲染。

二、自己的 Form

效果和代码可以在 https://stackblitz.com/edit/react-ts-uoj5pj 查看。


2.1 getFieldDecorator

/** * 实现 getFieldDecorator 方法 * 初始化时将 initialValue 赋值给输入框的 value * 输入框变化时可以拿到 value */const getFieldDecorator = (key: string, options: any) => {  // 判断是否第一次赋值,避免死循环  const first = Object.keys(formData).indexOf(key) === -1;  if (options.rules) {    rules[key] = [...options.rules];  }  if (first && options.initialValue) {    setFormData({ ...formData, [key]: options.initialValue });  }  return (formItem) => {    if (errObj[key]) {      formItem = {        ...formItem,        props: { ...formItem.props, className: 'input err' },      };    }    return (      <div className="form-item">        {React.cloneElement(formItem, {          name: key,          value: formData[key] || '',          onChange: (e: any) => {            // 输入框值变化时去除错误提示            setErrObj({ ...errObj, [key]: '' });            setFormData({ ...formData, [key]: e.target.value });          },          onBlur: () => {            // 当前默认 blur 时进行校验            validateFields();          },        })}        <div className="err-text">{errObj[key] || ' '}</div>      </div>    );  };};
复制代码

2.2 validateFields

// 绑定校验方法const validateFields = (cb?: any) => {  let errObjTemp = {};  Object.keys(rules).forEach((key) => {    rules[key].forEach((rule) => {      if (rule?.required && (!formData[key] || formData[key].trim() === '')) {        errObjTemp = {          ...errObjTemp,          [key]: rule?.message || `${key}为必填项!`,        };        setErrObj(errObjTemp);      }    });  });  cb && cb(Object.keys(errObjTemp).length ? errObjTemp : undefined, formData);};
复制代码

2.3 createForm

const createForm = (FormFunc) => (props) => {  const [formData, setFormData] = useState({});  const [errObj, setErrObj] = useState({});  const rules = {};    ...    // 将自定义方法挂载到 props 上  return FormFunc({ ...props, getFieldDecorator, validateFields });};
复制代码


发布于: 刚刚阅读数: 2
用户头像

还未添加个人签名 2021.05.06 加入

还未添加个人简介

评论

发布
暂无评论
Form 表单在数栈的应用(下):深入篇