手寫el-form表單組件,再也不怕面試官問我form表單原理
前言
element-ui中Form組件的簡(jiǎn)單使用
源碼實(shí)現(xiàn)需求分析
校驗(yàn)方法
el-input組件實(shí)現(xiàn)
el-form-item組件實(shí)現(xiàn)
el-form組件實(shí)現(xiàn)
組件實(shí)現(xiàn)中遺留的問題
源碼解析
參考文獻(xiàn)

前言
最近在用elementUI的form表單組件的時(shí)候,在實(shí)現(xiàn)嵌套表單的校驗(yàn)的時(shí)候,遇到了一些困難,我想之所以困難的原因在于我對(duì)elementui里的form表單組件不夠熟悉,于是就深入了解了一下源碼,并嘗試自己去實(shí)現(xiàn)一個(gè)自己的form表單
歡迎關(guān)注《前端陽(yáng)光》,加入技術(shù)交流群,加入內(nèi)推群
element-ui中Form組件的簡(jiǎn)單使用
<template>
<el-form :model="info" :rules="rules" ref="forms" >
<el-form-item label="用戶名:" prop="userName">
<el-input v-model="info.userName" placeholder="請(qǐng)輸入用戶名"></el-input>
</el-form-item>
<el-form-item>
<el-input type="password" v-model="info.userPassword" placeholder="請(qǐng)輸入密碼"></el-input>
</el-form-item>
<el-form-item>
<button @click="save">提交</button>
</el-form-item>
</el-form>
<template\>
<script>
data() {
return {
info: {
userName:'',
userPassword:''
},
rules: {
userName: { required:true, message:'用戶名不能為空' },
userPassword: { required:true, message:'密碼不能為空' }
}
}
},
methods: {
save() {
this.$refs.forms.validate((result) => {
let message ='校驗(yàn)經(jīng)過';
if (!result) {
message ='校驗(yàn)未經(jīng)過';
}
alert(message)
}
}
</script>
首先要清楚一下組件的使用方式
el-form接收model和rule兩個(gè)prop
model表示表單綁定的數(shù)據(jù)對(duì)象 rule表示驗(yàn)證規(guī)則策略,表單驗(yàn)證
el-form-item接收的prop屬性,對(duì)應(yīng)form組件的model數(shù)據(jù)中某個(gè)key值,如果rule剛好有key,給定的條件下去如失去焦點(diǎn)驗(yàn)證規(guī)則匹不匹配。
也就是el-form-item要獲得model[prop]和rule[prop]兩個(gè)值,檢查 model[prop]是否符合rule[prop]設(shè)置的規(guī)則。
源碼實(shí)現(xiàn)需求分析
實(shí)現(xiàn)一個(gè)el-form組件,其中接受model與rules兩個(gè)props,而且開放一個(gè)驗(yàn)證方法validate,用于外部調(diào)用,驗(yàn)證組件內(nèi)容
實(shí)現(xiàn)一個(gè)el-form-item組件,其中接受label與prop兩個(gè)props。且在這里要注意的是el-form-item能夠做為中間層,鏈接el-form與el-form-item中的的slot,并進(jìn)行核心的驗(yàn)證處理,因此數(shù)據(jù)驗(yàn)證部分在這個(gè)組件中進(jìn)行。
實(shí)現(xiàn)一個(gè)el-input組件,實(shí)現(xiàn)雙向綁定,其中接受value與type兩個(gè)props
好了,分析完基本需求以后,下面咱們開干。npm
校驗(yàn)方法
咱們這里使用一個(gè)對(duì)數(shù)據(jù)進(jìn)行異步校驗(yàn)的庫(kù)async-validator,element-ui中也是使用的這個(gè)庫(kù)。
el-input組件實(shí)現(xiàn)
input組件中須要實(shí)現(xiàn)雙向綁定以及向上層el-form-item傳遞數(shù)據(jù)和通知驗(yàn)證。
// 雙向綁定的input本質(zhì)上實(shí)現(xiàn)了input而且接收一個(gè)value
// 這里涉及到的vue組件通訊為$attrs,接受綁定傳入的其余參數(shù),如placeholder等
<template>
<input :type="type" :value="value" @input="onInput" v-bind="$attrs" />
</template>
<script>
// 這里涉及到的vue組件通訊為provide/inject
export default {
props: {
value: {
type: String,
default: ‘’,
},
type: {
type: String,
default: 'text'
}
},
},
methods: {
onInput(e) {
this.$emit('input', e.target.value);
// 通知父元素進(jìn)行校驗(yàn) 使用this.$parent找到父元素el-form-item
this.$parent.$emit('validate');
}
}
</script>
el-form-item組件實(shí)現(xiàn)
el-form-item組件做為數(shù)據(jù)驗(yàn)證中間件,要接受el-form中的數(shù)據(jù),結(jié)合el-input中的數(shù)據(jù)根據(jù)el-form中的rules進(jìn)行驗(yàn)證,并進(jìn)行錯(cuò)誤提示
<template>
<div>
<label v-text="label"></label>
<slot></slot>
<p v-if="error" v-text="error"></p>
</div>
</template>
<script>
// 引入異步校驗(yàn)數(shù)據(jù)的庫(kù)
import Schema from 'async-validator';
// 這里涉及到的vue組件通訊為provide/inject
export default {
// 接收el-form組件的實(shí)例,方便調(diào)用其中的屬性和方法
inject: ['form'],
props: {
label: {
type: String,
default: '',
},
prop: {
type: String,
required: true,
default: ''
}
},
},
data() {
return {
// 錯(cuò)誤信息提示
error:''
}
},
mounted(){
// 監(jiān)聽校驗(yàn)事件
this.$on('validate', () => { this.validate() })
},
methods: {
// 調(diào)用此方法會(huì)進(jìn)行數(shù)據(jù)驗(yàn)證,并返回一個(gè)promise
validate() {
// this.prop為驗(yàn)證字段,如: userName
// 獲取驗(yàn)證數(shù)據(jù)value,如: userName的值
const value = this.form.model[this.prop];
// 獲取驗(yàn)證數(shù)據(jù)方法,如: { required:true, message:'用戶名不能為空' }
const rules = this.form.rules[this.prop];
// 拼接驗(yàn)證規(guī)則
const desc= { [this.prop]: rules };
// 實(shí)例化驗(yàn)證庫(kù)
const schema = new Schema(desc);
// 這里會(huì)返回一個(gè)promise
return schema.validate(
{ [this.prop]: value },
errors => {
if (errors) {
this.error = errors[0].message;
} else {
this.error = '';
}
}
)
}
}
</script>
el-form組件實(shí)現(xiàn)
咱們上面分析過el-form只須要接受props值,并開放一個(gè)驗(yàn)證方法validate判斷校驗(yàn)結(jié)果,而后把內(nèi)嵌的slot內(nèi)容展現(xiàn)出來,那么el-form實(shí)現(xiàn)就相對(duì)簡(jiǎn)單了
<template>
<div>
<slot></slot>
</div>
</template>
<script>
// 這里涉及到的vue組件通訊為provide/inject
export default {
// 由于上面需求分析提到,須要在form-item組件中進(jìn)行驗(yàn)證,因此要將form實(shí)例總體傳入form-item中,方便后續(xù)調(diào)用其方法和屬性
provide() {
return {
form: this
}
},
props: {
model: {
type:Object,
required:true,
default: () => ({}),
},
rules: {
type:Object,
default: () => ({})
}
},
},
methods: {
// 這是供外部調(diào)用的validate驗(yàn)證方法 接收一個(gè)回調(diào)函數(shù) 把驗(yàn)證結(jié)果返回出去
validate(callback) {
// 使用this.$children找到全部el-form-item子組件,獲得的值為一個(gè)數(shù)組,并調(diào)用子組件中的validate方法并獲得Promise數(shù)組
const tasks = this.$children
.filter(item => item.prop)
.map(item => item.validate());
// 全部任務(wù)必須所有成功才算校驗(yàn)經(jīng)過,任一失敗則校驗(yàn)失敗
Promise.all(tasks)
.then(() => callback(true))
.catch(() => callback(false))
}
}
</script>
到這里Form組件的構(gòu)建基本就結(jié)束了,這里涉及到的Vue組件通訊有不少,學(xué)習(xí)這部分源碼能很大程度上的幫助咱們理解Vue中組件通訊的機(jī)制以及提高咱們的編程能力。
組件實(shí)現(xiàn)中遺留的問題
實(shí)現(xiàn)到這步其實(shí)還不能徹底放心,這個(gè)組件還不夠健壯。由于在組件源碼中還有一些處理在這里尚未提到。 若是在el-form組件中嵌套層級(jí)很深的話 this.$children可能拿到的并非el-form-item,一樣el-input的this.$parent拿到的也不是el-form-item,那這個(gè)問題要怎么處理呢?其實(shí)在vue 1.x中提供了兩個(gè)方法全局方法dispatch和boardcast,他能夠根據(jù)指定componentName來遞歸查找相應(yīng)組件并派發(fā)事件,在vue 2.x中這個(gè)方法被廢棄了??墒莈lement-ui以為這個(gè)方法有用,因而又把他實(shí)現(xiàn)了一遍,而且在解決上面這個(gè)問題中就可使用到,具體源碼以下:
const boardcast = function (componentName, eventName, params) {
this.$children.forEach(child => {
let name = child.$options.componentName;
if (componentName === name) {
child.$emit.apply(child, [eventName].concat(params));
} else {
boardcast.apply(child, [componentName, eventName].concat(params));
}
});
}
export default {
methods: {
// 向上尋找父級(jí)元素
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options.componentName;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
// 向下尋找子級(jí)元素
boardcast(componentName, eventName, params) {
boardcast.call(this, componentName, eventName, params);
}
}
};
使用mixin混入的方式,用這個(gè)方法對(duì)上面代碼組件代碼進(jìn)行改造,能夠解決查找父元素子元素的問題數(shù)
到這里,實(shí)際上已經(jīng)完成一個(gè)基本的表單了,當(dāng)然,element的表單功能是比這個(gè)強(qiáng)大得多的,例如el-form-item要獲得rule不止可以在el-form中獲取,也可以通過直接以props的方式傳入el-form-item,這時(shí)候,props獲得的rule就會(huì)覆蓋掉el-form的rule。
為了更全面的了解element的el-form表單是怎么實(shí)現(xiàn)的,為了提高我們的編程能力,建議看看el-form的源碼解析。
歡迎關(guān)注《前端陽(yáng)光》,加入技術(shù)交流群,加入內(nèi)推群
源碼解析
v-model, rules 和 ref
v-model 配合 prop 使用,對(duì)應(yīng)的是要校驗(yàn)字段的值(prop 一定是在 el-form-itme上面,在源碼部分會(huì)解釋為什么)
rule 和 prop 對(duì)應(yīng),指的是每個(gè)字段的校驗(yàn)規(guī)則(在源碼部分會(huì)解釋為什么)
ref 最后一步校驗(yàn)使用,和v-model 對(duì)應(yīng)
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px">
<el-form-item label="活動(dòng)名稱" prop="name">
<el-input v-model="ruleForm.name"></el-input>
</el-form-item>
<el-form-item label="年齡" prop="age">
<el-input v-model.number="ruleForm.age"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">立即創(chuàng)建</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
<script>
var checkAge = (rule, value, callback) => {
// rule => { validator: '', field: "score.0", fullField: "score.0", type: "string", max_age: '' ...}
// field 是 對(duì)應(yīng)props里面的值
// validator 是async-validator 里面的 validator(description)
// value 要校驗(yàn)的值
//console.log(rule.max_age)
if (!value) {
return callback(new Error('年齡不能為空'));
}
if (!Number.isInteger(value)) {
callback(new Error('請(qǐng)輸入數(shù)字值'));
} else {
if (value < rule.max_age) {
callback(new Error('必須年滿18歲'));
} else {
callback();
}
}
};
export default {
data() {
return {
ruleForm: {
name: '',
age:''
},
rules: {
name: [
{ required: true, message: '請(qǐng)輸入活動(dòng)名稱', trigger: 'blur', validator: function() {} },
{ min: 3, max: 5, message: '長(zhǎng)度在 3 到 5 個(gè)字符', trigger: 'blur', validator: function() {} }
],
age: [
{max_age:18, validator: checkAge, trigger: 'blur' }// checkAge自定義規(guī)則函數(shù)
]
}
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid, form) => {
// form 里面是校驗(yàn)沒通過的prop
if (valid) {
alert('submit!');
} else {
console.log('error submit!!')
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
}
}
</script>
也可以將校驗(yàn)規(guī)則寫在form上面
<el-form :model="numberValidateForm" ref="numberValidateForm" label-width="100px" class="demo-ruleForm">
<el-form-item
label="年齡"
prop="age"
:rules="[
{ required: true, message: '年齡不能為空'},
{ type: 'number', message: '年齡必須為數(shù)字值'}
]">
<el-input type="age" v-model.number="numberValidateForm.age" autocomplete="off"></el-input>
</el-form-item>
以至于循環(huán)使用也是沒有問題的
<el-form>
<el-form-item
v-for="(domain, index) in dynamicValidateForm.domains"
:label="'域名' + index"
:key="domain.key"
:prop="`domains.${index}.value`" //綁定的prop
:rules="[
{ required: true, message: '域名不能為空', trigger: 'blur' },
{reg:/^--------$/, validator: checkDomain, trigger: 'blur' }
]"
>
</el-form-item>
然后來分析一波源碼
// form.vue
//#76 form-item會(huì)emit一個(gè)事件,接收就好
created() {
this.$on('el.form.addField', (field) => {
if (field) {
this.fields.push(field);
}
});
/* istanbul ignore next */
this.$on('el.form.removeField', (field) => {
if (field.prop) {
this.fields.splice(this.fields.indexOf(field), 1);
}
});
},
// # 109
// 我們使用的this.$refs['formname'].validate 里面的validate 就是這個(gè)validate
validate(callback) {
if (!this.model) { // 如果沒有模板直接報(bào)錯(cuò)
console.warn('[Element Warn][Form]model is required for validate to work!');
return;
}
let promise;
// if no callback, return promise
if (typeof callback !== 'function' && window.Promise) {
promise = new window.Promise((resolve, reject) => {
callback = function(valid) { // 這個(gè)valid是從form-item 里面返回的,下面會(huì)講
valid ? resolve(valid) : reject(valid);
};
});
}
let valid = true;
let count = 0;
// 如果需要驗(yàn)證的fields為空,調(diào)用驗(yàn)證時(shí)立刻返回callback
if (this.fields.length === 0 && callback) {
callback(true);
}
let invalidFields = {};
this.fields.forEach(field => { // 猜測(cè)這個(gè)field應(yīng)該是一個(gè)form-item 的示例
field.validate('', (message, field) => { // 這個(gè)validate也是form-item里面的
if (message) {
valid = false; // 存在校驗(yàn)沒通過
}
invalidFields = objectAssign({}, invalidFields, field); // 應(yīng)該是重新了Object.assign
if (typeof callback === 'function' && ++count === this.fields.length) { // 最后一個(gè)的處理
callback(valid, invalidFields); // 如果cb是函數(shù),正常執(zhí)行,參數(shù)是校驗(yàn)結(jié)果和校驗(yàn)失敗的field
}
});
});
if (promise) {
return promise;// 如果沒有cb,那么返回一個(gè)promise,如果有promise返回一個(gè)promise, 這樣寫提高兼容性
}
},
// form-item.vue ,這里主要講幾個(gè)關(guān)鍵的方法
// # 54
provide() {
return {
elFormItem: this
};
},
inject: ['elForm'],
// 對(duì)內(nèi)注入elForm, 對(duì)外拋出elFormItem
//#189 每個(gè)form-item 單獨(dú)校驗(yàn)
import AsyncValidator from 'async-validator';
validate(trigger, callback = noop) { // 這個(gè)就是我上文提到的form-item 里面的validate
this.validateDisabled = false;
const rules = this.getFilteredRule(trigger); //獲取rules
if ((!rules || rules.length === 0) && this.required === undefined) { // 沒有rules, 直接通過
callback();
return true;
}
this.validateState = 'validating';
const descriptor = {};
if (rules && rules.length > 0) {
rules.forEach(rule => {
delete rule.trigger;
});
}
descriptor[this.prop] = rules; //每個(gè)form-item 單獨(dú)校驗(yàn)
const validator = new AsyncValidator(descriptor);
const model = {};
model[this.prop] = this.fieldValue; // 這里就是為什么一定要有model, 而且props必須可以直接訪問
validator.validate(model, { firstFields: true }, (errors, invalidFields) => { // 參考asyn-validator 不展開
// validation failed, errors is an array of all errors
// fields is an object keyed by field name with an array of
// errors per field
// https://github.com/yiminghe/async-validator
//- firstFields: Boolean|String[], Invoke callback when the first validation rule of the specified sofield generates an error, no more validation rules of the same field are processed. true means all fields. 所以項(xiàng),只要有規(guī)則一個(gè)產(chǎn)生error, 該項(xiàng)后面規(guī)則都不執(zhí)行。
//說到底就是對(duì)async-validator一個(gè)封裝
this.validateState = !errors ? 'success' : 'error';
this.validateMessage = errors ? errors[0].message : '';
callback(this.validateMessage, invalidFields);
this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
});
},
//# 252
getRules() {
let formRules = this.form.rules; // parent 組件的rules
const selfRules = this.rules; // prop 傳入的rules
const requiredRule = this.required !== undefined ? { required: !!this.required } : []; // prop 傳入
const prop = getPropByPath(formRules, this.prop || ''); // 判斷parent 傳的rules是否和prop傳入沖突
formRules = formRules ? (prop.o[this.prop || ''] || prop.v) : []; // 以prop 為準(zhǔn)
return [].concat(selfRules || formRules || []).concat(requiredRule); // 整合
},
getFilteredRule(trigger) {
const rules = this.getRules();
return rules.filter(rule => {
if (!rule.trigger || trigger === '') return true; // 全觸發(fā)
if (Array.isArray(rule.trigger)) {
return rule.trigger.indexOf(trigger) > -1; //按需觸發(fā)
} else {
return rule.trigger === trigger;
}
}).map(rule => objectAssign({}, rule));
},
// #130
form() { // 找到parent為el-form的組件
let parent = this.$parent;
let parentName = parent.$options.componentName;
while (parentName !== 'ElForm') {
if (parentName === 'ElFormItem') {
this.isNested = true;
}
parent = parent.$parent;
parentName = parent.$options.componentName;
}
return parent;
},
// util #47
export function getPropByPath(obj, path, strict) {
let tempObj = obj;
path = path.replace(/\[(\w+)\]/g, '.$1'); // enleve []
path = path.replace(/^\./, ''); // enleve first.
let keyArr = path.split('.');
let i = 0;
for (let len = keyArr.length; i < len - 1; ++i) {
if (!tempObj && !strict) break;
let key = keyArr[i];
if (key in tempObj) {
tempObj = tempObj[key]; // neste address
} else {
if (strict) {
throw new Error('please transfer a valid prop path to form item!');
}
break;
}
}
return {
o: tempObj,
k: keyArr[i],
v: tempObj ? tempObj[keyArr[i]] : null
};
};
參考文獻(xiàn)
https://www.shangmayuan.com/a/1481921e712d4136b6edf6a9.html
https://juejin.cn/post/6870015646520836109#comment
歡迎關(guān)注《前端陽(yáng)光》,加入技術(shù)交流群,加入內(nèi)推群
