Vue表單生成器設計實踐

前言

在公司一直從事基於Vue框架後臺應用的前端研發,而該類應用的頁面有較多的通過表單交互來增刪改查的操作,爲了進行優雅的開發體驗,也有感於項目當前的代碼,遂封裝一個更合適的表單生成器form-generator.vue

稍有從業經驗的人都曉得,這類生成器在基於iviewant-design這樣的組件庫下實現不算複雜,因此這裏主要是闡述表單生成器的設計思路。

當前項目的表單生成操作有以下問題:

  1. 實現form生成功能的組件代碼僵硬,是通過v-if指令根據type字段來判斷要渲染哪個組件,相應的schema如下:

    filterList: {
        list: {
            mallName: {
                name: 'mallId',
                displayName: '商場',
                type: 'SELECTION',
                notNull: true,
                value: '',
                options: {
                    option: []
                }
            },
            billDate: {
                name: 'billDate',
                displayName: '賬單期',
                type: 'MONTH',
                notNull: false,
                value: '',
                options: {
                    option: []
                }
            }
            // ......
        }
    }
    

如果有其他類型的組件要渲染,則要更改當前的form結構數據和生成器實現組件中的v-if相關代碼。

  1. 可以看到,當前的form結構數據,將form的schema和相應的值value寫在同一個對象中,這樣,在將form結構數據寫在Vuex類似的狀態庫中時,造成form結構數據和實際使用的代碼組件的上下文的不統一。

  2. form結構的數據的構造較複雜,有很深的嵌套,代碼的可讀性不友好。

思路

Vue框架有內置組件component,鏈接如下。用法是:渲染一個“元組件”爲動態組件。依 is 的值,來決定哪個組件被渲染。通過component內置組件和is特性,我們可以很優雅的實現不同類型組件的分發渲染。這樣我們只需要通過一個特定的字段componentType指明當前要渲染的組件名,然後賦值給is。而不需要冗長的v-if相關代碼。

其次,我們將表單生成器的schema和表單最終存放的值一起存放到model中。

那麼,我們的表單生成器的結構就出來了。

{
    model: {
        mallId: '',
        billDate: ''
    },
    schema: {
        fields: [
          {
             name: 'mallId',
             displayName: '商場',
             componentType: 'Select',
             placeholder: '不限',
             required: true,
             trigger: 'change',
             propObj: {
               transfer: true,
               filterable: true,
               clearable: true
             },
             options: []
          },
          {
              name: 'billDate',
              displayName: '賬單期',
              componentType: 'DatePicker',
              placeholder: '請選擇時間',
              propObj: {
                  transfer: true,
                  type: 'month',
                  class: 'datePicker'
              }
          }
        ]
    }
}

在實際生產使用中,schema部分存放在Vuex狀態庫中,而model部分則存放在具體組件的data中。這樣做的考慮在於:類似Select組件的枚舉值是從服務端獲取到的,在接口請求到數據中當即將其設置到Vuex狀態庫中,而具體頁面中存放的model對象可能會用於頁面組件其他部分代碼的操作。當然,也可以將這個form數據結構一起存放到Vuex狀態庫中或者直接存放到具體組件的data中。

結構解讀

整個對象存放兩個屬性:model和schema。

  1. model對象用於存放最終和form交互到的值,form-generator.vue組件的model屬性即傳遞該值,並且使用了sync修飾符進行了數據的雙向綁定。

  2. schema對象主要的屬性是fields,該字段是一個數組,用來存放具體待渲染爲form組件的數據結構。

    1. 屬性clearVerifyMsg用來驅動表單進行全部組件DOM層次的重置。

    2. 屬性colProp用於每個表單組件外層包裹的ICol組件的柵格寬度控制,默認值爲{ lg: 8, md: 12 },及其其他ICol組件相關屬性。

    3. fields數組下的每一項的屬性如下:

      1. name:其值對應model中的字段名,在渲染的時候將model中的值雙向綁定到當前渲染的組件中。

      2. componentType:is屬性來使用,用來確定當前要渲染的組件名

      3. displayName:表單當前組件的label,會賦值給包裹着組件的FormItem外層組件的label屬性中。

      4. hide:使用v-if指令控制當前組件是否渲染

      5. placeholder:佔位符

      6. required:用在FormItem組件中,控制當前組件是否需要驗證

      7. propObj:使用v-bind="propObj"語法來設置當前組件的props

      8. options:當前待渲染組件爲Select時,該組件的子組件Option要渲染的列表

      9. optPropObj:當前待渲染組件爲Select時,子組件Option的屬性

      10. append:當前待渲染組件爲Input時,對應slot的值

      11. handler:在組件觸發事件時,對當前組件值的特定的處理

      12. trigger:控制當前組件在以什麼方式觸發值的校驗操作,默認爲blur

      13. validator:使用了組件庫對應的驗證功能,當值爲string時,爲該類型,當值爲object時,直接使用該值進行驗證,當值爲function時,爲自定義的驗證器

其他

該組件基於iview組件進行開發,會用到相應的組件和表單驗證功能。所以,該組件也可用於ant-design-vue類似的組件庫。

缺點

  1. 由於使用了component內置組件進行相應組件的渲染分發,所以,對組件的屬性定製不友好,每個item都需要傳入需要的props,一種解決方案是:對該組件做一次包裹,然後componentType屬性傳入包裹後的組件名。
  2. 每個itemon-change事件的回調函數的入參個數不一,且還需傳入原生的event對象,如何處理不同組件傳遞參數的問題還未很好處理。

具體代碼

<template>
  <IForm
    slot="from"
    ref="formValidate"
    :model="model"
    :rules="ruleValidate"
    :label-width="labelWidth"
    :inline="inline"
  >
    <template v-for="(item, idx) in fieldList">
      <ICol v-if="!item.hide" :key="idx" v-bind="colProp">
        <FormItem
          :label="item.displayName"
          :prop="item.name"
          :required="item.required"
        >
          <component
            :is="item.componentType"
            :ref="item.name"
            v-model.trim="model[item.name]"
            :placeholder="item.placeholder"
            v-bind="item.propObj"
            @on-change="(value, $event) => handleChange(value, $event, item)"
          >
            <template v-if="item.options">
              <Option
                v-for="(opt, i) in item.options"
                :key="i"
                :value="opt.value"
                :disabled="opt.disabled"
                v-bind="item.optPropObj"
                >{{ opt.label }}</Option
              >
            </template>
            <template v-if="item.append">
              <span slot="append" :style="item.appendStyle">{{
                item.append
              }}</span>
            </template>
          </component>
        </FormItem>
      </ICol>
    </template>
    <slot name="bottom">
      <div class="bottomBar" :style="bottomBarStyle">
        <Button type="primary" @click="handleSubmit('formValidate')">{{
          okText
        }}</Button>
        <Button
          v-if="hasReset"
          class="custom_btn"
          @click="handleReset('formValidate')"
          >重置</Button
        >
        <slot name="bottom-extra-btns" />
      </div>
    </slot>
  </IForm>
</template>
<script>
const isObject = obj =>
  Object.prototype.toString.call(obj).slice(8, -1) === 'Object'

export default {
  name: 'FormGenerator',
  props: {
    model: {
      type: Object,
      default() {
        return {}
      }
    },
    schema: {
      type: Object,
      default() {
        return {
          fields: []
        }
      },
      validator: function(obj) {
        return obj.hasOwnProperty('fields') && Array.isArray(obj.fields)
      }
    },
    inline: {
      type: Boolean,
      default: true
    },
    labelWidth: {
      type: [Number, String],
      default: 110
    },
    okText: {
      type: String,
      default: '查詢'
    },
    hasReset: {
      type: Boolean,
      default: true
    },
    bottomBarStyle: {
      type: Object,
      default() {
        return {}
      }
    },
    resetToDefaultValue: {
      type: [Boolean, Object],
      default: false
    }
  },
  data() {
    return {
      ruleValidate: {}
    }
  },
  computed: {
    fieldList() {
      return this.schema.fields
    },
    colProp() {
      const obj = isObject(this.schema.colProp) ? this.schema.colProp : {}

      return {
        lg: 8,
        md: 12,
        ...obj
      }
    }
  },
  watch: {
    'schema.clearVerifyMsg': function(newVal) {
      if (newVal) {
        this.$refs['formValidate'].resetFields()
      }
    }
  },
  created() {
    this.ruleInit()
  },
  methods: {
    handleSubmit(name) {
      this.$refs[name].validate(valid => {
        if (valid) {
          const result = JSON.parse(JSON.stringify(this.model))

          this.$emit('on-submit', result)
        }
      })
    },
    handleReset(name) {
      if (this.resetToDefaultValue) {
        const newValue = isObject(this.resetToDefaultValue)
          ? this.resetToDefaultValue
          : this.model

        this.$emit('update:model', newValue)
      } else {
        this.$refs[name].resetFields()
      }
      this.$emit('on-reset')
    },
    handleChange(value, evt, item) {
      const { handler } = item

      let verifiedValue = ''

      if (typeof handler === 'function') {
        verifiedValue = handler(value, item, evt)
      } else if (value instanceof InputEvent) {
        verifiedValue = value.target.value
      } else {
        verifiedValue = value
      }

      if (verifiedValue !== undefined) {
        this.$nextTick(() => {
          this.$set(this.model, item.name, verifiedValue)
          this.$emit('update:model', this.model)
        })
      }
    },
    ruleInit() {
      this.fieldList.forEach(
        ({ required, validator, name, trigger = 'blur' }) => {
          if (required) {
            this.$set(this.ruleValidate, name, [
              {
                trigger,
                required: true,
                message: '必填項,不能爲空'
              }
            ])
          } else if (validator) {
            if (typeof validator === 'string') {
              this.$set(this.ruleValidate, name, [
                {
                  trigger,
                  required: true,
                  type: validators,
                  message: `只能輸入${validator}類型`
                }
              ])
            } else if (typeof validator === 'function') {
              this.$set(this.ruleValidate, name, [{ trigger, validator }])
            } else if (
              isObject(validator) &&
              validator.hasOwnProperty('type')
            ) {
              this.$set(this.ruleValidate, name, [validator])
            } else {
              throw new Error(`字段${name}的屬性type的類型錯誤`)
            }
          } else {
            this.$set(this.ruleValidate, name, [])
          }
        }
      )
    }
  }
}
</script>
<style lang="less" scoped>
.bottomBar {
  text-align: right;
  margin-left: 108px;
}
</style>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章