天天看點

❤️Vue混入(mixin)項目實戰: ElementUI Form表單擴充自定義校驗規則類型 (包含源碼解讀、詳細代碼)❤️場景思考具體代碼總結

文章目錄

  • 場景
  • 思考
    • Mixin
  • 具體代碼
    • 目錄結構
    • validator相關代碼
    • FormItem相關
    • 按需引用
    • 使用
  • 總結

場景

ElementUI

Form

表單元件自帶的校驗規則是不是有點少,通過

yarn.lock

查詢

ElementUI

得知校驗使用了

async-validator

依賴

❤️Vue混入(mixin)項目實戰: ElementUI Form表單擴充自定義校驗規則類型 (包含源碼解讀、詳細代碼)❤️場景思考具體代碼總結

閱讀

async-validator

相關文檔得知内置類型如下:

  • string: 必須是 string,預設類型;
  • number: 必須是 number;
  • boolean: 必須是 boolean;
  • method: 必須是 function;
  • regexp: 必須是正則或者是在調用 new RegExp 時不報錯的字元串;
  • integer: 必須是number類型且為整數;
  • float: 必須是number類型且為浮點數;
  • array: 必須是數組,通過 Array.isArray 判斷;
  • object: 必須是對象且不為數組;
  • enum: 值必須出現在 enum 枚舉值中;
  • date: 合法的日期,使用 Date 對象判斷;
  • url: url類型;
  • hex: 16進制;
  • email: 郵箱位址;

常用類型有

email

,卻沒

phone

,如何自定義規則類型呢,比如

ip

mac

phone

,且達到同樣的效果。

寫法如下:

computed: {
   rules() {
     const required = true;
     return {
       email: [{ required, type: 'email', message: '請輸入正确的郵箱' }],
       phone: [{ required, type: 'phone', message: '請輸入正确的手機号碼' }],
       url: [{ required, type: 'url', message: '請輸入正确的url' }],
       ip: [{ required, type: 'ip', message: '請輸入正确的IP位址' }],
       mac: [{ required, type: 'mac', message: '請輸入正确的MAC位址' }]
     }
   }
 },
           

實作效果如下:(左:失敗、右:成功)

❤️Vue混入(mixin)項目實戰: ElementUI Form表單擴充自定義校驗規則類型 (包含源碼解讀、詳細代碼)❤️場景思考具體代碼總結

思考

通過閱讀

ElementUI

FormItem

元件源碼,

FormItem

元件

validate

函數引用了

async-validator

,具體到下面

29

行代碼,它初始化了一個校驗器,若我們在初始化之前改造

AsyncValidator

函數是不是就能實作自定義類型擴充?

FormItem

核心源碼解讀:

// 路徑 node_modules\element-ui\packages\form\src\form-item.vue
import AsyncValidator from 'async-validator';
export default {
  methods: {
    // 核心校驗函數 trigger值為change、blur 預設change
    validate(trigger, callback = noop) {
    	// 開放校驗
        this.validateDisabled = false;
        // 過濾對應trigger的規則
        const rules = this.getFilteredRule(trigger);
        // 若無規則或無必填 回調并傳回
        if ((!rules || rules.length === 0) && this.required === undefined) {
          callback();
          return true;
        }
		// 設定目前校驗狀态:校驗中
        this.validateState = 'validating';
		// 以下代碼可參考async-validator使用文檔
        const descriptor = {};
        // 因為async-validator的rules沒有trigger屬性 删除每條規則的trigger 
        if (rules && rules.length > 0) {
          rules.forEach(rule => {
            delete rule.trigger;
          });
        }
        // 以FormItem目前prop屬性為鍵指派對應rules
        descriptor[this.prop] = rules;
		// 以descriptor為構造參數 初始化一個校驗器
        const validator = new AsyncValidator(descriptor);
        const model = {};
		// 擷取目前FormItem的對應字段值
        model[this.prop] = this.fieldValue;
		// 觸發表單校驗 firstFields: true 指當某一規則校驗失敗時,終止校驗;
        validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
          this.validateState = !errors ? 'success' : 'error';
          this.validateMessage = errors ? errors[0].message : '';
		  // 回調校驗資訊、不符合的字段
          callback(this.validateMessage, invalidFields);
          // 對應 Form Events 如果elForm存在,則每次觸發validate事件
          // 參數:被校驗的表單項 prop 值,校驗是否通過,錯誤消息(如果存在)
          this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
        });
      },
  }
}
           

通過閱讀

asycn-validator

官方文檔,使用例子如下,是不是很熟悉,

FormItem

元件

validate

函數照着官方例子進一步封裝,下面的

Schema

也就是上面的

AsyncValidator

,隻要弄懂

Schema

大概便能實作我們要的效果。

import Schema from 'async-validator';

const descriptor = {
  // name字段的校驗規則
  name: {type: "string", required: true},
};

const validator = new Schema(descriptor);

validator.validate({ name: "muji" }, (errors, fields) => {
  if (errors) {
    // 校驗不通過 do something
    return;
  }
  // 校驗通過 do something
});
           

通過閱讀

asycn-validator

源碼,

Schema

類開放了一個

register

注冊器,不過官方文檔并沒有這個API的介紹,推測是注冊校驗規則類型。

// 路徑node_modules\async-validator\es\index.js
Schema.register = function register(type, validator) {
  if (typeof validator !== 'function') {
    throw new Error('Cannot register a validator by type, validator is not a function');
  }
  validators[type] = validator;
};
           

追溯到

validators

,這裡存放了内置類型,驗證了剛才的設想,我們在

new AsyncValidator(descriptor)

之前注冊自己想要的類型,便可擴充。

❤️Vue混入(mixin)項目實戰: ElementUI Form表單擴充自定義校驗規則類型 (包含源碼解讀、詳細代碼)❤️場景思考具體代碼總結

我們拎個預設類型

string

的源碼觀摩一下:

// node_modules\async-validator\es\validator\string.js
import rules from '../rule/';
import { isEmptyValue } from '../util';

/**
 *  對string類型校驗.
 *
 *  @param rule 校驗規則.
 *  @param value 源對象字段的值.
 *  @param callback 回調函數.
 *  @param source 校驗後的源對象.
 *  @param options 校驗選項.
 *  @param options.messages 校驗資訊.
 */
function string(rule, value, callback, source, options) {
  var errors = [];
  var validate = rule.required || !rule.required && source.hasOwnProperty(rule.field);
  if (validate) {
    // 如果表單值為空 且不是必填 不校驗
    if (isEmptyValue(value, 'string') && !rule.required) {
      return callback();
    }
    // 校驗必填
    rules.required(rule, value, source, errors, options, 'string');
    if (!isEmptyValue(value, 'string')) {
      // 以下代碼為校驗邏輯 可換成自定義業務校驗
      // 校驗值類型
      rules.type(rule, value, source, errors, options);
      // 校驗是否滿足區間
      rules.range(rule, value, source, errors, options);
      // 校驗正則
      rules.pattern(rule, value, source, errors, options);
      if (rule.whitespace === true) {
        // 校驗空白字元
        rules.whitespace(rule, value, source, errors, options);
      }
    }
  }
  callback(errors);
}

export default string;
           

如果我們擴充一個

phone

類型,就要實作類似上面這個

string

校驗函數,

上面

26

36

行代碼可換成我們需要的校驗函數,通過

register

函數注冊,便注冊了一個

phone

類型。

校驗函數代碼如下:

function validator(rule, value, callback, source, options) {
  const errors = [];
  // 如果表單值為空 且不是必填 不校驗
  if (isEmptyValue(value, 'string') && !rule.required) {
    return callback();
  }
  // 校驗必填
  rules.required(rule, value, source, errors, options);
  // 手機号校驗
  if (!isEmptyValue(value, 'string') && !/^1\d{10}$/.test(value)) {
    errors.push('phone格式不正确');
  }
  return callback(errors);
}

           

上面

10

12

代碼是會根據業務變化的的,其餘代碼都是不變的,我們将校驗業務抽出封裝一下。

代碼如下(上下對比一下):

function validator(rule, value, callback, source, options) {
  const type = rule.type;
  const errors = [];
  // 如果表單值為空 且不是必填 不校驗
  if (isEmptyValue(value, 'string') && !rule.required) {
    return callback();
  }
  // 校驗必填
  rules.required(rule, value, source, errors, options);
  // 自定義規則校驗 ruleDict存放各種自定義的校驗規則
  if (!isEmptyValue(value, 'string') && !ruleDict[type](value)) {
    errors.push(format('%s 格式不正确', rule.fullField));
  }
  return callback(errors);
}
           

ruleDict

存放各個類型的校驗,比如

port

phone

等等。

部分代碼如下:

// ruleDict校驗規則
export default {
  // 端口号
  port(value) {
    return /^[1-9][0-9]*$/.test(value) && parseInt(value, 10) <= 65535;
  },
  // 手機号
  phone(value) {
    return /^1\d{10}$/.test(value);
  }
};
           

統一進行

register

,并将

Schema

吐出。

// 注冊validator
Object.keys(ruleDict).forEach(key => {
  Schema.register(key, validator);
});
export default Schema;
           

Schema

改造好了,那

FormItem

元件怎麼引用呢?這是就想到了我們把源碼整個

copy

一份不就好了?

這是一種辦法,不過還有更好的,本文主角——

混入(mixin)

;(總是最後出場)。

Mixin

混入

mixin

是Vue自帶的,用來複用Vue元件一種靈活方式。

mixin

對象可以包含任意元件選項,常見選項props、data、components、methods 、computed等等,還有常見的

生命周期鈎子

比如created、mounted。

不僅

mixin

對象群組件之間互不影響,而且當元件和

mixin

對象含有同名選項時,這些選項将以恰當的方式進行“合并”。

精簡混入代碼如下:

import { FormItem } from 'element-ui';
import { noop } from 'element-ui/src/utils/util';
// 引入擴充後的校驗器
import AsyncValidator from './validator/index';

export default {
  name: 'ElFormItem',
  // 混入一個FormItem元件
  mixins: [FormItem],
  methods: {
    // 核心校驗函數 trigger值為change、blur 預設change
    validate(trigger, callback = noop) {
      // 省略其餘代碼
      const validator = new AsyncValidator(descriptor);
      // 省略其餘代碼
    }
  }
};
           

如上代碼,隻要混入了

FormItem

元件,即可擁有其全部元件選項,即複制版

FormItem

元件,再重寫

methods

validate

函數即可引入我們擴充後的

AsyncValidator

這麼實作的好處:
  1. 不用拷貝整個代碼;
  2. 元件和複用元件之間互不影響,并以恰當方式合并;
  3. ElementUI元件更新,最多影響

    validate

    這一個函數;

最後再按需引用

FormItem

元件:

// 引入mixin後的FormItem元件
import FormItem from '@/components/Mixins/FormItem/index.js'
// 按需引用
Vue.component(FormItem.name, FormItem);
           

具體代碼

目錄結構

|-- components                  # 元件
        |   |-- Mixins                  # 混用元件
        |   |   |-- FormItem            
        |   |   |   |-- index.js        # mixin後的ElementUI FormItem元件
        |   |   |   |-- validator       # async-validator 校驗器相關
        |   |   |       |-- index.js    # 注冊自定義元件
        |   |   |       |-- rules.js    # 自定義校驗規則
           

validator相關代碼

// src\components\Mixins\FormItem\validator\rules.js
const patterns = {
  ip: /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/,
  mac: /^(([a-fA-F0-9]{2}[:-]?)){5}[a-fA-F0-9]{2}$/
};

export default {
  ip(value) {
    return patterns.ip.test(value);
  },
  port(value) {
    return /^[1-9][0-9]*$/.test(value) && parseInt(value, 10) <= 65535;
  },
  mac(value) {
    return patterns.mac.test(value);
  },
  phone(value) {
    return /^1\d{10}$/.test(value);
  }
};

           
// src\components\Mixins\FormItem\validator\index.js
import Schema from 'async-validator';
import rules from 'async-validator/lib/rule';
import { format, isEmptyValue } from 'async-validator/lib/util';
// 規則清單
import ruleList from './rules';

/**
 *  @param rule 校驗規則.
 *  @param value 源對象字段的值.
 *  @param callback 回調函數.
 *  @param source 校驗後的源對象.
 *  @param options 校驗選項.
 *  @param options.messages 校驗資訊.
 */
function validator(rule, value, callback, source, options) {
  const type = rule.type;
  const errors = [];
  // 如果表單值為空 且不是必填 不校驗
  if (isEmptyValue(value, 'string') && !rule.required) {
    return callback();
  }
  // 校驗必填
  rules.required(rule, value, source, errors, options);
  // 自定義規則校驗
  if (!isEmptyValue(value, 'string') && !ruleList[type](value)) {
    errors.push(format('%s 格式不正确', rule.fullField));
  }
  return callback(errors);
}

// 注冊validator
Object.keys(ruleList).forEach(key => {
  Schema.register(key, validator);
});

export default Schema;
           

FormItem相關

// src\components\Mixins\FormItem\index.js
import { FormItem } from 'element-ui';
import { noop } from 'element-ui/src/utils/util';
import AsyncValidator from './validator/index';

export default {
  name: 'ElFormItem',
  mixins: [FormItem],
  methods: {
    // 核心校驗函數 trigger值為change、blur 預設change
    validate(trigger, callback = noop) {
      // 開放校驗
      this.validateDisabled = false;
      // 過濾對應trigger的規則
      const rules = this.getFilteredRule(trigger);
      // 若無規則或無必填 回調并傳回
      if ((!rules || rules.length === 0) && this.required === undefined) {
        callback();
        return true;
      }
      // 設定目前校驗狀态:校驗中
      this.validateState = 'validating';
      // 以下代碼可參考async-validator使用文檔
      const descriptor = {};
      // 因為async-validator的rules沒有trigger屬性 删除每條規則的trigger
      if (rules && rules.length > 0) {
        rules.forEach(rule => {
          delete rule.trigger;
        });
      }
      // 以FormItem目前prop屬性為鍵指派對應rules
      descriptor[this.prop] = rules;
      // 以descriptor為構造參數 初始化一個校驗器
      const validator = new AsyncValidator(descriptor);
      const model = {};
      // 擷取目前FormItem的對應字段值
      model[this.prop] = this.fieldValue;
      // 觸發表單校驗 firstFields: true 指當某一規則校驗失敗時,終止校驗;
      validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
        this.validateState = !errors ? 'success' : 'error';
        this.validateMessage = errors ? errors[0].message : '';
        // 回調校驗資訊、不符合的字段
        callback(this.validateMessage, invalidFields);
        // 對應 Form Events 如果elForm存在,則每次觸發validate事件
        // 參數:被校驗的表單項 prop 值,校驗是否通過,錯誤消息(如果存在)
        this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
      });
    }
  }
};
           

按需引用

在main.js裡按需引用

// 引入mixin後的FormItem元件
import FormItem from '@/components/Mixins/FormItem/index.js'
// 按需引用
Vue.component(FormItem.name, FormItem);
           

使用

寫法還是和原來一樣如下:

<template>
	<el-form ref="form" :model="form" :rules="rules" label-width="120px">
	  <el-form-item label="ip" prop="ip">
	    <el-input v-model="form.ip" />
	  </el-form-item>
	  <el-form-item label="MAC" prop="mac">
	    <el-input v-model="form.mac" />
	  </el-form-item>
	</el-form>
</template>
<script>
export default {
  data() {
    return {
      form: {}
    }
  },
  computed: {
    rules() {
      const required = true;
      return {
        ip: [{ required, type: 'ip', message: '請輸入正确的IP位址', trigger: 'blur' }],
        mac: [{ required, type: 'mac', message: '請輸入正确的MAC位址', trigger: 'blur' }]
      }
    }
  },
}
</script>
           

寫法還是和原來一樣,卻多了自定義校驗規則,豈不美哉。

總結

通過源碼解讀,一步一步摸索出來的,新人創作不易,求點贊關注給點動力。