Form Schema 定义详解

2,094 阅读7分钟

作者:汪曦

背景

上期分享了基于 Formily 的表单设计器实现原理,JSON Schema 是表单设计器和表单渲染组件之间沟通的语言。为了更深入理解表单设计器的核心,本期为大家详细解读表单 Schema 详细格式及快速入门实践。

Formily 提供了 JSON Schema、JSX Schema、纯 JSX 三种开发模式。由于 JSON 可以序列化保存到数据库中,所以 JSON Schema 的方式非常适合后端动态渲染表单,前端完全不需要维护 schema,只需利用 Formliy 提供的 SchemaForm 来渲染后端返回的 schema 即可。我们仅需通过 Form Builder 或者 Page Designer 之类的工具来输出 JSON Schema,然后交给 SchemaForm 或者 PageEngine 之类的组件来渲染。

说明:

Form Schema 遵循 json schema spec的描述规范动态生成表单。

JSON Schema 规范

JSON Schema 是一个社区推动的 JSON 文件协议,用于规范 JSON 文件内容。它与平台无关,可以描述任意复杂的数据结构,相比 XML,JSON 的描述格式更加紧凑,可读性更好。JSON Schema 在 JSON 的格式上,加入了一些列的标准化属性,用于描述结构化数据。Formily 遵循 JSON Schema 使用最广泛的draft-07 标准,并在其规范上扩充了自己的属性。

我们可以借助 ajv 这类 JSON Schema 验证工具,来认识不同 Schema 规范的区别,详情可参考 draft-07 (and draft-06)。也可以执行 npm i ajv ,在 nodejs 下查看 drat-07 的规范:

require("ajv/dist/refs/json-schema-draft-07.json")

Form Schema 结构

Formily 的 Form Schema 将标准的 JSON Schema 的 properties 属性进行了扩充,关键字段说明如下:

属性名描述类型
title字段标题React.ReactNode
name字段所属的父节点属性名string
description字段描述React.ReactNode
default字段默认值any
type字段类型string, object, array, number
enum枚举字段string[], number[], Array<{ label: React.ReactNode, value: any }>
required字段是否必填string, bool
maximum校验最大值(大于)number
minimum校验最小值(小于)number
maxLength校验最大长度number
minLength校验最小长度number
pattern正则校验规则string, RegExp
properties对象属性{[key : string]:Schema}
items数组描述Schema, Schema[]
editable字段是否可编辑boolean
visible字段是否可见(数据+样式)boolean
display字段样式是否可见boolean
x-props字段扩展属性{ [name: string]: any }
x-index字段顺序number
x-component字段 UI 组件名称,大小写不敏感string
x-component-props字段 UI 组件属性{}

通过对比 Form Schema 和标准的 json-schema-spec ,不难发现x-propsx-componentx-component-props 等 x- 开头的属性是新增的,这些属性被 Formily 用来控制组件的渲染、数据校验、联动以及定义副作用等等。

Form Schema 生成表单

理解了 Form Schema 的定义,我们就可以借助 Formily 的 SchemaForm 组件来动态生成表单。

  1. 举个例子,通过如下代码片段就可以渲染出一个带用户名、密码的基本登录界面了。

    import React from 'react'
    import ReactDOM from 'react-dom'
    import { SchemaForm } from '@formily/next'
    import { Input } from '@project/components'
    
    const Title = () => (
      <h2 style={{display: 'flex', justifyContent: 'center'}}>
        登录
      </h2>
    )
    
    const schema = {
      type: 'object',
      properties: {
        title: {
          type: 'string',
          'x-component': 'Title'
        },
        username: {
          type: 'string',
          title: '用户名',
          'x-component': 'Input'
        },
        password: {
          type: 'string',
          title: '密码',
          'x-component': 'Input'
        }
      }
     }
    
    const App = () => {
      return (
        <SchemaForm
          components={{
            InputTitle
          }}
          onSubmit={console.log}
          schema={schema}
        />
      )
    }
    ReactDOM.render(<App />, document.getElementById('root'))
    

    说明:

    SchemaForm 有两个属性很关键。

    • schema 属性是外界传入的 json schema,这个 schema 通常作为字符串存在后端,前端通过接口去获取,JSON.parse 之后交给 SchemaForm 渲染。整个过程,前端完全不关心 schema 的来源以及如何构造。
    • components 属性定义了 schema 渲染时的可用组件上下文。form schema 中每个 x-component 属性都是一个 string,x-component 作为 key,要在 components map 中找到 value 并且 value 是一个合法的组件(可以是 react、vue、angular 的组件,formily 不和具体的 UI 库绑定),这样就可以渲染出具体的组件。

    效果图如下: file

    以 react 为例,大致的渲染过程是:

    const fieldSchema = get(formSchema, 'properties.field_x')
    const compName = fieldSchema['x-component']
    const compProps = fieldSchema['x-component-props']
    
    React.createElement(SchemaForm.components[compName], compProps)
    
  2. 理解了基本登录页面的渲染,我们来看一个更加复杂的 schema 示例,效果图如下:

    const schema = {
      "version": "1.0",
      "type": "object",
      "properties": {
        "radio": {
          "type": "string",
          "enum": [
            "1",
            "2",
            "3",
            "4"
          ],
          "title": "Radio",
          "name": "radio",
          "x-component": "radio"
        },
        "select": {
          "type": "string",
          "enum": [
            "1",
            "2",
            "3",
            "4"
          ],
          "title": "Select",
          "name": "select",
          "x-component": "select"
        },
        "checkbox": {
          "type": "string",
          "enum": [
            "1",
            "2",
            "3",
            "4"
          ],
          "title": "Checkbox",
          "name": "checkbox",
          "x-component": "checkbox"
        },
        "textarea": {
          "type": "string",
          "title": "TextArea",
          "name": "textarea",
          "x-component": "textarea"
        },
        "number": {
          "type": "number",
          "title": "数字选择",
          "name": "number",
          "minimum": 0,
          "maximum": 100,
          "x-component": "numberpicker"
        },
        "boolean": {
          "type": "boolean",
          "title": "开关选择",
          "name": "boolean",
          "x-component": "switch"
        },
        "date": {
          "version": "1.0",
          "key": "date",
          "type": "string",
          "title": "日期选择",
          "name": "date",
          "x-component": "datepicker"
        },
        "daterange": {
          "type": "date",
          "title": "日期范围",
          "default": [
            "2018-12-19",
            "2021-12-19"
          ],
          "name": "daterange",
          "x-component": "daterangepicker"
        },
        "upload": {
          "type": "array",
          "title": "卡片上传文件",
          "name": "upload",
          "x-component-props": {
            "listType": "card"
          },
          "x-component": "upload"
        },
        "range": {
          "type": "number",
          "title": "范围选择",
          "name": "range",
          "x-component-props": {
            "min": 0,
            "max": 1024,
            "marks": [
              0,
              1024
            ]
          },
          "x-component": "range"
        },
        "transfer": {
          "type": "number",
          "enum": [
            {
              "key": 1,
              "title": "选项1"
            },
            {
              "key": 2,
              "title": "选项2"
            }
          ],
          "x-component-props": {},
          "title": "穿梭框",
          "name": "transfer",
          "x-component": "transfer"
        },
        "rating": {
          "type": "number",
          "title": "等级",
          "name": "rating",
          "x-component": "rating"
        }
      }
    }
    
    const components = {
      // 可用的组件上下文
    }
    
    ReactDOM.render(<SchemaForm schema={schema} components={components} />, document.body)
    

file

总结

综上,用 JSON Schema 来描述表单适合于低代码或者数据中台的快速开发。前端不需要维护 schema,schema 可以存在后端,随意分发动态渲染。但是缺点是 JSON Schema 的表达能力没有 JSX 强,在处理复杂交互时,前端还是需要使用 JSX。

另外 JSON Schema 在交互过程中的反复解析也是性能的瓶颈,尤其是 schema 的内容很多且表单需要做复杂联动和批量更新时,性能问题更加明显。原因是 Formily 内部会对状态做深拷贝,同时也做了深度遍历脏检测,这种方式能够提升用户体验,但在大数据场景下,就会出现性能问题, 此时需要考虑屏蔽 Formily 的局部重复渲染,回到 react 的整树渲染。

当然任何方案都只是解决了部分的问题,一个方案能满足 80% 的场景,剩下的 20% 可以回退到其它的方案。Everything is tradeoff。

我们注意到 JSON Schema 是递归描述的,解析的函数也是递归的。更好的方案是将解析,转换这类 CPU 密集型任务转移到单独的线程,或者交给性能更高的工具来做(如:WebAssembly)。在后续的文章中,我们会分析大数据量下 JSON Schema 渲染的性能,以及探索用 Web Worker,Wasm 来处理 CPU 密集型任务相比于目前的纯 JS 方案能带来多大的提升,敬请期待。

引用链接

[1] json schema spec:json-schema.org/

[2] draft-07 标准:json-schema.org/specificati…

[3] draft-07 (and draft-06):ajv.js.org/guide/schem…)

关于全象云

全象云平台(portal.clouden.io)是青云科技自主研发的低代码平台,是基于云原生、用于辅助构建企业各类数字化应用的工具和集成平台。

平台目前提供云上无代码和低代码两种应用开发模式,屏蔽了技术的复杂度。支持可视化设计器,让开发人员和业务用户能够通过简单的拖拽、参数配置等方式快速完成应用开发。同时集成了 IDaaS 身份认证能力、容器 DevOps 能力,支持企业存量业务与全象云业务融合。平台还包含丰富的开发接口和强大的插件机制,开发者可根据需要不断拓展平台的应用能力。

全象云的愿景是:在企业生产经营的各个象限、各个环节提供软件构件或支持服务。 file

本文由博客一文多发平台 OpenWrite 发布!