Quickly generate and parse JSON Schema & form UI

一、JSON Schema

JSON (JavaScript Object Notation) is a lightweight & common data exchange format. The basic data structure is key-value, which has the advantage of being easy to generate and parse. JSON can flexibly express the data structure required by the program.

​ But JSON itself does not have a specific specification (the structure itself does not support comments), so the description of the data itself is missing. For example, developers or programs cannot judge whether the age in the following data is a string or not. expected type.

{
  "name": "John Doe",
  "mobile": "1370000001",
  "age": "30"
}
复制代码

​ JSON Schema defines a set of specifications that can describe JSON relatively completely. To describe the data structure we need based on the JSON Schema specification, or to develop programs based on this specification, the desired effect can be achieved.

scenes to be used:

1. Data verification

​ Probably the most common scenario of JSON Schema, whether it is front-end or back-end, there is a need to verify data, form verification, automated testing of CI/CD, etc.

Taking the above JSON as an example, if you want to specify age as number, and it must be less than or equal to 20, you can declare a JSON Schema like this

{
  "$schema": "http://json-schema.org/schema",
  "title": "Person",
  "description": "an example",
  "type": "object",
  "properties": {
    "name": {
      "type": "string"
    },
    "mobile": {
      "type": "string"
    },
    "age": {
      "type": "number",
      "maximum": 20
    },
  }
}
复制代码

​ Then when age is a string or a number, but it is not within the range, it will prompt the verification failure

​ Simple validation example: www.jsonschemavalidator.net/s/rrRbDzHF

application

Ajv.js

​ A verification library based on JSON Schema, commonly used for data verification in nodejs, browser, WeChat applet and other scenarios, by declaring a JSON Schema to quickly verify data without code development.

Example:

const Ajv = require('ajv');
const ajv = new Ajv();
// schema
const schema = {
  $schema: 'http://json-schema.org/schema',
  ...
};
const validate = ajv.compile(schema);
// 验证的数据
const validData = {
  ...
  age: '30',
};
const validResult = validate(validData);
if (validResult) {
  // 验证通过
  console.log('pass');
} else {
  // 验证不通过
  console.log(validate.errors);
  // [
  //   {
  //     keyword: 'type',
  //     dataPath: '.age',
  //     schemaPath: '#/properties/age/type',
  //     params: { type: 'number' },
  //     message: 'should be number'
  //   }
  // ]
}

复制代码

​ 不只是JavaScript/Typescript,其他编程语言也有基于JSON Schema实现的校验器,如Java的Snow 、go的gojsonschema和Python的jschon 等等都是基于此去开发的。所以通过JSON Schema规范,还可以保持前后端校验的一致。

2. form自动生成

​ JSON Schem虽然有规范约束,但仍然还是一份描述数据的JSON配置,那么基于这份配置,逻辑上就能自动渲染出功能完整的表单UI。

应用
  1. vue-json-schema-form:基于 vue.js 和JSON Schema 渲染form,最新版本已经支持Vue3

  2. form-render:基于react.js的表单解决方案,最新版本使用Ant Design作为视觉主题

  3. formily:跨端能力,逻辑可跨框架,主要模块(react.js+antd为例):

    @formily/core:实现状态管理、表单校验等逻辑,和UI无关

    @formily/react:实现交互效果,视图桥接

    @formily/antd:扩展组件库,开箱即用的表单UI

​ 以上表单渲染库都有提供对应的表单设计器,可以通过拖拽的形式快速生成JSON Schema,整体流程如下:

image.png

​ 如何选择合适的库?如是基于react.js的low code项目,那其实form-render就已经足够了。formily虽然支持的场景很多,但有一定的接入成本(从官方文档就能看出),而且包的体积也相对较大。如form-render,只需要引入form-render,然后正确传入props就能直接渲染出预期的UI。

​ 常见的在低代码平台中,都会有表单模块,但这部分的逻辑通常并不是整个低代码项目的核心,那就可以交由form-render这类表单渲染库去做,基于此就能减少开发单独维护表单映射或者校验的代码。当然也有可能需要开发部分定制的widgets,以适配于较为复杂或者更切合业务的情况。

二、form-render

​ from-render整体可以分为core和widgets。core实现了表单映射、校验和监听等等,widgets就是一些UI组件实现了。

core

映射

widgets包含了内置组件和扩展组件,内置的组件已经提供,基本包含在这里:x-render.gitee.io/generator/p…

// form-render-core/src/index.js
<ConfigProvider locale={zhCN} {...configProvider}>
	<FRCore widgets={{ ...defaultWidgets, ...widgets }} {...rest} />
</ConfigProvider>
复制代码

​ 如果要覆盖默认组件,可以使用mapping注册到form映射表内

  // form-render-core/src/index.js
  const tools = useMemo(
    () => ({
      widgets,
      mapping: { ...defaultMapping, ...mapping },
  ...
复制代码

​ 需要注意的是这里只是form映射表,同时还需要将自定义的widgets注册到表内。无论是内置或者扩展的组件都会,只要实现了一个基于映射表的getWidgetName方法就能获取到需要映射的组件名,渲染出对应的UI。

// form-render-core/src/core/RenderField/ExtendedWidget.js
// JSON Schema指定widget
let widgetName = getWidgetName(schema, mapping);
const customName = schema.widget || schema['ui:widget']; 
if (customName && widgets[customName]) {
  widgetName = customName;
}
const readOnlyName = schema.readOnlyWidget || 'html'; // 指定readOnly模式下的widget,或者使用默认html
if (readOnly && !isObjType(schema) && !isListType(schema)) {
  // 基础组件的readOnly会默认使用readOnlyName
  widgetName = readOnlyName;
}
if (!widgetName) {
  widgetName = 'input';
  return <ErrorSchema schema={schema} />;
}
const Widget = widgets[widgetName];
const extraSchema = extraSchemaList[widgetName];
...
复制代码
// form-render-core/src/core/RenderField/index.js
// 单属性UI最基础的内容
const RenderField = props => {
	...
  return (
    <>
      {_showTitle && titleElement}
      <div
        className={`${contentClass} ${hideTitle ? 'fr-content-no-title' : ''}`}
        style={contentStyle}
      >
        {/* Widget渲染 */}
        <ExtendedWidget {...widgetProps} />
        {/* 说明信息 */}
        <Extra {...widgetProps} />
        {/* ErrorMessage,校验相关 */}
        <ErrorMessage {...messageProps} />
      </div>
    </>
  );
}
复制代码

校验

​ 需要实现两个基础的校验方法,validateSingle(单属性校验)和validateAll(表单校验),具体的校验逻辑可以通过一些开源工具去实现,如form-render使用的是async-validator作为校验工具,async-validator是一个表单异步校验的工具,Ajv.js也可以异步校验,只需要初始化的时候带上schema内带上{$async: true}

Ajv.js async-validator
server 支持 支持
client 支持 支持
同步校验 支持 不支持
异步校验 支持 支持
package size 119.6 kb 14.2kb

​ 多数情况下的表单校验都会选择异步执行,所以包括form-render这类表单渲染库,或者一些开源组件库(如element)会使用async-validator作为校验工具。

// form-render-core/src/core/RenderField/index.js
const validateSingle = (data, schema = {}, path, options = {}) => {
	...
  /**
   * getDescriptorSimple会转换成匹配async-validator的数据结构,如果是其他的校验工具,可能就是另一种转换了
   * 以path为key,rules为value,和result的[path]: data是对应的
   */
  const descriptor = getDescriptorSimple(schema, path);
  let validator;
  try {
    // 校验
    validator = new Validator(descriptor);
  } catch (error) {
    return Promise.resolve();
  }
  // 错误提示的模板 type number string
  let messageFeed = locale === 'en' ? en : cn;
  merge(messageFeed, validateMessages);
  validator.messages(messageFeed);
  return validator
    .validate({ [path]: data })
    .then(res => {
      return [{ field: path, message: null }];
    })
    .catch(({ errors, fields }) => {
    	// 
      return errors;
    });
};
复制代码

​ validateAll只需要基于validateSingle遍历完成校验即可。validateSingle除了作为validateAll的一部分,同时也会在validateField中使用,为单个属性实时校验使用。

  const onChange = value => {
    // 节流、表单方法等
    ...
    validateField({
      path: dataPath, // 路径
      formData: formDataRef.current, // 表单数据
      flatten, // schema 的转换结构,[path]: {parent, children, schema}
      options: {
        locale,
        validateMessages,
      },
    })
    ...
  };
复制代码

​ 只是有校验是不够的,最重要的是同时要提示数据校验不通过的原因,所以还需要实现message动态模板,以及ErrorMessage组件承载错误提示。如form-render,实现了validateMessageCN.js作为message模板,ErrorMessage.js作为错误提示组件。

监听

​ 数据监听常见于低代码的场景中,预期是希望用户输入对应的属性后,能实时在渲染器响应,同步渲染UI。form-render提供了watch属性,用于数据的监听的唤起回调。

// form-render-core/src/Watcher.js
	...
  /**
   * formData当前表单的数据,watchKey被监听的key
   * getValueByPath主要是处理#和普通的key
   * 如果是#,返回的就是formData
 
   */
  const value = getValueByPath(formData, watchKey);
  // callback
  const watchObj = watch[watchKey];

  useEffect(() => {
    const runWatcher = () => {
      if (typeof watchObj === 'function') {
        try {
          // 执行回调函数,并把value传递到外层
          watchObj(value);
        } catch (error) {
          console.log(`${watchKey}对应的watch函数执行报错:`, error);
        }
      } else if (watchObj && typeof watchObj.handler === 'function') {
        try {
          // 适配多个参数的情况,其实目前的话,主要是handler和immediate
          watchObj.handler(value);
        } catch (error) {
          console.log(`${watchKey}对应的watch函数执行报错:`, error);
        }
      }
    };

    if (firstMount) {
      const immediate = watchObj && watchObj.immediate;
      if (immediate) {
        // 如果immediate为true,会在首次加载的时候触发一次watch
        runWatcher();
      }
    } else {
      runWatcher();
    }
  ...
复制代码

​ 需要注意的是,存在对象或者数组嵌套的情况,getValueByPath也需要有根据path来获取value的能力。如form-render是通过lodash-es模块的get方法来实现的。

​ 通过watch映射表构建多个watch实例。

...
{
  {/* watchList = Object.keys(watch) */}
  watchList.length > 0
  ? watchList.map((item, idx) => {
    	{/* null */}
      return (
        <Watcher
          key={idx.toString()}
          watchKey={item}
          watch={watch}
          formData={formData}
          firstMount={firstMount}
        />
      );
    })
  : null
}
...
复制代码

widgets

​ widgets主要是包含了内置组件,部分组件是直接使用了组件库提供的组件,如TextArea、InputNumber等,这些组件只需要调整下样式就能直接用于表单渲染了;但大部分组件都是经过封装后再使用的,如Slider、Color和Date组件等,不同的组件封装的逻辑不同,比如Slider包含了组件库的Slider和InputNumber,并对schema做解构,构建成对应的props。

**form-render自定义组件:**input, checkbox, checkboxes, color, date, time, dateRange, timeRange, imageInput, url, list, map, multiSelect, radio, select, slider, switch, upload, html, rate

**form-render组件库组件:**number, textarea, treeSelect

Guess you like

Origin juejin.im/post/7084132766933057550