百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

Vue3引入Formily实践实录

toyiye 2024-04-05 10:51 30 浏览 0 评论

调研

最近收到了一个任务,需要实现一个动态的表单。效果如下:

  • 图一

  • 图二

「主要逻辑」:第一个下拉框的数据能决定第二个下拉框的内容,第二个下拉框的内容是远程加载的;第三个下拉框的内容能决定后续控件的形式;后面的加号是增加行,括号是增加括号,删除是删除这一行。最后的检索式,是整体表单计算出来的。

「分析」:这个表单逻辑和联动都比较复杂和频繁,而且有些还是远程获取数据,并不是写死的。如果说用elementplus的组件来做,处理逻辑、联动和数据回显的时候都会比较麻烦,肯定不简洁。我之前有做过一个小型的复杂表单联动,有过这种手动处理逻辑和联动的麻烦经历。所以我就决定不使用之前的方式处理现在的业务,需要寻找一个新的方式来解决这个需求。

「方案」:在这里我就省去了找解决方案的过程,直接说答案,最后是决定使用Formily。原因有几个:

  • 大厂出品:后续有不会的地方,能在网上搜得到

  • 生态丰富:涵盖了vue和react,elementui、elementplus、antd等不同的版本

了解

我此次负责的项目的技术栈是Vue3,所以我主要去了解了以下几个模块:

  • Fromily主站
  • Formily Vue核心库
  • Formily elementplus
  1. 「主站文档」首先是对Formily进行了解释,说明了Formily这个产品的定位和功能,其次的「场景案例」以及「进阶指南」的代码实例非常友好(案例使用react写的,使用vue的时候有一些差别)。读完此文档后对于Formily有了一定的了解;除此之外,主站中的API内容在后续实践中比较重要,也需要熟悉。
  2. 「Vue核心库」中,讲解了核心架构以及核心概念,核心概念中最重要的是三种开发模式:
  • Template 开发模式
  • 该模式主要是使用 Field/ArrayField/ObjectField/VoidField 组件
<template>
  <FormProvider :form="form">
    <Field name="input" :component="[Input, { placeholder:'请输入' }]" />
  </FormProvider>
</template>

<script>
  import { Input } from 'ant-design-vue'
  import { createForm } from '@formily/core'
  import { FormProvider, Field } from '@formily/vue'
  import 'ant-design-vue/dist/antd.css'

  export default {
    components: { FormProvider, Field },
    data() {
      return {
        Input,
        form: createForm(),
      }
    },
  }
</script>


  • JSON Schema 开发模式

该模式是给 SchemaField 的 schema 属性传递 JSON Schema 即可

<template>
   <FormProvider :form="form">
     <SchemaField :schema="schema" />
   </FormProvider>
 </template>

 <script>
   import { Input } from 'ant-design-vue'
   import { createForm } from '@formily/core'
   import { FormProvider, createSchemaField } from '@formily/vue'
   import 'ant-design-vue/dist/antd.css'

   const { SchemaField } = createSchemaField({
     components: {
       Input,
     },
   })

   export default {
     components: { FormProvider, SchemaField },
     data() {
       return {
         form: createForm(),
         schema: {
           type: 'object',
           properties: {
             input: {
               type: 'string',
               'x-component': 'Input',
               'x-component-props': {
                 placeholder: '请输入',
               },
             },
           },
         },
       }
     },
   }
 </script>


  • Markup Schema 开发模式
  • 该模式算是一个对源码开发比较友好的 Schema 开发模式,同样是使用 SchemaField 相关组件。
  • Markup Schema 模式主要有以下几个特点:
    • 主要依赖 SchemaStringField/SchemaArrayField/SchemaObjectField...这类描述标签来表达 Schema
    • 每个描述标签都代表一个 Schema 节点,与 JSON-Schema 等价
    • SchemaField 子节点不能随意插 UI 元素,因为 SchemaField 只会解析子节点的所有 Schema 描述标签,然后转换成 JSON Schema,最终交给RecursionField渲染,如果想要插入 UI 元素,可以在 SchemaVoidField 上传x-content属性来插入 UI 元素
<template>
  <FormProvider :form="form">
    <SchemaField>
      <SchemaStringField
        x-component="Input"
        :x-component-props="{ placeholder: '请输入' }"
      />
      <div>我不会被渲染</div>
      <SchemaVoidField x-content="我会被渲染" />
      <SchemaVoidField :x-content="Comp" />
    </SchemaField>
  </FormProvider>
</template>

<script>
  import { Input } from 'ant-design-vue'
  import { createForm } from '@formily/core'
  import { FormProvider, createSchemaField } from '@formily/vue'
  import 'ant-design-vue/dist/antd.css'

  const SchemaComponents = createSchemaField({
    components: {
      Input,
    },
  })

  const Comp = {
    render(h) {
      return h('div', ['我也会被渲染'])
    },
  }

  export default {
    components: { FormProvider, ...SchemaComponents },
    data() {
      return {
        form: createForm(),
        Comp,
      }
    },
  }
</script>


  1. 「Formily elementplus」

这个就类似于组件库,讲解具体组件如何使用。

熟悉

这个阶段熟悉了一些官网案例的用法。在三种使用模式中,最后选择了「JSON Schema」模式,这种模式组件看着更简洁,只需掌握配置规则。

运用

  • 因为 Element-Plus 是基于 Sass 构建的,如果你用 Webpack 配置请使用以下两个 Sass 工具
"sass": "^1.32.11",
"sass-loader": "^8.0.2"


  • 安装
$ npm install --save element-plus
$ npm install --save @formily/core @formily/vue @vue/composition-api @formily/element-plus


我的目录结构是这样:

核心的Formily代码在「filter.vue』中,JSON配置我提炼到了「form_obj.js」里

filter.vue:

<template>
  <FormProvider :form="form" class="lkkkkkkk">
    <SchemaField :schema="schema" :scope="{ useAsyncDataSource, loadData }" />
    <div class="btn flex flex-right">
      <div class="btn-inner">
        <el-button type="primary" plain :disabled="valid" @click="saveFilter">保存</el-button>
        <el-button type="primary" plain @click="resetFilter">重置</el-button>
        <Submit plain @submit-failed="submitFailed" @submit="submit">查询</Submit>
      </div>
    </div>
  </FormProvider>
</template>

<script setup>
  import { createForm } from '@formily/core';
  import { FormProvider, createSchemaField } from '@formily/vue';
  import {
    Submit,
    FormItem,
    Space,
    Input,
    Select,
    DatePicker,
    ArrayItems,
    InputNumber,
  } from '@formily/element-plus';
  import conditionResult from './conditionResult.vue';
  import { onMounted, ref } from 'vue';
  import { action } from '@formily/reactive';
  import { getFormObj, arrToText } from './form_obj';
  import { setLocal, getLocal } from '@/utils';

  const { SchemaField } = createSchemaField({
    components: {
      FormItem,
      Space,
      Input,
      Select,
      DatePicker,
      ArrayItems,
      InputNumber,
      conditionResult,
    },
  });

  const form = createForm();
  const schema = ref();
  const fieldMap = new Map();
  const valid = ref(true);

  const fieldCollect = (arr) => {
    arr.forEach((item) => {
      fieldMap.set(item.value, item);
    });
  };
  // 模拟远程加载数据
  const loadData = async (field) => {
    const table = field.query('.table').get('value');
    if (!table) return [];
    return new Promise((resolve) => {
      setTimeout(() => {
        if (table === 1) {
          const arr = [
            {
              label: 'AAA',
              value: 'aaa',
            },
            {
              label: 'BBB',
              value: 'ccc',
            },
          ];
          resolve(arr);
          fieldCollect(arr);
        } else if (table === 2) {
          const arr = [
            {
              label: 'CCC',
              value: 'ccc',
            },
            {
              label: 'DDD',
              value: 'ddd',
            },
          ];
          resolve(arr);
          fieldCollect(arr);
        }
      }, 1000);
    });
  };

  // 远程数据处理
  const useAsyncDataSource = (service) => (field) => {
    field.loading = true;
    service(field).then(
      action.bound((data) => {
        field.dataSource = data;
        field.loading = false;
      })
    );
  };

  // 获取表数据
  const getTables = () => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve([
          { label: '就诊信息表', value: 1 },
          { label: '诊断信息表', value: 2 },
        ]);
      }, 1000);
    });
  };

  // 初始化表单
  const initForm = async () => {
    const tables = await getTables();
    schema.value = getFormObj(tables);
    const originFilter = getLocal('formily');
    // 设置初始值或者是回显值
    if (originFilter) {
      form.setInitialValues(originFilter);
      // form.setInitialValues({
      //   array: [
      //     {
      //       table: 1,
      //       field: '',
      //       condition: 'contain',
      //       text: '',
      //       relationship: 'none',
      //       bracket: 'none',
      //     },
      //   ],
      //   escape: '',
      // });
    }
  };

  // 保存
  const saveFilter = () => {
    setLocal('formily', form.values);
  };
  // 重置
  const resetFilter = () => {
    form.setValues(getLocal('formily'));
  };
  // 查询
  const submit = (values) => {
    // 将数组转换成中文释义。
    const sentence = arrToText(fieldMap, values.array);
    console.log(sentence);
    // 将值设置到检索式中
    form.setValuesIn('escape', sentence);
    valid.value = false;
  };
  const submitFailed = () => {
    valid.value = true;
  };
  onMounted(() => {
    initForm();
  });
</script>


form_obj.js:

...
// Formily配置
export function getFormObj(tables) {
  return {
    type: 'object',
    properties: {
      array: {
        type: 'array',
        'x-component': 'ArrayItems',
        'x-decorator': 'FormItem',
        title: '检索条件',
        items: {
          type: 'object',
          properties: {
            space: {
              type: 'void',
              'x-component': 'Space',
              properties: {
                sort: {
                  type: 'void',
                  'x-decorator': 'FormItem',
                  'x-component': 'ArrayItems.SortHandle',
                },
                table: {
                  type: 'string',
                  title: '信息表',
                  enum: tables,
                  required: true,
                  'x-decorator': 'FormItem',
                  'x-component': 'Select',
                  'x-component-props': {
                    style: {
                      width: '160px',
                    },
                  },
                },
                field: {
                  type: 'string',
                  title: '字段',
                  required: true,
                  // default: 1,
                  // enum: [
                  //   { label: '入院年龄', value: 1 },
                  //   { label: '主要诊断', value: 2 },
                  //   { label: '手术名称', value: 3 },
                  // ],
                  'x-decorator': 'FormItem',
                  'x-component': 'Select',
                  'x-component-props': {
                    style: {
                      width: '160px',
                    },
                  },
                  'x-reactions': ['{{useAsyncDataSource(loadData)}}'],
                },
                condition: {
                  type: 'string',
                  title: '条件',
                  required: true,
                  //   default: 'contain',
                  enum: conditionArr,
                  'x-decorator': 'FormItem',
                  'x-component': 'Select',
                  'x-component-props': {
                    style: {
                      width: '130px',
                    },
                  },
                },
                text: {
                  type: 'string',
                  required: true,
                  'x-decorator': 'FormItem',
                  'x-component': 'Input',
                  'x-reactions': [
                    {
                      dependencies: ['.condition'],
                      fulfill: {
                        state: {
                          visible: "{{$deps[0] === 'contain'}}",
                        },
                      },
                    },
                  ],
                  'x-component-props': {
                    style: {
                      width: '160px',
                    },
                    placeholder: '请选择',
                  },
                },
                range: {
                  type: 'object',
                  properties: {
                    space: {
                      type: 'void',
                      'x-component': 'Space',
                      properties: {
                        start: {
                          type: 'number',
                          required: true,
                          'x-reactions': `{{(field) => {
                            field.selfErrors =
                              field.query('.end').value() <= field.value ? '左边必须小于右边' : ''
                          }}}`,
                          //   default: 1,
                          'x-decorator': 'FormItem',
                          'x-component': 'InputNumber',
                          'x-component-props': {
                            style: {
                              width: '150px',
                            },
                            placeholder: '左临界数值',
                          },
                        },
                        end: {
                          type: 'number',
                          required: true,
                          //   default: 10,
                          'x-decorator': 'FormItem',
                          'x-component': 'InputNumber',
                          'x-reactions': {
                            dependencies: ['.start'],
                            fulfill: {
                              state: {
                                selfErrors: "{{$deps[0] >= $self.value ? '左边必须小于右边' : ''}}",
                              },
                            },
                          },
                          'x-component-props': {
                            style: {
                              width: '150px',
                            },
                            placeholder: '右临界数值',
                          },
                        },
                      },
                    },
                  },
                  'x-reactions': [
                    {
                      dependencies: ['.condition'],
                      fulfill: {
                        state: {
                          visible: "{{$deps[0] === 'range'}}",
                        },
                      },
                    },
                  ],
                },
                relationship: {
                  type: 'string',
                  title: '关系',
                  required: true,
                  //   default: '',
                  enum: relationArr,
                  'x-decorator': 'FormItem',
                  'x-component': 'Select',
                  'x-component-props': {
                    style: {
                      width: '160px',
                    },
                  },
                },
                bracket: {
                  type: 'string',
                  title: '括号',
                  required: true,
                  //   default: '',
                  enum: bracketArr,
                  'x-decorator': 'FormItem',
                  'x-component': 'Select',
                  'x-component-props': {
                    style: {
                      width: '130px',
                    },
                  },
                },
                copy: {
                  type: 'void',
                  'x-decorator': 'FormItem',
                  'x-component': 'ArrayItems.Copy',
                },
                remove: {
                  type: 'void',
                  'x-decorator': 'FormItem',
                  'x-component': 'ArrayItems.Remove',
                },
              },
            },
          },
        },
        properties: {
          add: {
            type: 'void',
            title: '添加条目',
            'x-component': 'ArrayItems.Addition',
          },
        },
      },
      // properties: {
      // },
      escape: {
        type: 'string',
        title: '检索式',
        'x-component': 'conditionResult',
        'x-decorator': 'FormItem',
      },
    },
  };
}
...


此案例的完整代码,我放在我的github了,有需要自取。

延伸

完成这个案例之后,后续还有一个类似的表单需求,我本来是准备用这个来做的,但是这个需求是要放到IE上运行,所以我留了一个心眼。先写了一个小案例,测试了一下Vue2+elementui+Formily打包后在IE浏览器能否运行,最后发现是不可以。

后续再查资料中发现确实是不兼容IE的,大家在使用的时候要考虑这个场景。

总结

以上就是在Vue3中引入Formily解决需求的过程,经历了调研、了解、熟悉、运用的过程,Formily是一个比较好的表单处理工具,解决了表单联动、逻辑处理和回显的痛点,如果大家遇到此类需求,可以考虑一下使用这个工具,但是此工具不兼容IE的情况也要考虑进去。「吐槽一句就是,Formily文档写得其实不是很明朗」


作者:李仲轩
链接:https://juejin.cn/post/7312646198094086185

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码