近期开发的移动端项目直接上了vue3
,新特性composition api
确实带来了全新的开发体验.开发者在使用这些特性时可以将高耦合的状态和方法放在一起统一管理,并能视具体情况将高度复用的逻辑代码单独封装起来,这对提升整体代码架构的健壮性很有帮助.
如今新启动的每个移动端项目基本上都包含注册登录模块,本次实践过程中针对登录注册中的表单控件做了一些经验上的总结,通过抽离提取共性代码来提升代码的可维护性和开发效率.
接下来观察一下美工同学提供的图片.
通过观察上面几张产品图片,可以清晰看出构成整个登录注册模块的核心组件就是input
输入框.只要把输入框组件开发完备,其他页面直接引用就行了.
输入框开发完了只实现了静态页面的展示,另外我们还要设计一套通用的数据校验方案应用到各个页面中的表单控件.
从上面分析可知,输入框组件是整个登录注册模块的核心内容.我们先看一下输入框组件有哪几种UI
形态.
形态一
左侧有文字+86
,中间是输入框,右侧如果检测到输入框有数据输入显示叉叉图标,如果没有数据为空隐藏图标.
形态二
左侧只有一个输入框,右侧是文案.文案的内容可能是验证码,也可能是点击验证码后显示的倒计时文案.
形态三
左侧依旧只有一个输入框,右侧如果检测到输入框有内容显示叉叉图标,如果内容为空隐藏图标.
依据上面观察而来的现象分析,我们设计这款input
组件时可以将其分为左中右三部分.左侧可能是文案,也可能是空.中间是一个输入框.右侧可能是文案也可能是叉叉图标.
模板内容如下:
{
{ lt }}
{
{ timerData.content }}
布局上将左中右的父级设置为display:flex
,子级的三个元素全部设置成display:inline-block
行内块模式,目的是为了让左侧和右侧依据自身内容自适应宽度,而中间的input
设置成flex:1
充满剩余的宽度.
理论上这样的布局是可行的,但实践中发现了问题.
Demo
效果图如下:
右侧持续增加宽度时,中间input
由于默认宽度的影响导致让右侧向外溢出了,这并不是我们想要的.
解决这个问题的办法很简单,只需要将中间input
的width
设置为0
即可,如下便达到了我们想要的效果.
外部页面引用上述封装的组件结构如下:
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
对应着子组件要绑定的状态名称,如下所示.
rt="close"
placeholder="请输入手机号码"
v-model:value="form_data.password"
/>
接下来子组件里首先声明要绑定的属性value
,并监听输入框的oninput事件
.代码如下:
...
...
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
,其次给每个输入框组件增加了一个属性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
表单的实例.模板上只需要加上一个
ref
属性就可以了.
用户点击注册按钮触发onSubmmit
函数,因为form
是使用ref
创建的变量,获取值要调用.value
.运行form.value.validate()
函数,就能让Form
表单下面的每一个子组件开始执行校验逻辑,如果全部通过就会返回true
,存在一个没通过返回false
.
从上面分析可知,Form
控件只对外暴露一个validate
函数,通过调用该函数就能知道校验是否通过.那么validate
如何知道该采用什么规则来校验呢?所以我们要先设计一套校验的规则rules
,把它传给Form
组件,那么它内部的validate
函数就能采用规则来执行校验.
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
为布尔值,它会告诉校验系统本次校验是成功还是失败.
rules
被定义好后传给Form
组件,Form
组件需要将校验逻辑分发给它的子组件.让其每个子组件都负责生成自己的校验函数.
从上面结构可以看出,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
内部处理,这样会极大的提升开发效率.