设计理念
代码设计和渲染的库和解法非常多,因为这里确实很痛。大家都想找一个省去重复劳动的解决方案,同时大家又都怀着千人千面的现实需求,这其实是一个“被诅咒的问题”:要便于使用就要减少配置量,要自由定制就要增加配置量。这样的问题硬解是解不来的,在不断的对接各种业务平台、不断的摸索中通过取舍、限制和牺牲,换来一个即便于使用,也支持定制的方案,一个我们自己愿意使用的方案:使用 JSON 配置来生成页面,可以减少页面开发工作量,极大提升效率。
极简 api
<CustomComponent value={this.state.value} onChange={this.onChange} />
以此基础最简版的 form-render 的 api
<SchemaRender
schema={schema}
formData={this.state.value}
onChange={this.onChange}
/>
其中 schema 用于描述表单的 UI 必不可少, formData 就是 value。这就是最简版可用的 SchemaRender 了!
import React, { useState } from 'react';
import { SchemaRender } from "~renderer";
const schema = {
type: 'object',
properties: {
string: {
title: '字符串',
type: 'string',
},
select: {
title: '单选',
type: 'string',
enum: ['a', 'b', 'c'],
enumNames: ['选项1', '选项2', '选项3'],
},
},
};
const Demo = () => {
const [formData, setFormData] = useState({});
return (
<SchemaRender schema={schema} formData={formData} onChange={setFormData} />
);
};
export default Demo;
复杂场景 Demo
const schema_json = {
type: "object",
properties: {
case1: {
title: "基础控件",
type: "object",
displayType: "column",
labelWidth: 110,
properties: {
input: {
title: "简单输入框",
type: "string",
displayType: "row",
required: true,
options: {
placeholder: "请输入"
}
},
email: {
title: "邮箱输入",
description: "邮箱格式验证",
type: "string",
displayType: "row",
format: "email",
required: true,
pattern: "^[.a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(.[a-zA-Z0-9_-]+)+$"
},
...
};
设计思路
根据自定义组件类型定义type解析关系
const widgets = {
checkbox,
checkboxes,
color,
date,
dateRange,
input
multipleSelect,
number,
radio,
select,
slider,
switch,
textarea,
html,
...
};
灵活处理映射关系mapping
const mapping = {
default: "input",
string: "input",
boolean: "switch",
integer: "number",
number: "number",
object: "map",
html: "html",
size: "size",
select: "select",
"date:dateTime": "date",
"string:upload": "upload
"string:date": "date",
"string:dateTime": "date",
"string:time": "date",
"string:textarea": "textarea",
"string:color": "color",
"range:date": "dateRange",
"range:dateTime": "dateRange",
...
};
通过高阶函数包装自定义组件
import React from "react";
// High order component
export default function fetcher(FieldComponent) {
return class extends React.Component {
render() {
return <FieldComponent {...this.props} />;
}
};
}
解析json配置生成单元组件
import React, { useRef, useMemo, useEffect, useImperativeHandle } from "react";
// todo: schemaParser解析core配置
function RenderField({ fields, onChange, ...settings }) {
const { Field, props } = schemaParser(settings, fields);
if (!Field) {
return null;
}
return <Field isRoot {...props} value={settings.data} onChange={onChange} formData={settings.formData} />;
}
/**
* @param generated 根据 Widget 生成的 Field
* @param customized 自定义的 Field
* @param mapping 字段 type 与 widgetName 的映射关系
*/
function FieldRender({
className = "",
name = "$Field",
schema = {},
formData = {},
widgets = {},
FieldUI = DefaultFieldUI,
fields = {},
mapping = {},
onChange = () => {},
forwardedRef
}) {
const originWidgets = useRef();
const generatedFields = useRef({});
const rootData = useMemo(() => schemaResolve(schema, formData), [schema, formData]);
// data修改比较常用,所以放第一位
const resetData = (newData, newSchema) => {
const _schema = newSchema || schema;
const _formData = newData || formData;
const res = schemaResolve(_schema, _formData);
return new Promise((resolve) => {
onChange(res);
resolve(res);
});
};
useImperativeHandle(forwardedRef, () => ({
resetData
}));
// 用户输入都是调用这个函数
const handleChange = (key, val) => {
onChange(val);
};
const generated = useMemo(() => {
let obj = {};
if (!originWidgets.current) {
originWidgets.current = widgets;
}
Object.keys(widgets).forEach((key) => {
const oWidget = originWidgets.current[key];
const nWidget = widgets[key];
let gField = generatedFields.current[key];
if (!gField || oWidget !== nWidget) {
if (oWidget !== nWidget) {
originWidgets.current[key] = nWidget;
}
gField = asField({ FieldUI, Widget: nWidget });
generatedFields.current[key] = gField;
}
obj[key] = gField;
});
return obj;
}, []);
const settings = {
className,
name,
schema,
data: rootData,
formData
};
const fieldsProps = {
// 根据 Widget 生成的 Field
generated,
// 自定义的 Field
customized: fields,
// 字段 type 与 widgetName 的映射关系
mapping
};
return <RenderField {...settings} fields={fieldsProps} onChange={handleChange} />;
}
// todo: 外层容器同组件耦合抽离
const Wrapper = ({ schema, ...args }) => {
return <FieldRender schema={combineSchema(schema)} {...args} />;
};
export default Wrapper;
如何做到容器支持无限级嵌套
import React, { useState } from "react";
// todo: getSubField处理子集对象或者数组形式的json解析
function SubComponent((p) {
return (
<>
{Object.keys(p.value).map((name) => {
return p.getSubField({
name,
value: p.value[name],
rootValue: p.value,
onChange(key, val, objValue) {
let value = { ...p.value, [key]: val };
// 第三个参数,允许object里的一个子控件改动整个object的值
if (objValue) {
value = objValue;
}
p.onChange(p.name, value);
}
});
})}
</>
);
});
export default memo(SubComponent);