雪花新闻

中台动态化表单中心的设计实现

前言

对于 ToB 业务而言,随着业务的不断壮大,接入的客户逐渐增加,相同页面的差异化的需求越来越多,尤其是在表单层面,小到多一个字段少一个字段这种简单的需求,大到整个页面不变的只剩下一些基础字段。

一旦这种差异化需求随着业务量的增长而膨胀起来。代码中的 IF ELSE 越来越多,项目就越来越难以维护。基于这个问题,比较普遍的解决方案要么是项目拆分,要么相同项目的代码分割。

这两种方案都有维护成本比较大的弊端,那么有没有更好点的解决方案呢。本文就带你了解一下动态化表单搭建。

什么是动态表单

先下个定义,动态表单是页面根据管理端配置的不同的 Schema 结构,动态渲染出不同的表单项的表单。

动态表单一般分两个部分,管理端和渲染端。

管理端配置表单项及相应的简单交互产出 Schema 数据。

渲染端根据 Schema 数据相应的渲染出不同的表单项并实现简单的交互。大致流程如下。

动态表单的实现

表单配置

对于 Schema 数据的配置,考虑到接入业务方的接入成本及维护成本。

管理端采用了可拖拽式的所见即所得配置面板。这里共分为四个部分,备选组件面板,拖拽面板,组件属性面板和表单属性配置(视图属性)。

具体实现如下图:

备选组件面板

左侧备选组件栏里的备选组件共分三种,容器组件,基础组件,自定义组件。

拖拽面板

拖拽面板就是维护组件展示关系的面板,同时提供拖拽排序、删除、复制、预览等功能。

具体实现方案采用的是 React-DnD 。

组件配置

属性配置面板本身就是个更加轻量级的动态表单实现。

只是 Schema 由开发者直接写死而没有一个可配置的页面而已(自定义组件注册部分例外)。

当在拖拽面板选中一个组件时,组件属性配置面板会渲染出相应组件的可配置项表单, 这里提供一下简单的组件属性配置面板的 Schema 供大家参考。

[{
  label: '是否可见',
  code: 'visible',
  widget: 'switchBtn',
  initialValue: true
},
{
  label: '是否可编辑', // 标签文案
  code: 'code', // 字段编码
  widget: 'switchBtn', // 组件类型
  initialValue: true, // 初始值 默认可编辑
  hide: 'exp: visible === false', // 是否隐藏
  required: true // 是否必填
}]

细心的同学会发现 hide 字段写了个表达式。这里通过 exp: 开头作为一个表达式的标识。表达式的可以使用的变量是属性表单内的值。

比如上面这个例子,visible 是上面定义了一个是否可见的字段。如果当前选中的这个组件不可见的话,是否可编辑本身就无从谈起,所以直接隐藏掉。

容器属性共有的属性有标题、编码、是否可见、以及容器结构是否对数据透明。

前面三个好理解。容器结构是否对数据透明是什么呢?

前面说过,我们的容器组件是可多层嵌套的,那问题来了,数据咋办,表单嵌套会导致数据也跟着嵌套。所以这里参考了阿里的 Formily 开源表单方案。使用一个 skip ,来使其对数据透明。即:

{
  "title": "表单",
  "type": "form",
  "fields": [{
    "name": "name",
    "label": "姓名"
  }, {
    "title": "子表单",
    "skip": true, // 表单结构对数据透明
    "name": "item",
    "type": "form",
    "fields": [
      {
        "name": "object",
        "label": "物品"
      }, {
        "name": "brand",
        "label": "品牌"
      }
    ]
  }]
}

skip 为 false 时返回的数据为:

{
  "name": "简名",
  "item": {
    "object": "电脑",
    "brand": "Mac"
  }
}

skip 为 true 时返回的数据为:

{
  "name": "简名",
  "object": "电脑",
  "brand": "Mac"
}

组件属性分为基本属性和组件属性,基本属性是所有属性共有的。标题,编码,是否可见,是否必填等属性都是基本属性。组件属性则是组件私有的属性。

比如 Select 组件会需要一个数据来源,以及该组件是否多选之类的。基本属性直接写死。组件私有属性则通过远程数据库维护。自定义组件的注册就需要涉及到这部分的数据管理。

自定义组件的注册表单如下:

其中组件可配置属性就是组件私有的属性的定义,注册时定义,配置该组件时赋值,渲染端渲染时应用。可配置属性还需要支持表达式的填写。

比如某个组件需要远程数据,url 提供了,但是参数需要取当前时间,这个时候就需要组件属性支持表达式的解析或者少量代码读写运行了。

这些属性除了组件自定义属性以外,还有组件默认值,组件自定义校验,组件 onChange 事件。

以自定义校验为例:

表单属性配置(视图属性)

这部分在上图中没有显示,是在组件属性右侧。表单属性分两部分,交互规则和接口绑定。

交互规则表单交互规则在表单级别绑定,而不是在字段级别。进行就近配置的目的,是为了方便管理,进入一个表单配置,该表单的交互在右侧一目了然。

* 接口绑定 * 则是表单渲染过程中有可能涉及到一些远程数据的读取,比如默认值等。这部分数据的配置需要用到远程数据。表单上绑定了接口之后,表单初始化之前先发请求获取绑定接口的数据,相应的表单组件里就可以使用到该数据进行初始化。

管理端数据流转

管理端的功能是构建出一个目标 Schema。

每个备选组件都有基本信息和相应的组件可配置属性信息。

组件基本信息主要用于组件面板展现。组件可配置属性需要在右侧属性配置时渲染成一个表单给使用者去配置,故而组件可配置信息又是一个简化版的 Schema,这里称为组件级 Schema。

在拖拽页面中添加一个组件,通过解析组件的组件级 Schema 及组件放置位置给目标 Schema 添加一个组件数据。

然后在拖拽页面中选中该组件,右侧属性配置会相应渲染出组件级 Schema 所描述的表单给用户配置填写。用户配置时直接修改目标 Schema 中相应选中组件的信息。

数据流转图大致如下:

表单动态渲染

因为表单页面还会有各种定制化的需求,表单渲染端这里采用组件的形式,提供了两个组件,一个组件作为表单页面的外层包裹组件主要功能是发请求获取相应的 schema.json 数据。

另一个组件就是通过上层组件的数据渲染相应的表单。示例:

import { FormPageWrap, MainForm } from './index';
@FormPageWrap({
  prefix: '/api/budget', // 业务方接口前缀
  getFormParams: (props) => { // 获取表单结构参数
    return {};
  },
  getDataParams: (props) => { // 获取表单回填数据参数
    return {};
  }
})
export default class FormPage extends Component {
  render() {
    return (
      <div>
        // 表单各种额外显示内容
        <MainForm // 表单渲染组件
          customComponent={{
            test: TestComp,
          }}
          extraParams={{}}
        />
        // 表单各种额外显示内容
      </div>
    );
  }
}

内部实现则是根据 Schema 渲染相应的组件。

待完善

目前系统部分功能还有待完善。具体有几点:

展望

对于动态化表单的能力远不止目前看到的动态表单搭建:

❉ 作者介绍 ❉

相关文章