• FormRender使用场景及原理简介


    有没有那么一种表单方案,上手成本低、与日常开发习惯类似,还可以低成本的使用现有组件进行编排?

    本文介绍一种轻量的表单配置方案,重头戏还是原生组件的沉淀,FormRender 作为胶水做表单的编排。它提供了简单表达式联动,表单数据双向绑定等便捷的能力,源码量也比较少约 8k,易读性比较高,魔改源码不是梦!

    最后我们还对比了 Formily 和 FormRender 在实现一个简单联动场景的差异,供大家参考~

    https://xrender.fun/form-render

    一站式中后台 表单解决方案

      

    目录

    一、背景介绍

    1.1 设计理念

    1.2 代码量

    二、现有能力

    2.1 数据双向绑定

    2.2 表达式联动

    2.3 字段映射

    2.4 自定义组件

    2.5 dependency 联动

    2.6 动态修改局部 schema

    2.7 watch 表单监听

    2.9 自定义校验

    2.10 详情与编辑态转换

    2.11 表单设计器

    三、代码分析

    3.1 表达式解析

    3.2 Watch 监听

    3.3 如何解析 schema

    四、方案对比

    4.1 Ant Form 实现

    4.2 Formily 实现

    4.3 FormRender 实现

    五、总结

    参考文档


    一、背景介绍

    1.1 设计理念

    • 通过 bind 字段,允许数据的双向绑定

    • 使用 {{...}} 书写表达式来完成简单的联动,值得一提的是,这里表达式支持所有 js 语法

     

    1.2 代码量

    • 代码行:8172

    • 文件数:56

    二、现有能力

    2.1 数据双向绑定

    • formData

    • rootValue

    示例:https://xrender.fun/playground 最简样例修改

    1. {{JSON.stringify(formData)}}
    2. {{JSON.stringify(rootValue)}}

    2.2 表达式联动

    示例:https://xrender.fun/playground 最简样例修改,实现字段联动展示隐藏

     
    
    1. {
    2. "type": "object",
    3. "properties": {
    4. "checkbox1": {
    5. "title": "展示更多内容",
    6. "type": "boolean"
    7. },
    8. "select1": {
    9. "title": "请假原因",
    10. "type": "string",
    11. "enum": ["a", "b", "c"],
    12. "enumNames": ["病假", "有事", "其它 (需注明具体原因)"],
    13. "hidden": "{{formData.checkbox1 !== true}}",
    14. "widget": "radio"
    15. },
    16. "input1": {
    17. "title": "具体原因",
    18. "type": "string",
    19. "format": "textarea",
    20. "hidden": "{{rootValue.checkbox1 !== true || formData.select1 !== 'c'}}"
    21. }
    22. }
    23. }

    2.3 字段映射

    前端一个组件对应后端多个字段

    1. const schema = {
    2. type: 'object',
    3. properties: {
    4. "dateRange": {
    5. "title": "日期范围",
    6. "type": "range",
    7. "format": "date",
    8. "bind": ["startDate", "endDate"]
    9. },
    10. },
    11. };

    2.4 自定义组件

     

    见示例:https://xrender.fun/playground 个性选择框

    • widget 选择组件

    • Props 传参

    1. import { Cascader } from 'antd';
    2. // 顶层引入注册 ...
    3. <Form form={form} schema={schema} widgets={{ cascader: Cascader }} />
    4. // schema 中使用
    5. location: { title: '省市区', type: 'string', widget: 'cascader', props: { ... } },

    2.5 dependency 联动

    示例1:https://xrender.fun/form-render/demos

    1. const schema = {
    2. "type": "object",
    3. "displayType": "row",
    4. "properties": {
    5. "select1": {
    6. "title": "输入框",
    7. "type": "string",
    8. "dependencies": ["useSelect"],
    9. "widget": "MyTextEditor",
    10. "width": "60%"
    11. },
    12. "useSelect": {
    13. "title": "输入框高度",
    14. "type": "number",
    15. "width": "60%"
    16. }
    17. }
    18. };
    19. const MyTextEditor = props => {
    20. const { addons } = props;
    21. console.log(addons.dependValues);
    22. let rows;
    23. if (addons && addons.dependValues) {
    24. rows = addons.dependValues[0] || 2;
    25. }
    26. return <TextArea rows={rows} />;
    27. };

    示例2:https://xrender.fun/form-render/demos/index2

    • 自定义 MyCheckbox 组件,通过 setValueByPath 方法修改下方多选值

    • Dependencies 传入 boxes 值,同步修改全选状态

    2.6 动态修改局部 schema

    示例:https://xrender.fun/form-render/advanced/watch

    1. form.setSchemaByPath('obj1.select1', {
    2. enum: ['east', 'south', 'west', 'north'],
    3. enumNames: ['东', '南', '西', '北']
    4. });

    2.7 watch 表单监听

    1. const Demo = () => {
    2. const form = useForm();
    3. const watch = {
    4. // # 为全局
    5. '#': val => {
    6. console.log('表单的实时数据为:', val);
    7. },
    8. input1: val => {
    9. form.setValueByPath('input2', val);
    10. },
    11. input2: () => {
    12. // 动态修改枚举值
    13. form.setSchemaByPath('x[].y.z', { enum: [1, 2, 3] }); }
    14. };
    15. return <FormRender form={form} schema={schema} watch={watch} />;
    16. };

    2.9 自定义校验

    示例:https://xrender.fun/form-render/demos/index4

    2.10 详情与编辑态转换

    • 编辑态:

    • 详情态:

    readOnly只读模式,一般用于预览展示,全文 text 展示booleanFALSE
    • 自定义组件中,通过 readOnlyWidget 属性指定展示态组件

    1. {
    2. "type": "object",
    3. "properties": {
    4. "string": {
    5. "title": "网址输入自定义组件",
    6. "type": "string",
    7. "widget": "input",
    8. "readOnlyWidget": "label"
    9. }
    10. }
    11. }

    2.11 表单设计器

    示例:https://xrender.fun/generator/playground

    下面我们自己定义一个组件,并定制表单设计器

    1. import { PageContainer } from '@ant-design/pro-components';
    2. import { message, Button } from 'antd';
    3. import Generator from 'fr-generator';
    4. const Demo = () => {
    5. const NewWidget = ({ value = 0, onChange, ...elesProps}) => {
    6. console.log('songzc-elesProps', elesProps);
    7. return <Button type='primary'
    8. {...elesProps}
    9. onClick={() => {
    10. message.success(value + 1);
    11. onChange(value + 1);
    12. }}>
    13. {value}
    14. Button>;
    15. };
    16. // {{JSON.stringify(formData)}}
    17. return (
    18. <PageContainer
    19. header={{
    20. title: '表单设计器',
    21. }}
    22. >
    23. <div style={{ height: '90vh' }}>
    24. <Generator
    25. widgets={{ NewWidget }}
    26. settings={[
    27. {
    28. title: '组件分类111',
    29. widgets: [
    30. {
    31. text: '计数器',
    32. name: 'asyncSelect',
    33. schema: {
    34. title: '计数器',
    35. type: 'number',
    36. widget: 'NewWidget',
    37. },
    38. setting: {
    39. title: { title: '标题', type: 'string' },
    40. props: {
    41. type: 'object',
    42. properties: {
    43. // colorP: { title: '颜色', type: 'string' },
    44. disabled: { title: '禁用', type: 'boolean' },
    45. ghost: { title: '幽灵', type: 'boolean' },
    46. }
    47. }
    48. },
    49. }
    50. ],
    51. },
    52. ]}
    53. commonSettings={{
    54. description: {
    55. title: '自定义共通用的入参',
    56. type: 'string',
    57. },
    58. }}
    59. />
    60. div>
    61. PageContainer>
    62. );
    63. };
    64. export default Demo;

    三、代码分析

    3.1 表达式解析

    • 通过正则匹配,首先判断是否为表达式

    • 通过 Function 转为函数,注入 formData、rootValue 全局变量

    3.2 Watch 监听

    1. const Demo = () => {
    2. const form = useForm();
    3. const watch = {
    4. // # 为全局
    5. '#': val => {
    6. console.log('表单的实时数据为:', val);
    7. },
    8. input1: val => {
    9. form.setValueByPath('input2', val);
    10. },
    11. };
    12. return <FormRender form={form} schema={schema} watch={watch} />;
    13. };
    • 监听 value 变化,执行 watch 内部函数

    3.3 如何解析 schema

    1. core.js 判断 schema 类型

    2. 渲染不同类型元素

    3. 递归调用渲染

    四、方案对比

    使用 FormRender 和 Formily 实现同一个场景:计算总价

    4.1 Ant Form 实现

    https://codesandbox.io/s/ji-ben-shi-yong-antd-4-21-6-forked-3dek61?file=/demo.js

    1. const App = () => {
    2. const [form] = Form.useForm();
    3. const [price, setPrice] = useState();
    4. const [count, setCount] = useState();
    5. useEffect(() => {
    6. if (price && count) {
    7. form.setFieldsValue({ total: price * count });
    8. }
    9. }, [price, count, form]);
    10. return (
    11. <Form form={form}>
    12. <Form.Item label="单价" name="price">
    13. <InputNumber onChange={value => setPrice(value)} />
    14. Form.Item>
    15. <Form.Item label="数量" name="count">
    16. <InputNumber onChange={value => setCount(value)} />
    17. Form.Item>
    18. <Form.Item label="总价" name="total">
    19. <InputNumber />
    20. Form.Item>
    21. Form>
    22. );
    23. };

    4.2 Formily 实现

    • https://codesandbox.io/s/pensive-surf-ssrl4w?file=/src/App.js

    1. export default () => {
    2. const form = useMemo(() => createForm(), [])
    3. return (
    4. <Form form={form} labelCol={6} wrapperCol={12}>
    5. <SchemaField>
    6. <SchemaField.Number
    7. title="单价"
    8. x-decorator="FormItem"
    9. x-component="NumberPicker"
    10. x-validator={[]}
    11. name="price"
    12. x-index={0}
    13. />
    14. <SchemaField.Number
    15. title="数量"
    16. x-decorator="FormItem"
    17. x-component="NumberPicker"
    18. x-validator={[]}
    19. name="count"
    20. x-index={1}
    21. />
    22. <SchemaField.String
    23. title="总价"
    24. x-decorator="FormItem"
    25. x-component="Input"
    26. x-validator={[]}
    27. name="total"
    28. x-index={2}
    29. x-pattern="readPretty"
    30. x-component-props={{}}
    31. x-reactions={{
    32. //拿出同级的pricecount数据,取乘积
    33. dependencies: ['.price', '.count'],
    34. when: '{{$deps[0] && $deps[1]}}',
    35. fulfill: {
    36. state: {
    37. value: '{{$deps[0] * $deps[1]}}',
    38. },
    39. },
    40. }}
    41. />
    42. SchemaField>
    43. Form>
    44. )
    45. }

    4.3 FormRender 实现

    1. const schema = {
    2. "type": "object",
    3. "properties": {
    4. "price": {
    5. "title": "单价",
    6. "type": "number"
    7. },
    8. "count": {
    9. "title": "数量",
    10. "type": "number"
    11. },
    12. "total": {
    13. "title": "总价",
    14. "type": "string",
    15. "props": {
    16. "value": "{{formData.price * formData.count}}"
    17. }
    18. }
    19. },
    20. "labelWidth": 120,
    21. "displayType": "row"
    22. }
    23. <Form
    24. schema={schema}
    25. widgets={{ site: SiteInput }}
    26. //...
    27. />

    Ant Form

    FormRender

    Formily

    上手难度

    (日常开发使用较为熟悉)

    (理念与 Ant Form类似,

    组件 props 设计相似)

    (性能原因重新设计了依赖关系和写法)

    成熟度

    性能

    五、总结

    • 设计理念以使用者角度考虑较多

      • formData 全局变量,简化数据监听、处理成本

      • {{}} 表达式使用方式简单

      • 组件接入成本低,符合 FormItem 受控规范(value/onChange)即可

      • Props 传参,符合日常开发经验,理解成本低

      • Rules 校验,支持正则和 validator,与 ANTD 一致

    • 使用及改造难度低

      • 代码量目前较小,框架级改造容易

      • 项目中沉淀组件的接入成本低

      • 特殊参数较少,符合国际schema 规范:https://json-schema.org/understanding-json-schema/

      • 局部表单场景适用,可通过 value 与外部原生代码交互

    1. <Form.Item label="测试" name="partForm">
    2. <FormRender
    3. form={form}
    4. schema={schema}
    5. onFinish={onFinish}
    6. widgets={{ MyCheckbox, DatePicker }}
    7. />
    8. Form.Item>
    9. <Form.Item label="测试" name="partForm">
    10. <FormRender
    11. form={form}
    12. schema={schema}
    13. onFinish={onFinish}
    14. widgets={{ MyCheckbox, DatePicker }}
    15. />
    16. Form.Item>

    参考文档

    FormRender 开源第二弹:一站式表单解决方案 - 掘金本文旨在介绍 Form Render (FR)这个阿里开源表单解决方案。简述它想解决的问题,我们的理念和解法,这半年走过的路,以及 FR 的未来规划 所见即所得的表单设计器,拖拖拽拽完成表单的开发。并可自由定制成符合自身喜好的独特设计器 这个未来其实以近在眼前。FormRend…https://juejin.cn/post/6870316361726787591

    开始使用 - XRenderhttps://xrender.fun/form-render【问卷】XRender 使用场景 · Issue #94 · alibaba/x-renderhttps://github.com/alibaba/x-render/issues/94

  • 相关阅读:
    【Mybatis编程:统计相册表中的数据的数量】
    PMP真的有用吗?
    python random应用实例 从可选池随机选取指定个数的元素并随机排序
    上周热点回顾(12.4-12.10)
    MySQL(高级、面试) —— (MySQL优化 二)Explain 优化MySQL索引
    高数_证明_方向导数最大值方向为梯度方向
    微信小程序实战,基于vue2实现瀑布流
    解密prompt系列5. APE+SELF=自动化指令集构建代码实现
    七夕当然要学会SQL优化好早点下班去找对象
    Cmake、Qt与VS编译VTK(生成QVTK)
  • 原文地址:https://blog.csdn.net/zhichaosong/article/details/126146281