<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          手寫el-form表單組件,再也不怕面試官問我form表單原理

          共 34144字,需瀏覽 69分鐘

           ·

          2021-08-22 08:56


          • 前言

          • 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:truemessage:'用戶名不能為空' },
                   userPassword: { required:truemessage:'密碼不能為空' }
              }
             }
            },
            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: {
                          typeString,
                          default'',
                      },
                      prop: {
                          typeString,
                          requiredtrue,
                          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 {
                      formthis
                  }
              },
              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, rulesref

          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: [
                      { requiredtruemessage'請(qǐng)輸入活動(dòng)名稱'trigger'blur'validatorfunction({} },
                      { min3max5message'長(zhǎng)度在 3 到 5 個(gè)字符'trigger'blur'validatorfunction({} }
                    ],
                    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 {
                elFormItemthis
              };
            }, 
            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, { firstFieldstrue }, (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)推群


          瀏覽 123
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美性爱免费在线视频海量版 | 99精品在线 | 天天操动漫 | 日本很黄的视频免费在线观看视频 | 新www视频亚洲 |