一、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...
应用
一个基于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。
应用
- vue-json-schema-form:基于 vue.js 和JSON Schema 渲染form,最新版本已经支持Vue3
- form-render:基于react.js的表单解决方案,最新版本使用Ant Design作为视觉主题
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/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.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。
<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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。