有没有那么一种表单方案,上手成本低、与日常开发习惯类似,还可以低成本的使用现有组件进行编排?
本文介绍一种轻量的表单配置方案,重头戏还是原生组件的沉淀,FormRender 作为胶水做表单的编排。它提供了简单表达式联动,表单数据双向绑定等便捷的能力,源码量也比较少约 8k,易读性比较高,魔改源码不是梦!
最后我们还对比了 Formily 和 FormRender 在实现一个简单联动场景的差异,供大家参考~
https://xrender.fun/form-render
一站式中后台 表单解决方案

目录
schema 以国际标准的 JSON schema 为基础,同时能够方便使用任何 antd 的 props
通过 bind 字段,允许数据的双向绑定
使用 {{...}} 书写表达式来完成简单的联动,值得一提的是,这里表达式支持所有 js 语法

代码行:8172
文件数:56

formData
rootValue
示例:https://xrender.fun/playground 最简样例修改
- {{JSON.stringify(formData)}}
- {{JSON.stringify(rootValue)}}

示例:https://xrender.fun/playground 最简样例修改,实现字段联动展示隐藏
- {
- "type": "object",
- "properties": {
- "checkbox1": {
- "title": "展示更多内容",
- "type": "boolean"
- },
- "select1": {
- "title": "请假原因",
- "type": "string",
- "enum": ["a", "b", "c"],
- "enumNames": ["病假", "有事", "其它 (需注明具体原因)"],
- "hidden": "{{formData.checkbox1 !== true}}",
- "widget": "radio"
- },
- "input1": {
- "title": "具体原因",
- "type": "string",
- "format": "textarea",
- "hidden": "{{rootValue.checkbox1 !== true || formData.select1 !== 'c'}}"
- }
- }
- }
前端一个组件对应后端多个字段
- const schema = {
- type: 'object',
- properties: {
- "dateRange": {
- "title": "日期范围",
- "type": "range",
- "format": "date",
- "bind": ["startDate", "endDate"]
- },
- },
- };


见示例:https://xrender.fun/playground 个性选择框
widget 选择组件
Props 传参
- import { Cascader } from 'antd';
-
- // 顶层引入注册 ...
- <Form form={form} schema={schema} widgets={{ cascader: Cascader }} />
-
- // schema 中使用
- location: { title: '省市区', type: 'string', widget: 'cascader', props: { ... } },
示例1:https://xrender.fun/form-render/demos

- const schema = {
- "type": "object",
- "displayType": "row",
- "properties": {
- "select1": {
- "title": "输入框",
- "type": "string",
- "dependencies": ["useSelect"],
- "widget": "MyTextEditor",
- "width": "60%"
- },
- "useSelect": {
- "title": "输入框高度",
- "type": "number",
- "width": "60%"
- }
- }
- };
-
- const MyTextEditor = props => {
- const { addons } = props;
- console.log(addons.dependValues);
- let rows;
- if (addons && addons.dependValues) {
- rows = addons.dependValues[0] || 2;
- }
- return <TextArea rows={rows} />;
- };
示例2:https://xrender.fun/form-render/demos/index2
自定义 MyCheckbox 组件,通过 setValueByPath 方法修改下方多选值
Dependencies 传入 boxes 值,同步修改全选状态

示例:https://xrender.fun/form-render/advanced/watch
- form.setSchemaByPath('obj1.select1', {
- enum: ['east', 'south', 'west', 'north'],
- enumNames: ['东', '南', '西', '北']
- });
- const Demo = () => {
- const form = useForm();
- const watch = {
- // # 为全局
- '#': val => {
- console.log('表单的实时数据为:', val);
- },
- input1: val => {
- form.setValueByPath('input2', val);
- },
- input2: () => {
- // 动态修改枚举值
- form.setSchemaByPath('x[].y.z', { enum: [1, 2, 3] }); }
- };
- return <FormRender form={form} schema={schema} watch={watch} />;
- };
示例:https://xrender.fun/form-render/demos/index4

编辑态:

详情态:

| readOnly | 只读模式,一般用于预览展示,全文 text 展示 | boolean | FALSE |
自定义组件中,通过 readOnlyWidget 属性指定展示态组件
- {
- "type": "object",
- "properties": {
- "string": {
- "title": "网址输入自定义组件",
- "type": "string",
- "widget": "input",
- "readOnlyWidget": "label"
- }
- }
- }
示例:https://xrender.fun/generator/playground

下面我们自己定义一个组件,并定制表单设计器
- import { PageContainer } from '@ant-design/pro-components';
- import { message, Button } from 'antd';
- import Generator from 'fr-generator';
-
- const Demo = () => {
-
- const NewWidget = ({ value = 0, onChange, ...elesProps}) => {
- console.log('songzc-elesProps', elesProps);
- return <Button type='primary'
- {...elesProps}
- onClick={() => {
- message.success(value + 1);
- onChange(value + 1);
- }}>
- {value}
- Button>;
- };
- // {{JSON.stringify(formData)}}
- return (
- <PageContainer
- header={{
- title: '表单设计器',
- }}
- >
- <div style={{ height: '90vh' }}>
- <Generator
- widgets={{ NewWidget }}
- settings={[
- {
- title: '组件分类111',
- widgets: [
- {
- text: '计数器',
- name: 'asyncSelect',
- schema: {
- title: '计数器',
- type: 'number',
- widget: 'NewWidget',
- },
- setting: {
- title: { title: '标题', type: 'string' },
- props: {
- type: 'object',
- properties: {
- // colorP: { title: '颜色', type: 'string' },
- disabled: { title: '禁用', type: 'boolean' },
- ghost: { title: '幽灵', type: 'boolean' },
- }
- }
- },
- }
- ],
- },
- ]}
- commonSettings={{
- description: {
- title: '自定义共通用的入参',
- type: 'string',
- },
- }}
- />
- div>
- PageContainer>
- );
- };
-
- export default Demo;
通过正则匹配,首先判断是否为表达式
通过 Function 转为函数,注入 formData、rootValue 全局变量

- const Demo = () => {
- const form = useForm();
- const watch = {
- // # 为全局
- '#': val => {
- console.log('表单的实时数据为:', val);
- },
- input1: val => {
- form.setValueByPath('input2', val);
- },
- };
- return <FormRender form={form} schema={schema} watch={watch} />;
- };
监听 value 变化,执行 watch 内部函数

1. core.js 判断 schema 类型

2. 渲染不同类型元素

3. 递归调用渲染

使用 FormRender 和 Formily 实现同一个场景:计算总价
https://codesandbox.io/s/ji-ben-shi-yong-antd-4-21-6-forked-3dek61?file=/demo.js

- const App = () => {
- const [form] = Form.useForm();
- const [price, setPrice] = useState();
- const [count, setCount] = useState();
- useEffect(() => {
- if (price && count) {
- form.setFieldsValue({ total: price * count });
- }
- }, [price, count, form]);
-
- return (
- <Form form={form}>
- <Form.Item label="单价" name="price">
- <InputNumber onChange={value => setPrice(value)} />
- Form.Item>
- <Form.Item label="数量" name="count">
- <InputNumber onChange={value => setCount(value)} />
- Form.Item>
- <Form.Item label="总价" name="total">
- <InputNumber />
- Form.Item>
- Form>
- );
- };
https://codesandbox.io/s/pensive-surf-ssrl4w?file=/src/App.js

- export default () => {
- const form = useMemo(() => createForm(), [])
-
- return (
- <Form form={form} labelCol={6} wrapperCol={12}>
- <SchemaField>
- <SchemaField.Number
- title="单价"
- x-decorator="FormItem"
- x-component="NumberPicker"
- x-validator={[]}
- name="price"
- x-index={0}
- />
- <SchemaField.Number
- title="数量"
- x-decorator="FormItem"
- x-component="NumberPicker"
- x-validator={[]}
- name="count"
- x-index={1}
- />
- <SchemaField.String
- title="总价"
- x-decorator="FormItem"
- x-component="Input"
- x-validator={[]}
- name="total"
- x-index={2}
- x-pattern="readPretty"
- x-component-props={{}}
- x-reactions={{
- //拿出同级的price与count数据,取乘积
- dependencies: ['.price', '.count'],
- when: '{{$deps[0] && $deps[1]}}',
- fulfill: {
- state: {
- value: '{{$deps[0] * $deps[1]}}',
- },
- },
- }}
- />
- SchemaField>
-
- Form>
- )
- }

- const schema = {
- "type": "object",
- "properties": {
- "price": {
- "title": "单价",
- "type": "number"
- },
- "count": {
- "title": "数量",
- "type": "number"
- },
- "total": {
- "title": "总价",
- "type": "string",
- "props": {
- "value": "{{formData.price * formData.count}}"
- }
- }
- },
- "labelWidth": 120,
- "displayType": "row"
- }
- <Form
- schema={schema}
- widgets={{ site: SiteInput }}
- //...
- />
|
| Ant Form | FormRender | Formily |
| 上手难度 | 中 (日常开发使用较为熟悉) | 低 (理念与 Ant Form类似, 组件 props 设计相似) | 高 (性能原因重新设计了依赖关系和写法) |
| 成熟度 | 高 | 中 | 高 |
| 性能 | 中 | 中 | 高 |
设计理念以使用者角度考虑较多
formData 全局变量,简化数据监听、处理成本
{{}} 表达式使用方式简单
组件接入成本低,符合 FormItem 受控规范(value/onChange)即可
Props 传参,符合日常开发经验,理解成本低
Rules 校验,支持正则和 validator,与 ANTD 一致
使用及改造难度低
代码量目前较小,框架级改造容易
项目中沉淀组件的接入成本低
特殊参数较少,符合国际schema 规范:https://json-schema.org/understanding-json-schema/
局部表单场景适用,可通过 value 与外部原生代码交互
- <Form.Item label="测试" name="partForm">
- <FormRender
- form={form}
- schema={schema}
- onFinish={onFinish}
- widgets={{ MyCheckbox, DatePicker }}
- />
- Form.Item>
- <Form.Item label="测试" name="partForm">
- <FormRender
- form={form}
- schema={schema}
- onFinish={onFinish}
- widgets={{ MyCheckbox, DatePicker }}
- />
- Form.Item>
https://xrender.fun/form-render【问卷】XRender 使用场景 · Issue #94 · alibaba/x-render
https://github.com/alibaba/x-render/issues/94