10分钟精通Ant Design Form表单

12,522 阅读7分钟
  1. 被人诟病的Form
  2. Form的原理
  3. Vue版Form的进化史

本文适合React、Vue开发者阅读,10分钟不够?那就再加10分钟。

被人诟病的Form

antd被人吐槽最多的除了彩蛋之外,那应该就是Form表单了。如果需要使用Form自带的收集校验功能,需要使用Form.create()包装组件,每一个需要收集的值还需要getFieldDecorator进行注册。官方文档大量的让人眼花缭乱的API,大概率没有多少人读完了整个文档,即便读完了,大概率也记不住。

写这篇文章不是为了吐槽Form表单,当然我也并没有更好的优化Form表单的方案,本文的目的是希望大家能够通过本文了解Form表单的本质,更好的使用的Form表单。

Form的原理

网络上有一些源码分析的文章,个人觉得收益比不高,逐条过api式的讲解甚是无趣。 一句废话概括原理:Form.create创建一个具有注册、收集、校验功能的"实例"。

我们把这句话分成几个关键词逐一分析:Form.create创建实例、注册、收集、校验 四个关键词

Form.create创建"实例":

实例?为什么不是组件。Form.create的核心能力是创建实例this.props.form,并不是创建组件。 这个实例提供一系列的方法,如注册、收集、校验

那为什么要包装组件呢?包装组件的目的是为了更新组件,仅此而已。

你应该知道所有需要该实例帮助你进行收集校验的组件,必须要通过getFieldDecorator进行修饰,一旦经过getFieldDecorator的修饰,那么该组件的值将完全由该实例管理。组件的更新需要组件所在上下文处执行render, 我们知道组件的更新有两种方式:1. 父组件更新了 2. 自身状态改变了

所以进一步讲,包装组件的目的就是为了被包装组件的父组件更新,一旦被getFieldDecorator修饰过的组件触发onChange事件,便会触发这个父组件的的更新(forceUpdate),从而促使被包装组件的render。 如:Form.create()(A) A就是我们所说的被包装组件

注册(getFieldDecorator):

getFieldDecorator的目的是为了把需要收集的数据在实例中进行注册,并把注册的值同步到被getFieldDecorator修饰的组件B上。所以组件B不能够在通过value赋值,组件B的状态将全部由getFieldDecorator托管。

收集、校验

收集校验就更简单了,你可以认为收集校验就是这个实例提供的几个方法而已。

丢弃Form.create

如果Form.create的核心能力是创建"实例",是不是意味着可以不用Form.create包裹组件呢? 答:是的,如果把更新组件的副能力解决掉。恰好Ant Design Vue就是这么去做的。

Vue版Form的进化史

起初我们使用了和React版一致的写法,必须使用Form.create包裹组件,但vue推崇的template语法很难再去使用,你不得不去在Vue中使用JSX,遭到了用户的各种吐槽。 然后我们进行了改版,将Form.create放在了Form中去执行,通过回调的方式将Form.create创建的示例传递回来:

<a-form :autoFormCreate="form => this.form = form">...</a-form>

注册通过a-form-item添加对应属性来劫持子元素进行注册。

<a-form-item
  fieldDecoratorId="name"
  :fieldDecoratorOptions="{
    rules: [{ required: true, message: 'xxxx !' }]
  }"
>
  <a-input/>
</a-form-item>

这样一种设计他有很大的问题:

  1. form不能及时拿到,我们应该在组件render之前拿到form实例
  2. 通过a-form-item劫持子元素有很大的限制,每一个a-form-item下只能注册一个,当然这个问题不大,我们可以在提供一个a-form-control专门用来注册组件,O__O "…嵌套好深。

最终方案:

实例:

既然Form.create的主要能力是创建"实例",我们可以暂时抛开组件,先解决构建实例的问题,

createForm(options = {}) {
  return new Vue(Form.create(options));
}

我们在组件上提供一个静态方法createForm来创建这个示例,那么有了这个和组件没有任何关系的方法,就可以随时创建"实例",同一个组件中也可以同时拥有多个"实例"。核心能力有了,但没有副能力也是不行的,就像没有了四肢的大脑,有心无力。 前面讲了,组件的更新需要组件所在上下文处执行render,那么问题就简单了,我们只需要把当前组件的上下文传递给这个"实例",当注册到实例的组件需要更新时,直接调用context.$forceUpdate()即可。代码如下:

createForm(context, options = {}) {
  return new Vue(Form.create({ ...options, templateContext: context })());
}
注册:

直接新增一个组件a-form-control专门用来劫持组件并注册是一个不错的选择,但是我不想让组件嵌套太深,所以我们还是使用a-form-item进行劫持组件,为了能够区分需要劫持的哪些组件,我们使用指令进行标记并传值, 之所以使用指令是因为我们不应该为一个需要注册的组件传递一个不相关的属性,如果传递一个未经声明的属性,则该属性会被挂载到dom上,如果要声明属性,就必须对自定义表单控件添加额外约束。而使用指令进行标记和传值不会存在这类问题。

<a-input
    v-decorator="[
      'note',
      {rules: [{ required: true, message: 'Please input your note!' }]}
    ]"
/>

校验收集和React版没有区别,都只是"实例"的方法。

为什么不支持双向绑定

严格来说并不是完全不支持,如果你不需要Form的自动收集、校验功能,是可以使用双向绑定的。 双向绑定在某些业务场景下的确可以节省很多代码,但对于某些情况下又给我们带来了不必要的麻烦。 举一个很简单也很常见的栗子: 在系统中同一份数据被多处组件(包含可编辑的Form)使用是常有的事情,我们在表单中改变这份数据,同时数据的改变同步到各个相关组件中,非常easy的完成了需求。但很多时候我们希望表单数据改变后并不需要及时的同步到其它组件中,而是当用户点击确定按钮后才将数据同步,我们就不得不将这份数据进行复制甚至是深复制来满足需求,甚是蛋疼。

而如果使用ant-design-vue单项数据流的方式,数据之间的流向就变得非常清晰,表单就像一个独立的沙盒,不管沙盒中的数据如何变化,都不会影响到沙盒的外部,而沙盒通过相关API方法和外部进行交互。

最后,10分钟精(wo)通(shi)不(biao)存(ti)在(dang)的,但希望大家能够通过本文对antd的Form有一个进一步的认知,Form依然还有很多的功能需要大家自己去探索,在这就不一一展开了,我想也没有必要展开。如果大家有更好的方案也欢迎提issue提pr,一起探讨,将ant-design-vue打造成世界第二好用的Vue UI组件库。 谁是第一好用的?你问我? 那当然也是ant-design-vue,且不接受任何异议,就是那么自信,那么臭不要脸。

最后的最后,给团队微信公众号打个广告,微信搜索“一点大数据技术团队”关注公众号,你没看错,就是大数据,如果你对大数据感兴趣,欢迎关注该公众号,我们每月会从团队内部筛选出两篇左右的高质量原创文章。