vue3如何優雅的實現移動端登錄註冊模塊

前言

近期開發的移動端項目直接上瞭 vue3 ,新特性 composition api 確實帶來瞭全新的開發體驗.開發者在使用這些特性時可以將高耦合的狀態和方法放在一起統一管理,並能視具體情況將高度復用的邏輯代碼單獨封裝起來,這對提升整體代碼架構的健壯性很有幫助.

如今新啟動的每個移動端項目基本上都包含註冊登錄模塊,本次實踐過程中針對登錄註冊中的表單控件做瞭一些經驗上的總結,通過抽離提取共性代碼來提升代碼的可維護性和開發效率.

接下來觀察一下美工同學提供的圖片.

註冊頁面

登錄頁面

忘記密碼頁面

修改密碼頁面

通過觀察上面幾張產品圖片,可以清晰看出構成整個登錄註冊模塊的核心組件就是 input 輸入框.隻要把輸入框組件開發完備,其他頁面直接引用就行瞭.

輸入框開發完瞭隻實現瞭靜態頁面的展示,另外我們還要設計一套通用的數據校驗方案應用到各個頁面中的表單控件.

輸入框組件

從上面分析可知,輸入框組件是整個登錄註冊模塊的核心內容.我們先看一下輸入框組件有哪幾種 UI 形態.

形態一

左側有文字 +86 ,中間是輸入框,右側如果檢測到輸入框有數據輸入顯示叉叉圖標,如果沒有數據為空隱藏圖標.

形態二

左側隻有一個輸入框,右側是文案.文案的內容可能是驗證碼,也可能是點擊驗證碼後顯示的倒計時文案.

形態三

左側依舊隻有一個輸入框,右側如果檢測到輸入框有內容顯示叉叉圖標,如果內容為空隱藏圖標.

佈局

依據上面觀察而來的現象分析,我們設計這款 input 組件時可以將其分為左中右三部分.左側可能是文案,也可能是空.中間是一個輸入框.右側可能是文案也可能是叉叉圖標.

模板內容如下:

<template>
 <div class="input">
  <!--左側,lt是左側內容-->
   <span class="left-text">
   {{ lt }}
   </span>
  
  <!--中間-->
  <input class="content" v-bind="$attrs" :value="value" @input="onChange" /> 
  
  <!--右側,rt判端是驗證碼還是叉叉圖標-->
   <div v-if="rt == 'timer'" class="right-section">
    {{ timerData.content }} <!--可能是'驗證碼',也可能是倒計時 -->
   </div>
   <div
   v-else-if="rt == 'close'"
   class="right-section"
   >
   <van-icon name="close" /> <!--叉叉圖標-->
   </div>
 </div> 
</template>

佈局上將左中右的父級設置為 display:flex ,子級的三個元素全部設置成 display:inline-block 行內塊模式,目的是為瞭讓左側和右側依據自身內容自適應寬度,而中間的 input 設置成 flex:1 充滿剩餘的寬度.

理論上這樣的佈局是可行的,但實踐中發現瞭問題.

Demo 效果圖如下:

右側持續增加寬度時,中間 input 由於默認寬度的影響導致讓右側向外溢出瞭,這並不是我們想要的.

解決這個問題的辦法很簡單,隻需要將中間 input 的 width 設置為 0 即可,如下便達到瞭我們想要的效果.

v-model

外部頁面引用上述封裝的組件結構如下:

 <InputForm
  lt="+86" <!--左側顯示+86--> 
  rt="close" <!--右側顯示叉叉圖標-->
  placeholder="請輸入手機號碼"
 />

外部頁面創建瞭一個表單數據 form_data 如下,但希望能通過 v-model 的形式將 form_data 的數據與子組件輸入框的值建立雙向數據綁定.

 const form_data = reactive({
  number_number: '', //用戶名
  password: '', //密碼
  ppassword: '', //重復密碼
  captcha: '', //驗證碼
 })

在 vue3 實現 v-model 非常簡便,在父組件中使用 v-model:xx 完成綁定,這裡的 xx 對應著子組件要綁定的狀態名稱,如下所示.

 <InputForm
    lt="+86"  <!--左側顯示+86--> 
    rt="close" <!--右側顯示叉叉圖標-->
    placeholder="請輸入手機號碼"
    v-model:value="form_data.password"
 />

接下來子組件裡首先聲明要綁定的屬性 value ,並監聽輸入框的 oninput事件 .代碼如下:

<template>
  <div class="input">
    ...
      <input class="content" v-bind="$attrs" :value="value" @input="onChange" /> 
    ...
  </div> 
</template>
export default defineComponent({
 props: {
  lt:String,
  rt: String,
  value: String
 },
 setup(props, context) {
  const onChange = (e:KeyboardEvent) => {
   const value = (e.target as HTMLInputElement).value;
    context.emit("update:value",value);
  };
  return {
    onChange
  }
 }
 })

oninput事件 的回調函數將獲取到的值使用 context.emit(“update:value”,value) 返回回去.

其中 update:value 裡前面部分 update: 為固定寫法,後面填寫要建立雙向綁定的狀態名稱.如此一來就輕易的完成瞭 v-model 的綁定.

數據校驗

一般來說隻要頁面上涉及到表單控件(比如輸入框),那麼就要針對相應的值做數據校驗.如果按照原始的方法,當用戶點擊按鈕, js 接受響應依次獲取每個表單項的值一一校驗.

這樣的做法當然可以實現功能,但並不高效和精簡.因為很多頁面都要做校驗,大量的校驗邏輯是重復書寫的.

我們接下來設計一套通用的校驗方案,將那些可以復用的邏輯代碼都封裝起來,並且能夠快速的應用到每個頁面上,提升開發效率.

依註冊頁面為例,模板代碼如下.創建四個輸入框組件:手機號,手機驗證碼,密碼和確認密碼.最後面再放置一個註冊按鈕.(為瞭看起來更清晰,下面的代碼將所有 ts 類型刪除)

 <Form ref="form" :rules="rules">
 
   <InputForm
    lt="+86"
    rt="close"
    v-model:value="form_data.number_number"
    placeholder="請輸入手機號碼"
    propName="number_number"
   />
   
   <InputForm
    rt="timer"
    v-model:value="form_data.captcha"
    placeholder="請輸入手機驗證碼"
    propName="captcha"
   />

   <InputForm
    rt="close"
    v-model:value="form_data.password"
    placeholder="請輸入密碼"
    type="password"
    propName="password"
   />

   <InputForm
    rt="close"
    v-model:value="form_data.ppassword"
    placeholder="請輸入確認密碼"
    type="password"
    propName="ppassword"
   />

   <Button text="註 冊" @sub="onSubmmit" /> <!--註冊按鈕-->

  </Form>

在借鑒瞭一些其他優秀框架的表單實踐後,我們首先是在最外層增加瞭一個組件 Form ,其次給每個輸入框組件增加瞭一個屬性 propName .這個屬性是配合 rules 一起使用的, rules 是手動定義的校驗規則,當它傳遞給 Form 組件後,子組件(輸入框組件)就能通過 propName 屬性拿到屬於它的校驗規則.

整體的實現思路可以從頭串聯一遍.首先是前端開發者定義好當前頁面的校驗規則 rules ,並將它傳遞給 Form 組件. Form 組件接受到後會將校驗規則分發給它的每個子組件(輸入框組件).子組件拿到校驗規則後就能夠針對輸入框的值做相應的數據校驗.

當用戶點擊註冊按鈕時,點擊事件會獲取 Form 組件的實例,並運行它的 validate 方法,此時 Form 組件就會對它旗下的每個子組件做一輪數據校驗.一旦所有校驗成功瞭, validate 方法返回 true .存在一個校驗沒通過, validate 方法就返回 false ,並彈出錯誤信息.

註冊頁面邏輯如下:

export default defineComponent({
 components: {
  InputForm, //輸入框
  Button, //註冊按鈕
  Form, //Form組件
 },
 setup(props) {
 
  const form_data = ...; //省略
  
  const rules = ...;
  
  //獲取最外層Form組件的實例
  const form = ref(null);
  
  const onSubmmit = ()=>{
   if (!form.value || !form.value.validate()) {
     return false;
   }
   //校驗通過瞭,可以請求註冊接口瞭
  }

  return {
   form,
   rules,
   onSubmmit,
   form_data
  };
 },
});

定義一個變量 form ,用它來獲取 Form 表單的實例.模板上 <Form ref=”form” :rules=”rules”> 隻需要加上一個 ref 屬性就可以瞭.

用戶點擊註冊按鈕觸發 onSubmmit 函數,因為 form 是使用 ref 創建的變量,獲取值要調用 .value .運行 form.value.validate() 函數,就能讓 Form 表單下面的每一個子組件開始執行校驗邏輯,如果全部通過就會返回 true ,存在一個沒通過返回 false .

從上面分析可知, Form 控件隻對外暴露一個 validate 函數,通過調用該函數就能知道校驗是否通過.那麼 validate 如何知道該采用什麼規則來校驗呢?所以我們要先設計一套校驗的規則 rules ,把它傳給 Form 組件,那麼它內部的 validate 函數就能采用規則來執行校驗.

rules設計

rules 是一個對象,例如上述註冊頁面的 rules 定義如下:

const rules = {
   number_number:[{
        type: 'required',
        msg:"請輸入正確的手機號" 
      }
        "phone"
      ],
   captcha:[
    {
     type: 'required',
     msg: '驗證碼不能為空'
    }
   ],
   password: [
    {
     type: 'required',
     msg: '請輸入密碼',
    },
    {
     type: 'minLength',
     params: 6,
     msg: '密碼長度不能小於6位',
    },
   ],
   ppassword:[
    {
     type: 'custome',
     callback() {
      if (form_data.password !== form_data.ppassword) {
       return {
        flag: false,
        msg: '兩次輸入的密碼不一致',
       };
      }
      return {
       flag: true,
      };
     },
    },
   ]
  }

我們定義的 rules 是一個鍵值對形式的對象. key 對應著模板上每個輸入框組件的 propName ,值是一個數組,對應著該輸入框組件要遵守的規則.

現在細致的看下每個對象下的值的構成,值之所以組織成數組形式,是因為這樣可以給輸入框增加多條規則.而規則對應著兩種形式,一種是對象,另外一種是字符串.

字符串很好理解,比如上面的 number_number 屬性,它就對應著字符串 phone .這條規則的意義就是該輸入框的值要遵守手機號的規則.當然字符串如果填 email ,那就要當做郵箱來校驗.

規則如果為對象,那麼它包含瞭以下幾個屬性:

 {
  type, // 類型
  msg, //自定義的錯誤信息
  params, //傳過來的參數值 比如 {type:'minLength',params:6},值最小長度不能低於6位
  callback //自定義校驗函數
 }

type 是校驗類型,它如果填 required ,表示是必填項.如果用戶沒填,點擊註冊按鈕提交時就會報出 msg 定義的錯誤信息.

另外 type 還可以填 minLength 或者 maxLength 用來限定值的長度,那到底限定為幾位呢,可以通過 params 傳遞過去.

最後 type 還可以填 custome ,那麼就是讓開發者自己來定義該輸入框的校驗邏輯函數 callback .該函數要求最後返回一個帶有 flag 屬性的對象,屬性 flag 為佈爾值,它會告訴校驗系統本次校驗是成功還是失敗.

Form表單

rules 被定義好後傳給 Form 組件, Form 組件需要將校驗邏輯分發給它的子組件.讓其每個子組件都負責生成自己的校驗函數.

<!-- 表單組件 -->
<template>
 <div class="form">
  <slot></slot>
 </div>
</template>

<script lang="ts">
import { ref, provide } from "vue";
export default defineComponent({
 name: "Form",
 props:{
  rules:Object
 },
 setup(props) {
  
  ...//省略

  provide("rules",props.rules); // 將校驗規則分發下去
  
  const validate = ()=>{
   //向外暴露的校驗函數
  }
  
  return {
   validate
  } 
 }
 }) 
</script>  

從上面結構可以看出, Form 組件模板提供瞭一個插槽的作用,在邏輯代碼裡利用 provide 將校驗規則傳給後代,並向外暴露一個 validate 函數.

子組件生成校驗函數

這一次又回到瞭登錄註冊模塊的核心組件 InputForm ,我們現在要給該輸入框組件添加校驗邏輯.

import { inject,onMounted } from "vue";
...

setup(props, context) {

 const rules = inject("rules");
 
 const rule = rules[props.propName];// 通過propName拿到校驗規則
 
 const useValidate = () => {
      const validateFn = getValidate(rule); // 獲取校驗函數
      const execValidate = () => { 
        return validateFn(props.value); //執行校驗函數並返回校驗結果    
      };
      onMounted(() => {
        const Listener = inject('collectValidate');
        if (Listener) {
         Listener(execValidate);
        }
      });  
 };
 
 useValidate(); //初始化校驗邏輯
 ...
}

rules 結構類似如下.通過 inject 和 propName 可以拿到 Form 分發給該輸入框要執行的規則 rule .

{
  captcha:[{
   type: 'required',
   msg: '驗證碼不能為空'
  }],
  password:[{
   type: 'required',
   msg: '請輸入密碼', 
  }]
}

再將規則 rule 傳遞給 getValidate 函數(後面會講)獲取校驗函數 validateFn .校驗函數 validateFn 傳入輸入框的值就能返回校驗結果.在這裡把 validateFn 封裝瞭一層賦予 execValidate 給外部使用.

在上面的代碼中我們還看到瞭 onMounted 包裹的邏輯代碼.當組件掛載完畢後,使用 inject 拿到 Form 組件傳遞下來的一個函數 Listener ,並將校驗函數 execValidate 作為參數傳遞進去執行.

我們再回到下面代碼中的 Form 組件,看一下 Listener 是一個什麼樣的函數.

setup(props) {

const list = ref([]);//定義一個數組

const listener = (fn) => {
 list.value.push(fn);
};

provide("collectValidate", listener); //將監聽函數分發下去

//驗證函數
const validate = (propName) => {
  const array = list.value.map((fn) => {
    return fn();
  });
  const one = array.find((item) => {
    return item.flag === false;
  });
  if (one && one.msg) {
    //驗證不通過
    Alert(one.msg);//彈出錯誤提示
    return false;
  } else {
    return true;
  }
};
...

從上面可以看出, Form 組件將 listener 函數分發瞭下去.而子組件在 onMounted 的生命周期鉤子裡,獲取到分發下來的 listener 函數,並將子組件內部定義的校驗函數 execValidate 作為參數傳遞進去執行.

這樣一來就可以確保每個子組件一旦掛載完畢就會把自己的校驗函數傳遞給 Form 組件中的 list 收集.而 Form 組件的 validate 方法隻需要循環遍歷 list ,就可以依次執行每個子組件的校驗函數.如果都校驗通過瞭,給外部頁面返回 true .存在一個不通過,彈出錯誤提示返回 false .

走到這裡整個校驗的流程已經打通瞭. Form 首先向子組件分發校驗規則,子組件獲取規則生成自己的校驗函數,並且在其掛載完畢後將校驗函數再返回給 Form 收集起來.這個時候 Form 組件向外暴露的 validate 函數就可以實現針對所有表單控件的數據校驗.

接下來最後一步研究子組件如果通過規則來生成自己的校驗函數.

校驗

首先編寫一個管理校驗邏輯的類 Validate .代碼如下.我們可以不斷的根據新需求擴充該類的方法,比如另外再增加 email 或者 maxLength 方法.

class Validate {

 constructor() {}

 required(data) { //校驗是否為必填  
  const msg = '該信息為必填項'; //默認錯誤信息
  if (data == null || (typeof data === 'string' && data.trim() === '')) {
   return {
    flag:false,
    msg
   }
  }
  return {
    flag:true
  }
 }
 
 //校驗是否為手機號
 phone(data) { 
  const msg = '請填寫正確的手機號碼'; //默認錯誤信息
  const flag = /^1[3456789]\d{9}$/.test(data);
  return {
   msg,
   flag
  }
 }
 
 //校驗數據的最小長度
 minLength(data, { params }) {
  
    let minLength = params; //最小為幾位
    
    if (data == null) {
     return {
      flag:false,
      msg:"數據不能為空"
     }
    }

    if (data.trim().length >= minLength) {
     return {flag:true};
    } else {
     return {
      flag:false,
      msg:`數據最小長度不能小於${minLength}位`
     }
    } 
  }

}

Validate 類定義的所有方法中,第一個參數 data 是被校驗的值,第二個參數是在頁面定義每條 rule 中的規則.形如 {type: ‘minLength’, params: 6, msg: ‘密碼長度不能小於6位’} .

Validate 類中每個方法最終的返回的數據結構形如 {flag:true,msg:””} .結果中 flag 就來標識校驗是否通過, msg 為錯誤信息.

校驗類 Validate 提供瞭各種各樣的校驗方法,接下來運用一個單例模式生成該類的一個實例,將實例對象應用到真實的校驗場景中.

 const getInstance = (function(){
  let _instance;
  return function(){
     if(_instance == null){
      _instance = new Validate();
     }
     return _instance;
   }
 })()

通過調用 getInstance 函數就可以得到單例的 Validate 實例對象.

輸入框組件通過給 getValidate 函數傳入一條 rule ,就能返回該組件需要的校驗函數.接下來看一下 getValidate 函數是如何通過 rule 來生成校驗函數的,代碼如下:

/**
 * 生成校驗函數
 */
export const getValidate = (rule) => {
  const ob = getInstance();//獲取 Validate類 實例對象
  const fn_list = []; //將所有的驗證函數收集起來
  //遍歷rule數組,根據其類型獲取Validate類中的校驗方法放到fn_list中收集起來
  rule.forEach((item) => {
   if (typeof item === 'string') { // 字符串類型 
    fn_list.push({
     fn: ob[item], 
    });
   } else if (isRuleType(item)) { // 對象類型
    fn_list.push({
     //如果item.type為custome自定義類型,校驗函數直接使用callback.否則從ob實例獲取 
     ...item, 
     fn: item.type === 'custome' ? item.callback : ob[item.type],
    });
   }
  });
  //需要返回的校驗函數
  const execuate = (value) => {
   let flag = true,
    msg = '';
   for (let i = 0; i < fn_list.length; i++) {
    const item = fn_list[i];
    const result = item.fn.apply(ob, [value, item]);//item.fn對應著Validate類定義的的校驗方法
    if (!result.flag) {
     //驗證沒有通過
     flag = false;
     msg = item.msg ? item.msg : result.msg;//是使用默認的報錯信息還是用戶自定義信息 
     break;
    }
   }
   return {
    flag,
    msg,
   };
  };
  return execuate;
};

rule 的數據結構形類似如下代碼.當把 rule 傳入 getValidate 函數,它會判端是對象還是字符串,隨後將其類型對應的校驗函數從 ob 實例中獲取存儲到 fn_list 中.

 [
  {
   type: 'required',
   msg: "請輸入電話號碼"
  },
  "phone"
 ]

getValidate 函數最終返回 execuate 函數,此函數也正是輸入框組件得到的校驗函數.在輸入框組件裡是可以拿到輸入框值的,如果將值傳給 execuate 方法調用.方法內部就會遍歷之前緩存的校驗函數列表 fn_list ,將值傳入每個校驗方法運行就能獲取該輸入框組件對當前值的校驗結果並返回回去.

以上校驗的邏輯也已經走通瞭.接下來不管是開發登錄頁,忘記密碼或者修改密碼的頁面,隻需要使用 Form 組件和輸入框 InputForm 組件組織頁面結構,並寫一份當前頁面的 rules 校驗規則即可.剩下的所有校驗細節和交互動作全部交給瞭 Form 和 InputForm 內部處理,這樣會極大的提升開發效率.

最終效果

總結

到此這篇關於vue3如何優雅的實現移動端登錄註冊模塊的文章就介紹到這瞭,更多相關vue3移動端登錄註冊模塊內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: