一、JSON Schema

​ JSON(JavaScript Object Notation)是一种轻量&常见的数据交换格式,基本的数据的结构就是key-value,具有易于生成和解析的优点,通过JSON可以灵活地表达程序所需要的数据结构。

​ 但JSON本身并没有特定的规范(本身结构也不支持注释),所以对于数据本身的描述是缺失的,比如说开发人员或者程序,就无法判断下面这份数据里面的age为string是否是符合预期的类型。

{
  "name": "John Doe",
  "mobile": "1370000001",
  "age": "30"
}

​ JSON Schema定义了一套能够较为完整地来描述JSON的规范,基于JSON Schema的规范去描述我们所需要的数据结构,或者基于这种规范去开发程序,就能实现预期效果。

使用场景:

1. 数据校验

​ 可能是JSON Schema最常见的场景,无论是前端还是后台都有校验数据的需求,表单校验,CI/CD的自动化测试等等。

以上面的JSON为例,如果要规定age为number,并且必须小于等于20,那么可以这样声明一份JSON Schema

{
  "$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
    },
  }
}

​ 那么当age为string,或者为number,但不在范围内的时候都会提示校验失败

​ 简单的校验示例:https://www.jsonschemavalidat...

应用

Ajv.js

​ 一个基于JSON Schema的校验库,常用于nodejs、浏览器、微信小程序等场景的数据校验,通过声明一个JSON Schema来快速验证数据,而无需进行代码开发。

示例:

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包含了内置组件和扩展组件,内置的组件已经提供,基本包含在这里:https://x-render.gitee.io/gen...。同时还支持由开发者自定义一些扩展组件,提供props.widgets传入自定义的object就能把扩展form组件注册到widgets映射表内。

// 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.jsasync-validator
server支持支持
client支持支持
同步校验支持不支持
异步校验支持支持
package size119.6 kb14.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。

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dd3dbb93298e432daee11d2b07857266~tplv-k3u1fbpfcp-zoom-1.image" style="zoom:36%;" />

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


Qlly
12 声望0 粉丝