文章目錄
- 場景
- 思考
-
- Mixin
- 具體代碼
-
- 目錄結構
- validator相關代碼
- FormItem相關
- 按需引用
- 使用
- 總結
場景
ElementUI
的
Form
表單元件自帶的校驗規則是不是有點少,通過
yarn.lock
查詢
ElementUI
得知校驗使用了
async-validator
依賴
閱讀
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位址' }]
}
}
},
實作效果如下:(左:失敗、右:成功)
思考
通過閱讀
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)
之前注冊自己想要的類型,便可擴充。
我們拎個預設類型
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
。
這麼實作的好處:
- 不用拷貝整個代碼;
- 元件和複用元件之間互不影響,并以恰當方式合并;
- 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>
寫法還是和原來一樣,卻多了自定義校驗規則,豈不美哉。
總結
通過源碼解讀,一步一步摸索出來的,新人創作不易,求點贊關注給點動力。