重構:從 0.1 構建一個 Vue 表單驗證外掛

FatGe發表於2019-03-19

工程諺語:如果它沒壞,就不要動它。

Published: 2019-03-19

之前在開發中後臺業務時候,基於 Vue 寫了一個表單驗證的外掛,由於時間比較急,再加上看過的原始碼比較少,就草草的實現了。過年期間看了 Vuex 以及 Vue-router 的原始碼,對外掛的實現有了一定的瞭解,再加上年後公司在裁員,業務有些停滯了,所以抽了兩天把它重構一下,也就應了標題的從0.1開發。

  1. 為什麼要進行重構;
  2. 業務場景下的基礎用法;
  3. 具體結構變動以及實現;
  4. 總結。

為什麼用進行重構

重構之前,here

資料流

原因:

  • 錯用設計模式,導致程式碼耦合嚴重,在重構之前,維護了一個 eventHandler ,用於管理校驗規則與結果,卻沒有進行很好的管理;
  • 利用 Vue 的自定義指令 v-validat 將來傳遞校驗規則,實現方式繁瑣,且所有狀態結果都耦合在元件的 data 中,但是其龐大、不易維護;
  • 利用 context.$forceUpdate(),引入髒檢測,導致整體效率偏低;
  • 部分功能實現方式有問題。

重構之後的結構:

重構:從 0.1 構建一個 Vue 表單驗證外掛

  • 將校驗規則、結果維護在當前元件中,v-validate 指令,只是做為介質,傳遞校驗的 action、rule。

業務場景下的基礎用法


本章用例,here

首先在全域性安裝外掛

import validator from "fat-validator";

Vue.use(validator);
複製程式碼

之後以 element-ui 的元件庫為例,建立一個表單

<template>
  <div class="mock" v-if="isVisible">
    <div class="form-wrapper">
      <i class="el-icon-close close-btn" @click.stop="close"></i>

      <div class="header">
        <span>使用者註冊</span>
        <span></span>
      </div>

      <div class="content">
        <form-item title="姓名" info="姓名不能修改" :warn="validateResult.name">
          <el-input
            placeholder="請輸入內容"
            v-model="name"
            v-validate.input="'name'"
            @change="handleChange"
          />
        </form-item>
      </div>

      <div class="footer">
        <el-button type="primary" @click="handleClick({ type: 'confirm' })"
          >確定</el-button
        >

        <el-button type="primary" @click="handleClick({ type: 'reset' })"
          >重置姓名</el-button
        >
      </div>
    </div>
  </div>
</template>

<script>
import popupWin from "./popup-win.js";
import formItem from "../components/form-item";
// 引入mixin的元件validatorMixin
import { validatorMixin } from "fat-validator";

export default {
  mixins: [popupWin, validatorMixin],

  components: {
    formItem
  },

  data() {
    return {
      name: ""
    };
  },

  validator() {
    return {
      name: [
        {
          need: () => !!this.name,
          warn: "不能為空"
        },
        {
          need: () => this.name.length <= 20,
          warn: "不能超過20個字元"
        }
      ]
    };
  },

  methods: {
    handleClick({ type }) {
      const handler = {
        reset: () => this.$validator.reset("name"),
        confirm: () => {
          if (this.$validator.validateAll()) {
            this.$emit("done", name);
          }
        }
      };

      handler[type] && handler[type]();
    },
    handleChange() {
      this.$validator.validate("name");
    }
  }
};
</script>
複製程式碼

利用 v-validate.input="'name'",在元件上繫結指令,其中 input 代表校驗觸發時,所需要的事件,'name' 代表所屬的校驗規則

validator() {
    return {
        name: [
            {
                need: () => !!this.name,
                warn: "不能為空"
            },
            {
                need: () => this.name.length <= 20,
                warn: "不能超過20個字元"
            }
        ]
    };
}
複製程式碼

同時預設新增狀態 validateResult.name 代表校驗的結果

this.$validator 可以呼叫四個方法:

  • validate 用於驗證單個規則,引數是key,例如上述 v-validate.input="'name'", 可以寫為 @input="$validator.validate('name')"
  • reset 用於重置某個驗證結果,例如要重置上述驗證結果this.$validator.reset('name')
  • validateAll 用於驗證所有規則,例如this.$validator.validateAll()
  • resetAll 用於重置所有規則,例如 this.$validator.resetAll()

具體結構變動以及實現


本章程式碼,here

首先利用 mixins 對錶單進行擴充套件,將 validatorMixin 注入到表單元件中,主要完成兩個任務

  • 其一:將 validateResult mixin 到元件中,方便元件利用校驗結果來展示不同資訊;

    const validatorMixin = {
        data() {
            return {
                validateResult: {},
            }
        }
       	...
    }
    複製程式碼
  • 其二:對元件擴充套件,新增 $validator 物件,用於實現 validateresetvalidateAll 等方法;

    beforeCreate() {
        if (isDef(this.$options.validator)) {
            const { _uid, $options: { validator } } = this
            const _validator = validator.call(this)
            ...
            this.$validator = _validator
        }
    }
    複製程式碼

主要對第二點進行下介紹,將validatorMixin mixin 到元件中

// validatorMixin
{
    data() {
        return {
            validateResult: {},
        }
    },
    beforeCreate() {
        if (isDef(this.$options.validator)) {
            const { _uid, $options: { validator } } = this
            const _validator = validator.call(this)
            ...
            this.$validator = _validator
        }
    },
}
// 元件
{
    mixins: [validatorMixin],
    data() {
        return {
            name: ""
        };
    },
    validator() {
        return {
            name: [{
                    need: () => !!this.name,
                    warn: "不能為空"
                },
                {
                    need: () => this.name.length <= 20,
                    warn: "不能超過20個字元"
                }
            ]
        };
    },
}
複製程式碼

beforeCreate 生命週期中,對元件的 $options 進行訪問,獲取到當前元件的 _uid、自定義的 validator

利用 validator.call(this),將 validator 的 context 繫結在當前元件中,這樣方便後續利用 this 指標來獲取當前元件的 data,簡化驗證規則。

const _validator = validator.call(this)
// 定義propConfig,防止方法被 enum 以及 write
const propConfig = {
    writable: false,
    enumerable: false,
}
// init
Object.keys(_validator).forEach((key) => {
    this.$nextTick(() => {
        this.$set(this.validateResult, key, '')
    })
})
複製程式碼

之後利用 this.$set 對之前 mixin validateResult 物件進行初始化,使得每個校驗結果都變為響應式。

**PS:**為什麼利用 this.$nextTick ,是要在 mixin 、元件化完成再對 validateResult 進行修改。

Object.defineProperties(_validator, {
    validate: {
        value(key) {
            validatorEmmiter.emit(`${_uid}-${key}`)
        },
        ...propConfig,
    },
    reset: {
        value: (key) => {
            this.validateResult[key] = ''
        },
        ...propConfig,
    },
    validateAll: {
        value: () => {
            Object.keys(_validator).forEach((key) => {
                const haveListeners = (eventName) =>
                validatorEmmiter.listenerCount(eventName)
                if (haveListeners(`${_uid}-${key}`)) {
                    validatorEmmiter.emit(`${_uid}-${key}`)
                }
            })
            return Object.keys(this.validateResult).every(
                (item) => this.validateResult[item] === ''
            )
        },
        ...propConfig,
    },
    resetAll: {
        value: () => {
            Object.keys(_validator).forEach((key) => {
                this.validateResult[key] = ''
            })
        },
        ...propConfig,
    },
})
複製程式碼

之後利用 Object.defineProperties_validator 進行擴充套件,新增 validateresetvalidateAll 等方法,每個方法的邏輯都比較簡單,其中 validatorEmmiter 是用來管理校驗Action'的,接下後詳細介紹。

import events from 'events'

class ValidatorEmmiter extends events {
    constructor() {
        super()
    }
}

const validatorEmmiter = new ValidatorEmmiter()
validatorEmmiter.setMaxListeners(100)
複製程式碼

validatorEmmiter 的實現,特別簡易,利用了 node 的 events 模組

class ValidatorEmmiter extends events:為什麼要存在,方便後續對 ValidatorEmmiter 進行擴充套件,管理事件。

上述簡單介紹了注入元件的校驗結果模組,接下介紹如何傳遞校驗規則、校驗Action,與之前一致的是,依然利用指令 v-validate 傳遞校驗規則

具體形式如 v-validate.input="'name'",代表著元件觸發 input 事件時候進行校驗,校驗規則為 name

{
    install(Vue) {
        const eventHandler = {}

        Vue.directive('validate', {
            bind(el, binding, vnode) {
                const { modifiers, value: key } = binding
                const { context: { _uid } } = vnode
                const method = Object.keys(modifiers)[0]
                ...
            },
 			...unbind
        })
    },
}
複製程式碼

具體API見 Vue外掛,利用引數 (el, binding, vnode) 獲取上述的元件的 _uid,校驗規則的 key,校驗的Action method

之後利用 validatorEmmiter 進行訂閱與釋出,具體時機為

  • 指令開始進行 bind 時,也就是元件 render 時,validatorEmmiter
// on
validatorEmmiter.on(`${_uid}-${key}`, () => {
    const { context: { validateResult, $validator } } = vnode
    // 找到不滿足的 rule
    const result = $validator[key].find((item) => !item.need())

    validateResult[key] = isDef(result) ? result.warn : ''
})
// emit
if (method) {
    eventHandler[`${_uid}-${key}`] = () => {
        validatorEmmiter.emit(`${_uid}-${key}`)
    }
    // 使用者監聽元件的事件,來emit對應的規則
    vnode.componentInstance.$on(
        method,
        eventHandler[`${_uid}-${key}`]
    )
}
複製程式碼
  • 當元件 destroyed 時,會觸發對應指令的 unbind ,需要對已監聽的事件進行 remove 以及 $off
unbind: function(el, binding, vnode) {
    const { modifiers, value: key } = binding
    const { context: { _uid, $validator } } = vnode
    const method = Object.keys(modifiers)[0]

    // reset & remove event
    $validator.reset(key)
    validatorEmmiter.removeAllListeners(`${_uid}-${key}`)
    if (method) {
        vnode.componentInstance.$off(
            method,
            eventHandler[`${_uid}-${key}`]
        )
    }
},
複製程式碼

總結


這篇主要是用來總結之前重構知識的吧,還有就是看了一些原始碼,總要有產出的吧。

相關文章