MuYunyun/oneForm

关于表单的探讨

MuYunyun opened this issue · 0 comments

出发点

中后台项目中表单可以说是最重要的一环, 基本上中台的项目都是在书写各种各样的表单。如何提高开发效率是 daform 正在探索和实践的点。

  • 重复性。
  • 动态表单。每次遇到动态表单的需求都是要花大精力进行, 而且与 redux 耦合性较强, 开发体验十分不好。

设计理念

  • 双向绑定
  • 栅格化布局
  • 错误逻辑集中化管理
  • 动态表单的解决方案
  • 不依赖第三方状态管理库
  • 自由搭配第三方 UI 组件库

可以像如下这样使用

import React from 'react'
import { Input } from 'antd' // 可以是其它 UI 库
import { Form, FormItem } from 'daform'

@Form()
class Demo1 extends React.Component {
  render() {
    return (
      <>
        <FormItem name="name" label="姓名"><Input /></FormItem>
        <FormItem name="age" label="年龄"><Input /></FormItem>
      </>
    )
  }
}

表单静默刷新 vs 双向绑定

如果表单间的数据无需联动, 差不多就是表单数据显示 => 全局对象的单向映射的模型, 这样子只要处理好这一方向的流动性就好了。但是一旦表单间的数据需要联动, 这就涉及到表单 <=> 全局对象的双向映射了(表单间某一数据发生变动通知全局对象, 全局对象改变另一数据变化, 呈现到表单对应数据上)。需要一种机制来在当前的 React 表单中实现双向绑定。借助 React 16.3 出来的 Context 机制, 实现了一个伪双向绑定的表单。

双向绑定

目前表单中修改的数据已经实时同步到内部维护的 formData 对象中, 但假若要使用 setFormItem(itemName, value) 向 formData 写入或修改数据后, 页面应当进行重新渲染, 这时候可以考虑使用观察者模式进行处理。但这个 api 当下的需求用到其实不是特别多, 这部分可以日后实现。

目前用 initialValue 能开发大部分需求。

关于 Context 使用

每当Provider的值发送改变时, 作为Provider后代的所有Consumers都会重新渲染。 从Provider到其后代的Consumers传播不受shouldComponentUpdate方法的约束,因此即使祖先组件退出更新时,后代Consumer也会被更新。

因为表单有联动的需求, 所以重绘表单是必要的。但是处于性能的考虑只重新渲染有变动的表单是十分必要的。结合 Context 的机制, 选择了在 Context 下面使用 PureComponent。

避免表单多次重绘

因为使用 React.cloneElement() 创建 React 对象传入子组件, 所以导致子组件每次都会重新渲染, 因此使用单例模式在内存里将创建的 React 对象进行缓存, 从而避免了多次重绘。目前的设计是仅当 disable 属性发生变化时, 才会再次调用 React.cloneElement()

栅格化布局

借助 sass/less 的语法能动态快速生成 24 栅格化布局。如下以 less 为例生成相应的布局样式

.generate-columns(24);
.generate-columns(@n, @i: 1) when (@i =< @n) {
  .col-@{i} {
    width: (@i * 100% / @n);
  }
  .generate-columns(@n, (@i + 1));
}

错误逻辑集中化管理

错误捕获使用了 async-validator, 个人倾向于将一个表单的错误逻辑放在一个文件里进行管理。

表单存在的状态形式

难免可视状态和编辑状态的字段是会有些微小的地方存在不一致, 基于这样的思考🤔, 暂时将表单的状态只分为编辑态禁用态

动态表单

提供一个动态表单组件 <Dynamic>

import React from 'react'
import { Input } from 'antd'
import { Form, FormItem } from 'daform'

@Form()
class Demo1 extends React.Component {
  render() {
    const { form } = this.props
    return (
      <Dynamic>
        <FormItem name="name" label="姓名"><Input /></FormItem>
        <FormItem name="age" label="年龄"><Input /></FormItem>
      </Dynamic>
    )
  }
}

表单里面所有数据结构(包括动态表单)都是扁平化的, 动态表单数据结构类似:

{
  "name0": "deku",
  "age0": "12",
  "name1": "siren",
  "age1": "13",
  "name2": "diana",
  "age2": "11"
}

测试用例

框架选择了 Jest + Enzyme;

测试应该覆盖到的几个点:

  • Form
    • Form 中的 data
  • FormItem
    • 传入 FormItem 的各个属性的校验
    • 渲染次数的校验