草庐IT

JSON Schema&表单UI快速生成解析

Qlly 2023-04-05 原文

一、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.jsonschemavalidator.net/s/rrRbDzHF

应用

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,整体流程如下:

​ 如何选择合适的库?如是基于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/generator/playground。同时还支持由开发者自定义一些扩展组件,提供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。

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

有关JSON Schema&表单UI快速生成解析的更多相关文章

  1. Ruby 解析字符串 - 2

    我有一个字符串input="maybe(thisis|thatwas)some((nice|ugly)(day|night)|(strange(weather|time)))"Ruby中解析该字符串的最佳方法是什么?我的意思是脚本应该能够像这样构建句子:maybethisissomeuglynightmaybethatwassomenicenightmaybethiswassomestrangetime等等,你明白了......我应该一个字符一个字符地读取字符串并构建一个带有堆栈的状态机来存储括号值以供以后计算,还是有更好的方法?也许为此目的准备了一个开箱即用的库?

  2. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  3. ruby - i18n Assets 管理/翻译 UI - 2

    我正在使用i18n从头开始​​构建一个多语言网络应用程序,虽然我自己可以处理一大堆yml文件,但我说的语言(非常)有限,最终我想寻求外部帮助帮助。我想知道这里是否有人在使用UI插件/gem(与django上的django-rosetta不同)来处理多个翻译器,其中一些翻译器不愿意或无法处理存储库中的100多个文件,处理语言数据。谢谢&问候,安德拉斯(如果您已经在ruby​​onrails-talk上遇到了这个问题,我们深表歉意) 最佳答案 有一个rails3branchofthetolkgem在github上。您可以通过在Gemfi

  4. ruby-on-rails - rails : "missing partial" when calling 'render' in RSpec test - 2

    我正在尝试测试是否存在表单。我是Rails新手。我的new.html.erb_spec.rb文件的内容是:require'spec_helper'describe"messages/new.html.erb"doit"shouldrendertheform"dorender'/messages/new.html.erb'reponse.shouldhave_form_putting_to(@message)with_submit_buttonendendView本身,new.html.erb,有代码:当我运行rspec时,它失败了:1)messages/new.html.erbshou

  5. ruby-on-rails - 由于 "wkhtmltopdf",PDFKIT 显然无法正常工作 - 2

    我在从html页面生成PDF时遇到问题。我正在使用PDFkit。在安装它的过程中,我注意到我需要wkhtmltopdf。所以我也安装了它。我做了PDFkit的文档所说的一切......现在我在尝试加载PDF时遇到了这个错误。这里是错误:commandfailed:"/usr/local/bin/wkhtmltopdf""--margin-right""0.75in""--page-size""Letter""--margin-top""0.75in""--margin-bottom""0.75in""--encoding""UTF-8""--margin-left""0.75in""-

  6. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  7. ruby-on-rails - Rails 编辑表单不显示嵌套项 - 2

    我得到了一个包含嵌套链接的表单。编辑时链接字段为空的问题。这是我的表格:Editingkategori{:action=>'update',:id=>@konkurrancer.id})do|f|%>'Trackingurl',:style=>'width:500;'%>'Editkonkurrence'%>|我的konkurrencer模型:has_one:link我的链接模型:classLink我的konkurrancer编辑操作:defedit@konkurrancer=Konkurrancer.find(params[:id])@konkurrancer.link_attrib

  8. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  9. ruby - 用逗号、双引号和编码解析 csv - 2

    我正在使用ruby​​1.9解析以下带有MacRoman字符的csv文件#encoding:ISO-8859-1#csv_parse.csvName,main-dialogue"Marceu","Giveittohimóhe,hiswife."我做了以下解析。require'csv'input_string=File.read("../csv_parse.rb").force_encoding("ISO-8859-1").encode("UTF-8")#=>"Name,main-dialogue\r\n\"Marceu\",\"Giveittohim\x97he,hiswife.\"\

  10. ruby-on-rails - 如何从 format.xml 中删除 <hash></hash> - 2

    我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为

随机推荐