Vue3引入Formily实践实录

此文章记录从调研、了解、熟悉、运用、延伸Formily的过程

调研

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

  • 图一

  • 图二

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

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

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

  • 大厂出品:后续有不会的地方,能在网上搜得到
  • 生态丰富:涵盖了vue和react,elementui、elementplus、antd等不同的版本

了解

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

  1. 「主站文档」首先是对Formily进行了解释,说明了Formily这个产品的定位和功能,其次的「场景案例」以及「进阶指南」的代码实例非常友好(案例使用react写的,使用vue的时候有一些差别)。读完此文档后对于Formily有了一定的了解;除此之外,主站中的API内容在后续实践中比较重要,也需要熟悉。

  2. 「Vue核心库」中,讲解了核心架构以及核心概念,核心概念中最重要的是三种开发模式:

  • Template 开发模式

    该模式主要是使用 Field/ArrayField/ObjectField/VoidField 组件

html 复制代码
<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 即可

html 复制代码
<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 元素
html 复制代码
<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 工具
json 复制代码
"sass": "^1.32.11",
"sass-loader": "^8.0.2"
  • 安装
ruby 复制代码
$ 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:

html 复制代码
<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:

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文档写得其实不是很明朗」

ok,就这样!下一篇文章再见

相关推荐
黑客老陈38 分钟前
新手小白如何挖掘cnvd通用漏洞之存储xss漏洞(利用xss钓鱼)
运维·服务器·前端·网络·安全·web3·xss
正小安44 分钟前
Vite系列课程 | 11. Vite 配置文件中 CSS 配置(Modules 模块化篇)
前端·vite
编程百晓君1 小时前
一文解释清楚OpenHarmony面向全场景的分布式操作系统
vue.js
暴富的Tdy1 小时前
【CryptoJS库AES加密】
前端·javascript·vue.js
neeef_se1 小时前
Vue中使用a标签下载静态资源文件(比如excel、pdf等),纯前端操作
前端·vue.js·excel
m0_748235611 小时前
web 渗透学习指南——初学者防入狱篇
前端
z千鑫1 小时前
【前端】入门指南:Vue中使用Node.js进行数据库CRUD操作的详细步骤
前端·vue.js·node.js
m0_748250742 小时前
Web入门常用标签、属性、属性值
前端