天天看點

Vue3引入Formily實踐實錄

調研

最近收到了一個任務,需要實作一個動态的表單。效果如下:

  • 圖一
Vue3引入Formily實踐實錄
  • 圖二
Vue3引入Formily實踐實錄

「主要邏輯」:第一個下拉框的資料能決定第二個下拉框的内容,第二個下拉框的内容是遠端加載的;第三個下拉框的内容能決定後續控件的形式;後面的加号是增加行,括号是增加括号,删除是删除這一行。最後的檢索式,是整體表單計算出來的。

「分析」:這個表單邏輯和關聯都比較複雜和頻繁,而且有些還是遠端擷取資料,并不是寫死的。如果說用elementplus的元件來做,處理邏輯、關聯和資料回顯的時候都會比較麻煩,肯定不簡潔。我之前有做過一個小型的複雜表單關聯,有過這種手動處理邏輯和關聯的麻煩經曆。是以我就決定不使用之前的方式處理現在的業務,需要尋找一個新的方式來解決這個需求。

「方案」:在這裡我就省去了找解決方案的過程,直接說答案,最後是決定使用Formily。原因有幾個:

  • 大廠出品:後續有不會的地方,能在網上搜得到
Vue3引入Formily實踐實錄
  • 生态豐富:涵蓋了vue和react,elementui、elementplus、antd等不同的版本
Vue3引入Formily實踐實錄

了解

我此次負責的項目的技術棧是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
           

我的目錄結構是這樣:

Vue3引入Formily實踐實錄

核心的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