被人诟病的 Form
Form 的原理
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>
这样一种设计他有很大的问题:
form 不能及时拿到, 我们应该在组件 render 之前拿到 form 实例
通过 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, 一起探讨, 将 https://github.com/vueComponent/ant-design-vue 打造成世界第二好用的 Vue UI 组件库. 谁是第一好用的? 你问我? 那当然也是 https://github.com/vueComponent/ant-design-vue , 且不接受任何异议, 就是那么自信, 那么臭不要脸.
最后的最后, 给团队微信公众号打个广告, 微信搜索 "一点大数据技术团队" 关注公众号, 你没看错, 就是大数据, 如果你对大数据感兴趣, 欢迎关注该公众号, 我们每月会从团队内部筛选出两篇左右的高质量原创文章.
来源: https://juejin.im/post/5c47ffff51882533e05ef4f9