前言
近期开发的移动端项目直接上了 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移动端登录注册模块内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!
原文链接:https://juejin.cn/post/6944346735540076575