重構:從 0.1 構建一個 Vue 表單驗證插件

工程諺語:若是它沒壞,就不要動它。html

Published: 2019-03-19vue

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

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

爲何用進行重構

重構以前,heregit

數據流

緣由:github

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

重構以後的結構:element-ui

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

業務場景下的基礎用法


本章用例,here設計模式

首先在全局安裝插件app

import validator from "fat-validator";

Vue.use(validator);
複製代碼

以後以 element-ui 的組件庫爲例,建立一個表單ide

<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}`]
        )
    }
},
複製代碼

總結


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

相關文章
相關標籤/搜索