面向复杂场景的高性能表单解决方案(入门篇)

面向复杂场景的高性能表单解决方案(入门篇)

阅读需要 15 分钟

作者:白玄 @baixuan,张猴

经过3年的洗礼,由阿里供应链平台前端团队研发打造的UForm终于发布!🎉🎉🎉
UForm 谐音 Your Form , 代表,这才是你想要的Form解决方案
高性能,高效率,可扩展,是UForm的三大核心特色
查看完整文档请戳 https://uformjs.org

上次在背景篇中,笔者大篇幅的阐述了UForm的核心设计理念,主要是为了帮助大家在后面使用UForm的过程中,能够触类旁通,高效解决问题,所以,现在进入正题,快速入门UForm

我们看看入门级别需要掌握哪些东西:

以上列举的知识点都将会配有 codesandbox 链接,方便大家快速学习与实践,掌握这些内容,对于一般的业务场景,基本可以使用UForm快速落地业务。

为了让大家更加有体感并且有对比,我们用原生的 form 、Ant Design、 SchemaForm 来实现一个简单的登录页面,填入姓名和密码, 然后 Submit 和 Reset。

对比

1. 原生 Form

<form method="post" action="action">
    <p>
      <label for="name">姓名:</label>
      <input type="text" name="name" id="name">
    </p>
    <p>
      <label for="password">密码:</label>
      <input type="password" name="password" id="password">
    </p>
    <p>
      <input type="submit" value="确定">
      <input type="reset" value="取消">
    </p>
</form>

通过标签描述很快可以写出来,但是也暴露出问题比如只能同步提交,还有如果要实现下面的功能的话,

  • 请求失败, loading, 成功不同阶段的区分;
  • 姓名和密码回填;
  • 校验和错误透出;

你会发现你页面出现很多 document.getElementById 去获取 element, 然后设置值或者 display 样式。

2. Ant Design

<Form onSubmit={(values) => { submit(values) }}>
  <Form.Item label="姓名">
    {
      getFieldDecorator('name')(<Input />)
    }
  </Form.Item>
  <Form.Item label="密码">
    {
      getFieldDecorator('password')(<Input.Password />)
    }
  </Form.Item>
  <Form.Item>
    <Button type="primary" htmlType="submit">
      提交
    </Button>
  </Form.Item>
</Form>


使用 Ant Design 我们需要在每一个输入框通过 getFieldDecorator 来做双向绑定,也很难做到服务端驱动渲染。但是优点是不需要引入 Field 概念,自由度高。

3. UForm

<SchemaForm onSubmit={(values) => { submit(values) }}>
  <Field type="string" name="name" title="姓名" />
  <Field type="string" name="password" title="密码" />
  <FormButtonGroup>
    <Submit />
    <Reset />    
  </FormButtonGroup>
</SchemaForm>

也可以通过自定义 Field 结合对象解构的方式实现,点击查看例子

描述也是较为简单, SchemaForm 可以类比成 form , Field 可以类比是 label 和 input 的结合体 , 从而来减少重复的属性设置比如 label 的 for 和 input 的 id。对于上面提出的使用原生 form 出现的问题, 我们下面会一步一步解决它。相信你应该会好奇 SchemaForm 和 Field 具备什么样的能力,下面来慢慢阐述。

基础组件

1. SchemaForm 组件

SchemaForm 组件是 UForm 的核心渲染引擎组件,提供的能力有:

  • 统一 Field 属性管理(样式/editable);
  • 初始化设置(value/defaultValue/initialValues);
  • 表单事件监听(onSubmit/onChange);
  • 副作用管理(effects)
  • 通讯管理(actions)

2. Field 组件

描述表单项的最小单位

Field 组件是 JSchema 的核心描述组件,什么是 JSchema,我们在背景篇里有详细说明。您只需要了解一个核心概念,Field组件的属性等价于JSON Schema每一个字段的描述信息。所以,只要JSON Schema支持什么属性配置,Field组件同样也支持什么属性配置,相反,也是一样。还有一个很重要的点,Field组件是不允许用户直接传onChange事件回调做响应处理的,如有任何疑问,请看背景篇

简单介绍所具备的能力有:

  • 表单项具体表象
  • 内置 (type)
  • 自定义 (x-component/x-render)
  • 属性 (x-props)
  • 校验规则
  • 内置 (required)
  • 自定义 (x-rules)
  • 默认值 (default)

了解了 UForm 两大基础组件之后,下面我们通过不断完善“登录功能”的例子来更为全面了解 UForm。一步一步完善的功能有:

  • 校验
  • 数据回显
  • 只读模式
  • 登录方式切换
  • 布局
  • 批量处理
  • 复杂联动
  • 服务端驱动

功能

我们通过登录例子结合 UForm 的特性不断去完善功能

1. 校验

我们大概了解了 Field 和 SchemaForm 了, 现在来体会下新增一些新的需求上去。先尝试的是加上检验, 没有姓名和密码我们肯定不让提交。Field 提供了 required 属性, 姓名用 required, 密码用 x-rules,殊途同归,点击查看例子

⚠️ 注意:用 x-rules 自定义了 required 功能,少了红色星点,功能没有 required 方便和强大。所以我们必填尽量设置 required,用 x-rules 自定义 required 是为了让大家方便看下 UForm 提供的能力。

自定义 x-rules 还支持异步,需要返回一个 promise 接口,比如我们实现一个功能是输入登录名需要跟后端校验登录名是不是已经注册了,没有注册的话需要 error 提示。点击查看例子

对于校验有三类

  • 配置化 required,直接配置到 Field 的 props 上;
  • 内置的 x-rules,比如 phone,email 等;
  • 自定义 x-rules 函数,这种是偏业务属性;

2. 数据回显

用户登录后要看他之前设置的密码和名称了,这个时候需要把数据回填到 form,点击查看例子

里面包含了四种方式

  • setFormState:动态设置整一个 Form 的 state,如果默认值是异步的,用这个较为方便;
  • setFieldState:动态设置局部 field,可以操作单个,也可以结合 FormPath 同时操作多个;
  • defaultValue:适用于初始化阶段,跟 React 非受控的概念一样,第二次修改 defaultValue 并不会起作用;
  • value:主要用于受控场景,结合 onChange 使用,不推荐这种写法,应该会导致全局 Field rerender;
  • initialValues:适用于异步数据初始化阶段,第二次修改 initialValues 有效果;

3. 只读模式

咱们再加上一个只读模式吧,也就是一开始不可编辑,只能预览,并且可以切换到编辑态,点击查看例子

里面包括了两种方式:

  • SchemaForm editable:统一设置整一个 Form 的编辑状态,初始化时使用;
  • actions.setFieldState:设置局部 Field 的编辑状态;

4. 登录方式切换

我们再加上一个需求就是不止支持一种登录方式, 还可以多个的,比如一些是手机号+验证码。这一次我们主要要学会就是处理联动,常规登录的话是有密码和姓名,但是在手机登录是只有手机号的,点击查看例子

我们监听了两个effect,onFormInit 和 onFieldChange,然后通过 actions.setFieldState 的方式来对应 Field 的显隐。

不同登录方式切换的时候要把提示信息显示到 form 里面,而这个提示信息又是在登录方式下拉框数据里面的。Field 内置了监听 onChange 获取 value 并设置,但是如果在 onChange 设置的时候获取下拉框其他数据呢?这个时候就需要用上 x-effect,点击查看例子。在 type 字段通过 dispath 自定义的 selectOptions effect,然后在 effects 监听此 effect,通过 actions 做一些操作。在这个例子中我们还通过 x-render 来自定义 x-component 的 style,x-render 更多玩法可以查看对应的文档。

5. 表单分块

我们把上面几个输入框分成几类,分别是登录类型、基础信息、提示信息,基础信息是包含提示信息的,要在样式结构上有所体现。我们通过 FormCard、FormItemGrid、FormCard 内置的 Form 内置布局系统来实现,点击查看例子

6. 批量处理

你看上面例子的代码就一定有疑问,比如

actions.setFieldState("name", state => {
  state.visible = true;
});

actions.setFieldState("password", state => {
  state.visible = true;
});

能不能统一处理呢?我们结合 FormPath,其实可以类比是 jQuery 强大的选择器,语法可能不太一样,但是思路是一样的,点击查看例子。我们实现的功能有:

  • 批量设置表单校验
  • 批量设置表单属性
  • 批量设置表单显示隐藏
  • 批量设置表单可编辑状态

7. 复杂联动

联动在前端领域都是一个复杂的东西,特别在结合时序和数据异步情况,会发现联动处理起来相当麻烦。UForm 主要提供了 actions 和 effects 能力来解决这个问题,点击查看例子 。例子中类型 Field的数据源是异步加载的,还有信息 Field 内容也是切换类型的时候异步获取的。还有我们加上确认密码,这个时候校验就要联动了,需要比对密码和确认密码两个 value 是否一致。具体使用可以看下例子。我们实现的功能可以分类成:

  • 联动控制字段属性,比如批量修改编辑状态;
  • 异步联动,比如切换类型加载信息内容填充到 Form;
  • 异步联动默认值,比如类型数据源是异步加载并选择第一项作为默认值;
  • 联动校验,比如确认密码和密码比对;

8. 服务端驱动

如果让后端直接告诉我们对应 Field 字段呢?内容由服务端来填充,我们只是处理一些联动。我们可以结合 SchemaForm 的 schema 字段来实现,点击查看例子。我们实现的功能有:

  • 在JSON Schema驱动模式中使用x-render
  • 在JSON Schema驱动模式中使用x-effect
  • 在JSON Schema驱动模式中使用x-rules

方案梳理

通过上面例子不断丰富功能,我们可以有体感得了解 UForm 整一个能力。我们再来回顾下,查漏补缺。

1. 通讯方案

1). 外—>内通讯场景

主要是在表单外部的某个动作需要控制表单字段的状态(值,显示隐藏,是否可编辑等)

在上面的例子我们主要实现了

  • 控制表单的值,通过 defaultValue,initialValues,setFormState 等;
  • 控制表单项显隐,通过 actions.setFieldState 设置 visible;
  • 控制表单项编辑状态,通过 SchemaForm 的 editable 和 actions.setFieldState 设置 Field 的 editable;
  • 外部异步流控制表单内部异步流,比如表单内部的联动是依赖外部异步事件流的场景,我们主要是使用actions.dispatch 派发自定义事件,然后统一在effects内做收敛处理

2). 内—>外通讯场景

内部与外部通讯,主要表现是表单的提交事件需要触发外部数据提交,表单数据变化需要触发外部状态变化

在上面的例子我们主要实现了

  • 使用onSubmit事件收集表单数据
  • 在effects事件中做更细粒度的变化订阅

3). 内—>内通讯场景

内部与内部的通讯场景,其实核心还是在effects函数内部做副作用处理,但是有一个特殊场景,存在一些effects内的默认事件处理器捕捉不到的事件,比如onSearch。或者onChange的第二个参数的数据拿不到。
对于这类场景,我们主要使用Field组件的x-effect属性做事件冒泡,投递给effects做处理。

在上面的例子我们主要实现了使用x-effect属性做effects事件冒泡

2. 布局方案

在上面例子中我们了解 FormCard 等布局组件,你一定会想了解为什么会存在布局组件?

主要是因为 JSchema 在描述复杂表单布局结构的时候,如果直接在<Field/>组件外包裹div,写样式,是不会有任何效果的,因为Field组件只是一个描述型标签,它不是具体的UI组件,这就跟Ant Design的Table组件的Column一样,只负责描述,不负责渲染,渲染是在内部做处理的。所以,为了让大家更方便的在JSchema中描述布局,我们支持了一系列的布局组件,可以让您自由顺畅的写复杂的表单布局。这里是布局组件的案例

当然,对于布局组件,您也是需要关注几点的:

  • 布局组件其实也是自定义 Field 组件,但是它并不会增加数据结构层级,您可以理解为它是一个虚拟字段。
  • 布局组件也可以传 name 属性,在 effects 中可以对布局组件做状态控制,比如控制它的显示隐藏,默认是不需要指定name的,如果不指定 name,UForm 会自动生成UFORM_NO_NAME_FIELD_这样形式的name值
  • 在 JSchema 中使用布局组件与在 JSON Schema 中使用布局组件的差异主要是:
  • JSchema中布局组件的属性(除了name属性)是 JSON Schema 里Field的x-props
  • JSchema中布局组件不需要指定 x-component,直接声明式使用对应的布局组件即可,JSON Schema中需要指定对应布局组件的 x-component key。
  • 默认布局组件所提供的 x-component key 有:
    • card 对应 FormCard
    • block 对应 FormBlock
    • layout 对应 FormLayout
    • grid 对应 FormItemGrid

如果是服务端驱动 Field 生成的话,可以通过点击表单布局例子中的 Print JSON Schema 按钮来查看具体的使用方式。

3. 自定义方案

UForm 会内置一些常规的组件,但是也希望大家发挥直接的想象力可以自定义 Field布局组件,UForm 可以做到兼容并包来建立生态。

1). Field

目前UForm已经集成了大多数Ant Design/Fusion Next的表单组件,以下是所有已支持的Field组件类型(以Ant Design为例)。

对于一些特殊交互场景,@uform/next和@uform/antd默认提供的Field组件可能并不能满足您的需求,只能通过创建自定义Field组件的形式来做扩展,幸好,创建自定义Field组件的方式并不难。

您只需要搭配使用 registerFormFieldconnect 函数便可以快速创建一个自定义Field组件,上面例子中有自定义一个 Text x-compoent


对于学习扩展自定义Field组件,最好的学习方式就是直接看源码。所以,上面所列举的自定义组件的源码,您可以一一看过去,从最简单的string到最复杂的array,都可以看看。

2). 布局组件

为什么要创建自定义布局组件,当然就是没法满足需求的时候才会需要自己创建布局组件。

话不多说,主要是使用 createVirtualBox API创建布局组件,需要注意的是,第一个参数的name就是x-component key,在JSchema中是不需要关心这个key的,只需要声明式使用即可,就是针对JSON Schema驱动渲染的场景是需要关心,点击查看自定义布局组件例子

总结

我们通过与原生的对比,来了解我们原生书写 Form 的不易。了解了 UForm 基础组件 SchemaForm 和 Field 提供的能力,我们再通过登录例子贯穿结合 UForm 所提供的能力,让大家更加有体感。最后我们对上面涉及到 UForm 能力进行了分类梳理,让大家更容易吸收掌握,以便快速落地业务。

编辑于 2019-05-31

文章被以下专栏收录

    面向复杂场景的中后台高性能表单解决方案 由阿里供应链前端团队打造。 团队持续招人中,简历投递 zhili.wzl@alibaba-inc.com