从Antd 源码到自我实现之 Form表单

10 篇文章 3 订阅
4 篇文章 0 订阅

前言

Antd 中的组件大部分基于蚂蚁金服的组件库 react-component。antd 与 react-component 都是开源项目,阅读其源码可以给我们带来很多收益,比如:

  • 了解各式各样的组件背后的实现思想
  • 怎样去实现一个对开发和用户都友好的组件,即简单易用,便于扩展。
  • 学习一些我们在写业务代码时不太会用到的 React 高级用法

但是阅读过 Antd 源码就会发现,代码量巨大而且庞杂,不易抓住重点,很容易导致从入门到放弃。我个人的看法是阅读框架源码的目的是掌握其核心实现思想,整体的运行流程,对我们自己面临类似需求时有所帮助与借鉴。没有必要对代码一行一行的死抠,代码的精度应介于粗粒度与细粒度之间。

我的阅读方法分为三步:

  1. 阅读 Antd 组件的官方文档,整理出组件的核心方法与属性
  2. 找出核心方法与属性在源码中的位置,作为阅读起点
  3. 了解到组件的实现思想后,自己搞一个极简版,在手撸的过程中加深体会。

接下来的过程中我不会把 antd 中的源码拎出来解读,我会介绍极简版组件的实现思路,争取用最少的代码展现 antd 组件背后的思想。读者可以将极简版与 antd 的源码做一个对照来加深理解。

Antd Form 简述

Form 表单具有数据收集、校验和提交功能的表单,包含复选框、单选框、输入框、下拉选择框等元素。

表单一定会包含表单域,表单域可以是输入控件,标准表单域,标签,下拉菜单,文本域等。Antd 中表单域形式为 <Form.Item />

<Form.Item {...props}>
  {children}
</Form.Item>

核心功能点提取

核心功能点作为组件能正常使用的最小集,对 Form 表单组件而言,应该能够完成数据收集,校验,提交这三个核心功能。下面这张图是我归集的核心功能点:

在这里插入图片描述

核心实现

数据收集 与 校验

Form.create()

Form.create() 用来包装业务场景中会用到 Form 的组件。通过 Form.create() 包装后,在业务组件中可以调用 this.props.form 来做表单的验证 (validateFIelds),表单字段值的设置 (setFieldValue),表单字段值的获取 (getFieldValue) 等。

class FormDemo extends React.Component {
	render() {
		return <Form>
			<Form.Item {...props}>
				this.props.form.getFieldDecorator([name], {
					initialValue: '',
					rules: [...],
					getValueFromEvent: callback
				})(
					<Input />
				);
			</Form.Item>
		</Form>
	}
}

export default Form.create()(FormDemo);

在实现层面上,Form.create() 会返回一个高阶函数 (HOC),此高阶函数通过属性代理(Props Proxy),将 props form 赋给作为参数传入的业务组件 WrappedComponent。通过 form 对象中的一系列方法,WrappedComponent 在原业务组件的基础上被赋予了更强大的能力(主要是指Form表单中输入控件的数据同步将被 Form 接管)。这种能力就体现在我们经常使用的 this.props.form[method]

function create() {
    return function decorate(WrappedComponent) {
        return class Decorator extends React.Component {

            static displayName = `HOC(${getDisplayName(WrappedComponent)})`;

            private form: WrappedFormUtils = {
                // 表单进行双向绑定
                getFieldDecorator: (name: string, props: FieldDecratorProps) =>       
                {
             	    ....
                },
				// 获取输入控件的值
                getFieldsValue: (fieldNames) => {
					...
                },
				// 设置输入控件的值
                setFieldValue: (name, value) => {
					...
                },
            }
             ....
            render() {
                return <WrappedComponent form={this.form}></WrappedComponent>;
            }
        };
    };
}

export default create;
this.props.form[method]
getFieldDecorator(id, options)

getFieldDecorator 的关键字是接管

经过 getFieldDecorator 包装的控件,表单控件会自动添加 value(或 valuePropName 指定的其他属性)onChange(或 trigger 指定的其他属性),数据同步将被 Form 接管,这会导致以下结果:

  1. 你不再需要也不应该用 onChange 来做同步,但还是可以继续监听 onChange 等事件。
  2. 你不能用控件的 value defaultValue 等属性来设置表单域的值,默认值可以用 getFieldDecorator 里的initialValue。
  3. 你不应该用 setState,可以使用 this.props.form.setFieldsValue 来动态改变表单值。

自实现代码如下:

/**
    * 经过 getFieldDecorator 包装的控件, 数据同步将被 Form 接管包括设置控件值,校验参数等
    *
    * @param {string} name 控件字段名
    *  @param {FieldDecratorProps} props 参数
    * @return {function} 包裹控件的高阶函数组件
*/
getFieldDecorator: (name: string, props: FieldDecratorProps) => {
    const {rules, initialValue, getValueFromEvent, trigger, ...others} = props;
    return FormItemComponent => {
        const error = this.getFieldError(name);
        return <div>
            {
                React.cloneElement(FormItemComponent, {
                    warning: error ? true : false,
                    name,
                    onChange: (e: any) => {
                        let value = e;
                        if (typeof props.getValueFromEvent === 'function') {
                            value = props.getValueFromEvent(e);
                        } else if (e.target) {
                            value = e.target.value;
                        }

                        this.form.setFieldValue(name, value).then(() => {
                            if (!trigger || trigger === 'onChange') {
                                this.validateField(name);
                            }
                        });
                    },
                    onBlur: (e: any) => {
                        if (trigger === 'onBlur') {
                            this.validateField(name);
                        }
                    },
                    rules,
                    value: this.state[name] ? this.state[name].value : initialValue,
                    ...others
                })
            }
            {
                error && <div className="miyun-form-item-error">{error}</div>
            }
        </div>;
    };
}

从代码可以看出,实现有如下几个要点:

  • 通过 React.cloneElement,为即将被监管的输入控件 (Input, Select etc…):
    1. 添加 onChange, onBlur 用来设置字段值,校验等,从而实现所谓 ‘接管’。(通过 trigger 来决定校验在 change 或 blur 时触发 )
    2. 输入控件初始值设置为 initialValue
  • 为校验结果添加错误展示信息。 (被包裹组件无感知,由 HOC 处理)
getFieldValue(name) 与 getFieldsValue(fieldNames)

在 Form.create() 返回的的高阶组件层面维护一个动态生成的维护输入控件信息的 state,包括控件 name, value, initialValue, rules 等信息。

state 结构如下:

  [name] : {
      rules,
      value: initialValue,
      initialValue: this.deepCopyValue(initialValue),
      error: ''
  }

方法实现如下:

 getFieldValue: (name) => {
     return this.state[name].value;
 }

 getFieldsValue: (fieldNames) => {
     const values = {};
     // 不传参数返回所有values
     if (!fieldNames) {
        this.fieldNames.forEach(name => {
            values[name] = this.state[name].value;
        });
     } else if (fieldNames instanceof Array) {
         fieldNames.forEach(name => {
             values[name] = this.state[name].value;
         });
     }
     return values;
 }
setFieldValue(name, value) 与 resetFields(names)
// 设置一组输入控件的值
setFieldValue: (name, value) => {
    return new Promise( (resolve, reject) => {
        this.setState({
            [name]: {
                ...this.state[name],
                value
            }
        }, resolve);
    });
}

// 重置一组输入控件的值(为 initialValue)与状态,如不传入参数,则重置所有组件
resetFields: names => {
    let fieldNames = (names instanceof Array && names.length > 0) ? names : this.fieldNames;
    fieldNames.forEach(name => {
        let currentState = this.state[name];
        this.setState({
            [name] : {
                ...this.state[name],
                value: this.deepCopyValue(currentState.initialValue),
                error: ''
            }
        })
    });
}
validateFields 校验字段
/**
 * 校验表单。如果不传names,则校验所有字段
  *
  * @param {string[] | function} names 表单字段names
  * @param {function} callback 校验结束回调
  */
 validateFields: (names, callback) => {
     let fieldNames = null;
     let cb = null;
     let values = null;
     if (typeof names === 'function') {
         fieldNames = this.fieldNames;
         cb = names;
         values = this.form.getFieldsValue();
     } else {
         fieldNames = names ? names : this.fieldNames;
         cb = callback;
         values = this.form.getFieldsValue(fieldNames);
     }
     let isError = false;
     fieldNames.forEach(name => {
         if (this.validateField(name)) {
             isError = true;
         }
     });
     cb && cb(isError, values);
 }

注: 校验回调函数 callback 中参数 values 的数据结构如下:

在这里插入图片描述

Context 中 registerField 和 unregisterField

在 Form.Item componentDidMount 的时候,在 Form.create() 高阶组件 Decorator 中动态注册对应的 field。因此需要在 Decorator 中声明 context 用以传递关于注册/注销的方法。

function create() {
    return function decorate(WrappedComponent) {
    	private form: WrappedFormUtils = {...}
    	
	    static childContextTypes = {
	        registerField: PropTypes.func,
	        unregisterField: PropTypes.func
	    };
		
	    getChildContext() {
           return {
               registerField: this.registerField,
               unregisterField: this.unregisterField
           };
       }
       // 注册表单控件
       registerField = (name, rules, initialValue) => {
           if (this.state[name]) {
               return;
           }
           this.setState({
               [name] : {
                   rules,
                   value: initialValue,
                   initialValue: this.deepCopyValue(initialValue),
                   error: ''
               }
           });
           this.fieldNames.push(name);
       }
       // 注销表单控件
       unregisterField = name => {
           this.setState({
               [name]: null
           }, () => {
               delete this.state[name];
               this.fieldNames.splice(this.fieldNames.indexOf(name), 1);
           });
       }
	}
}

Form.Item 中的相关实现代码:

export default class FormItem extends React.Component<IFormItemProps, any> {
    static contextTypes = {
        labelCol: PropTypes.object,
        layout: PropTypes.string,
        registerField: PropTypes.func,
        unregisterField: PropTypes.func,
        wrapperCol: PropTypes.object
    };

    componentWillUnmount() {
        const childrenArray = React.Children.toArray(this.props.children);
        const context = this.context;
        childrenArray.forEach((input: React.ReactElement) => {
            // TODO: 更优雅地获取 Form field
            const props = input.props.children[0].props;
            if (props) {
                context.unregisterField(props.name);
            }
        });
    }

    componentDidMount() {
        const childrenArray = React.Children.toArray(this.props.children);
        const registerField = this.context.registerField;
        childrenArray.forEach((input: React.ReactElement) => {
            // TODO: 更优雅地获取 Form field
            const props = input.props.children[0].props;
            if (props) {
                registerField(props.name, props.rules, props.value);
            }
        });
    }
    
	render() {
	    ...
	}
}

字段布局

Form 表单的字段布局主要依赖于 Grid (24栅格)。主要通过 Form.ItemlabelColwrapperCol

label 标签布局,同 组件,设置 span offset 值,如 {span: 3, offset: 12} 或 sm: {span: 3, offset: 12}

我们通过 Form.Item 的 render 方法实现就明白了:用户设置的 labelCol 和 wrapperCol 要符合 Gird 栅格系统的使用规范,Form.Item 作为一个高阶函数将这两个属性传递给 Grid.

另外,标签文本 label 和 标签 colon 也是在 Form.Item上设置的。

export default class FormItem extends React.Component<IFormItemProps, any> {
	...
	render() {
		const {
            classPrefix,
            className,
            label,
            labelAlign,
            children,
            colon,
            required,
            ...restProps
        } = this.props;
	
		return <Row
	            {...restProps}
	            className={...}
	        >
	
	            <Col
	                {...labelCol}
	                style={{
	                    textAlign: labelAlign
	                }}
	                className={...}
	            >
	                <label>
	                   { label && [label, colon] }
	                </label>
	            </Col>
	
	            <Col
	                {...wrapperCol}
	                className={...}
	            >
	                {children}
	             </Col>
	        </Row>;
	}
}

接下来我会继续发布我实现的另外一些组件如 Gird,Input,Menu, Select, Modal 等的实现思路,希望对您有所启发 ?

更多组件

更多组件自实现系列,更多文章请参考:

从Antd 源码到自我实现之 Grid 栅格系统

React 实现 Modal 思路简述

从Antd 源码到自我实现之 Menu 导航菜单

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值