vue3优雅实现移动端登录注册模块

2023-11-13

前言

近期开发的移动端项目直接上了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由于默认宽度的影响导致让右侧向外溢出了,这并不是我们想要的.

解决这个问题的办法很简单,只需要将中间inputwidth设置为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结构类似如下.通过injectpropName可以拿到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校验规则即可.剩下的所有校验细节和交互动作全部交给了FormInputForm内部处理,这样会极大的提升开发效率.

最终效果

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

vue3优雅实现移动端登录注册模块 的相关文章

  • CryptoPP的 Timer算法的使用

    密码学库Cryptopp不仅给提供了丰富的密码学算法 而且还包含了一些有用的工具类算法 比如本次要讲到的Timer 使用该类定义的对象 可以在程序中统计某一段代码的运行时间 前面我们在讲解随机数发生器的使用的时候 在程序中用到统计产生1G
  • 分布式日志系统的设计和实践

    什么是日志 日志是一种按照时间顺序存储记录的数据 它记录了什么时间发生了什么事情 提供精确的系统记录 根据日志信息可以定位到错误详情和根源 按照APM概念的定义 日志的特点是描述一些离散的 不连续的 事件 日志是按照错误级别分级的 常见的错
  • matlab 逆否,逆否命题与反证法

    在原命题 逆命题 否命题与逆否命题中 原命题与逆否命题等价 同真同假 所以证明一个命题成立可以去证明它的逆否命题成立 即先否定结论 在这个否定的结论下 去推出原来的条件的否定成立 例题一 1 判断命题 如果 x y neq 3 那么 x n

随机推荐

  • Python使用xlwt和xlrd读写excel文件

    Python使用xlwt和xlrd读写excel文件 xlwt和xlrd是两个相互配套的模块 在Python中 用于将数据写入Excel文件和读取Excel文件的数据 从字面即可看出xlwt是对xls格式的文件进行write xlrd是对x
  • nvidia-docker踩坑记录

    docker nvidia docker配置镜像创建容器 众所周知 想要在容器中使用nvidia的显卡 需要使用nvidia docker命令创建容器 环境说明 服务器端为Ubuntu18 04离线 nvidia smi正常使用 CUDA版
  • APIPOST入门+认识接口(前后端分离)

    APIPOST入门 认识接口 前后端分离 文章目录 APIPOST入门 认识接口 前后端分离 啥是 API 接口 笑话小案例 编写mock数据 如何解决跨域问题 后续要解决的 实战二维码 驾照题库实战项目 自己写一个接口 2 post和ge
  • 6:sort_values,loc,corr数据筛选,绘图

    一 使用sort values 对某一列 进行从小到大或者从大到小的排序 1 对一列进行操作 import pandas as pd df pd read excel r C Users 73575 Desktop 北京新发地菜价 xlsx
  • 整型的提升和截断详解(看完包会)

    所有常量值 在没有后缀得情况下 默认是4个字节 int型 将一个int型值赋给char型变量时 会发生整形截断 按存储顺序截断 先到先截 一个char截断一个字节即8个bit位 将char类型值按有常量值 在没有后缀得情况下 默认是4个字节
  • linux常用库 对应函数

    1 include
  • Vue在线引入地址

    Vue在线引入地址 vue2 vue3 CodePan在线运行ElementUI时添加的JS及CSS引用地址 Vue https cdn jsdelivr net npm vue 2 dist vue
  • 【Flink系列1】flink与spark的区别

    Flink简介 spark基本架构 flink基本架构 Spark提出的最主要抽象概念是弹性分布式数据集 RDD flink支持增量迭代计算 基于流执行引擎 Flink提供了诸多更高抽象层的API以方便用户编写分布式任务 1 DataSet
  • RabbitMQ镜像集群搭建

    RabbitMQ镜像集群搭建 消息队列 在消息的传输过程中保存消息的容器 MQ角色 Broker 即消息队列服务器实体 Exchange 消息交换机 它指定消息按什么规则 路由到哪个队列 Queue 消息队列载体 每个消息都会被投入到一个或
  • 基于SpringBoot并整合MyBatis和Thymeleaf模板引擎开发的图书管理系统

    先展示下前端页面 登录页面 用户注册页面 一 管理员相关页面以及功能 管理员主页信息以及左侧导航栏 页头可查看当前管理员的信息
  • Unity Application.LoadLevel() 已过时

    解决办法 使用EditorSceneManager方法 https blog csdn net shenqiankk article details 100137502
  • 大数据技术——Scala语言基础

    Scala基础知识 控制结构 if条件表达式 有一点与Java不同的是 Scala中的if表达式的值可以赋值给变量 while循环 for循环 基本语法 其中 变量 lt 表达式 被称为 生成器 generator 守卫 guard 的表达
  • finereport连接oracle_FINEREPORT连接远程ORACLE数据库

    有如下错误提示 SEVERE CannotcreatePoolableConnectionFactory Listenerrefusedtheconnectionwiththefollowingerror ORA 12505 TNS lis
  • python小技巧大应用--基础实用漂亮界面(无边框,圆角,可拖拽)

    这回要实现一个漂亮的基础界面 要具有如下特色 无边框 圆角 漂亮的背景 可拖拽移动 具有最小化 关闭按钮 界面与代码分离 支持qss 先展示一下最后的效果 那就开始工作吧 1 通过Qt Designer实现界面设计 将设计好的界面保存为di
  • 【数据结构】设计循环队列详解

    我的个人主页 我们登上并非我们所选择的舞台 演出并非我们所选择的剧本 Enchiridion 设计循环队列 前言 1 什么是循环队列 2 循环队列的设计 2 1 MyCircularQueue k 实现 2 2 isEmpty 和 isFu
  • usb 命名乱的一批,怎么破

    USB 的命名真是乱的一批 命名里 一股浓浓的 印度风扑面而来 我想给 iso 文件加个驱动直接跪了 被绕进去了 幸运的是速度没乱 以下用速度整理该文档 USB2 0 时代 12 mbps usb1 0 480 mbps usb2 0 US
  • 什么是Restful?

    REST 简介 REST 是英文 Representational State Transfer 的缩写 有中文翻译为 具象状态传输 REST 这个术语是由 Roy Fielding 在他的博士论文 Architectural Styles
  • 大数据课程最后任务-hive处理数据

    好的这是第五次也就是不加额外挑战任务的最后任务 基本过程来自于厦门大学的hive教程 主要是hive处理20w的数据 两部分 一部分是安装 来自http dblab xmu edu cn blog 959 一步分是运行http dblab
  • Yii Framework 开发教程(34) Zii组件-AutoComplete示例

    CJuiAutoComplete 在用户输入时可以根据用户输入的前几个字符自动提示用户可以输入的文字 它封装了 JUI autocomplete插件 基本用法如下 php view plain copy print
  • vue3优雅实现移动端登录注册模块

    前言 近期开发的移动端项目直接上了vue3 新特性composition api确实带来了全新的开发体验 开发者在使用这些特性时可以将高耦合的状态和方法放在一起统一管理 并能视具体情况将高度复用的逻辑代码单独封装起来 这对提升整体代码架构的