通过前端项目Mall-project (https://github.com/Ray2310/MallProject)使得对于vue技术的实现有了大致的了解和使用。 这里我将具体到一个模块的完成, 从而实现对于vue技术在登录模块下的各个方面的细致讲解。 首先,我们按照vue的思想, 通过组件的形式来完成对于项目的code。 因此按照项目的UI图 以及 登录模块的接口文档, 我们将项目划分为以下内容来进行将解
项目UI图
页面布局之顶部导航
顶部导航栏, 我们可以通过使用vant中的组件来实现,这样大大减少了code的工作量 首先我们通过使用vant组件库的按需导入, 从而实现压缩项目体积, 提升了项目的加载速度和性能, 同时也可以提升网络请求。 所以这里采用按需导入而不是全部导入。
组件导入实现步骤
- 创建
utils/vant-ui.js
作为专门封装vant组件的js模块, 我们只需要再main.js
中导入即可
// utils/vant-ui.js | |
//把引入组件的步骤抽离到单独的js文件 将需要导入的配置 放在此处。 | |
import Vue from 'vue' | |
//1. 按需导入组件 | |
import { Tabbar, TabbarItem , NavBar, Toast, Search, Swipe, SwipeItem, Grid, GridItem} from 'vant' | |
//2. 使用对应的组件 | |
Vue.use(Tabbar) | |
Vue.use(TabbarItem) | |
Vue.use(NavBar) | |
Vue.use(Toast) | |
Vue.use(GridItem) | |
Vue.use(Search) | |
Vue.use(Swipe) | |
Vue.use(SwipeItem) | |
Vue.use(Grid) | |
// main.js | |
// 导入vent中的需要的组件 | |
import '@/utils/vant-ui' |
- 在页面布局模块
views/login/index.vue
使用导入的需要的组件NavBar
<div class="login"> | |
<!-- 上方使用 NavBar 导航栏 --> | |
<van-nav-bar | |
title="会员中心" | |
left-text="返回" | |
right-text="按钮" | |
left-arrow | |
@click-left="onClickLeft" | |
@click-right="onClickRight" | |
/> | |
// 然后修改内容为我们自己的 | |
<van-nav-bar title="会员中心" left-text="" right-text="" left-arrow | |
@click-left="$router.go(-1)"/> |
注意: 这里有个返回上一页的箭头, 我们使用路由的方法来实现** @click-left="$router.go(-1)"**
页面布局之主体部分
通过上面的组件导入步骤介绍, 我们也大致知道了组件如何导入及其使用, 接下来的基本所有内容我们都是通过组件的形式实现的, 有的是使用vant组件库, 有的是我们自己封装实现。 下面就是页面布局的主体部分。就是通过我们自己封装的组件。
封装组件实现主题部分
其实这个模块也是可以复用的, 下次也就是改改里面的内容即可, 所以这也就是人们常说code就是ctrl+C/V
了, 因为coder追求的就是极致的便捷、快速。但是这对于初学者我认为还是不够友好的,因为还没有明白原理便开始CV, 那么也只是咀嚼别人吃过的, 没有自己的思想味道。 回归正题…. 主题部分也是在views/login/index.vue
中实现的, 只是用了不同的盒子。
<template> | |
<div class="login"> | |
<van-nav-bar title="会员登录" left-arrow @click-left="$router.go(-1)" /> | |
<div class="container"> | |
<div class="title"> | |
<h3>手机号登录</h3> | |
<p>未注册的手机号登录后将自动注册</p> | |
</div> | |
<div class="form"> | |
<div class="form-item"> | |
<input class="inp" maxlength="11" placeholder="请输入手机号码" type="text"> | |
</div> | |
<div class="form-item"> | |
<input class="inp" maxlength="5" placeholder="请输入图形验证码" type="text"> | |
<img src="@/assets/code.png" alt=""> | |
</div> | |
<div class="form-item"> | |
<input class="inp" placeholder="请输入短信验证码" type="text"> | |
<button>获取验证码</button> | |
</div> | |
</div> | |
<div class="login-btn">登录</div> | |
</div> | |
</div> | |
</template> | |
<script> | |
export default { | |
name: 'LoginPage' | |
} | |
</script> | |
<style lang="less" scoped> | |
.container { | |
padding: 49px 29px; | |
.title { | |
margin-bottom: 20px; | |
h3 { | |
font-size: 26px; | |
font-weight: normal; | |
} | |
p { | |
line-height: 40px; | |
font-size: 14px; | |
color: #b8b8b8; | |
} | |
} | |
.form-item { | |
border-bottom: 1px solid #f3f1f2; | |
padding: 8px; | |
margin-bottom: 14px; | |
display: flex; | |
align-items: center; | |
.inp { | |
display: block; | |
border: none; | |
outline: none; | |
height: 32px; | |
font-size: 14px; | |
flex: 1; | |
} | |
img { | |
width: 94px; | |
height: 31px; | |
} | |
button { | |
height: 31px; | |
border: none; | |
font-size: 13px; | |
color: #cea26a; | |
background-color: transparent; | |
padding-right: 9px; | |
} | |
} | |
.login-btn { | |
width: 100%; | |
height: 42px; | |
margin-top: 39px; | |
background: linear-gradient(90deg,#ecb53c,#ff9211); | |
color: #fff; | |
border-radius: 39px; | |
box-shadow: 0 10px 20px 0 rgba(0,0,0,.1); | |
letter-spacing: 2px; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
} | |
} | |
</style> |
功能实现之输入内容提示
用户输入了手机号, 但是输入了13位或在1位,那么我们就需要给个提示, 我们要求的手机号是11位。或者说输入的内容经过我们后端的校验发现是错的,那么我们前端也需要进行提示
校验手机号和图形验证码
// 校验手机号 和 图形验证码输入是否正确 | |
validFn(){ | |
if (!/^1[3-9]\d{9}$/.test(this.mobile)) { | |
this.$toast('请输入正确的手机号') | |
return false | |
} | |
// 这个逻辑有问题。 随便输入4位都可以通过 | |
if (!/^\w{4}$/.test(this.picCode)) { | |
this.$toast('请输入正确的图形验证码') | |
return false | |
} | |
return true | |
}, |
Toast提示
之前我们可能使用的alert
, 但是学习了vue之后,组件化开发的思想深入脑海, 所以翻找组件库vant ,我们发现Toast
这个组件就可以进行提示, 所以按照上面的组件导入思路,我们就可以实现下面这样的效果。
我们进行需要的时候可以直接使用Toast('提示的内容')
来实现,而他的本质其实是通过
将方法, 注册挂载到了Vue原型上this.$toast('提示内容')
功能实现之图形验证码
在获取图形验证码之前,我们需要对请求进行封装, 因为随着项目开发的深入, 代码随着堆积成山, 如果不进行封装维护, 那么就会形成别人口中的“始(shi)山代码” ,所以为了我们项目的可维护性,我们需要对请求进行封装
封装所有的请求及其login模块的请求
在utils/request.js
模块 ,我们将所有的请求都封装到这里, 这样就便于项目的维护 这些内容都可以通过参考axios官网来实现。
请求响应的封装
响应拦截器是咱们拿到数据的 第一个 “数据流转站”,可以在里面统一处理错误,只要不是 200 默认给提示,抛出错误
// 封装axios请求方法, 封装到request模块 | |
import { Toast } from "vant" | |
import axios from "axios" | |
//1. 创建axios实例。 以后通过使用创建出来的axios实例 , 进行自定义配置 | |
// 好处: 不会污染原始的aixos实例 | |
const instance = axios.create({ | |
baseURL: 'http://cba.itlike.com/public/index.php?s=/api/', | |
timeout: 5000, | |
}); | |
//2. 自定义配置 | |
//2.1 配置拦截器 | |
// 添加请求拦截器 | |
instance.interceptors.request.use(function (config) { | |
// 在发送请求之前做些什么 | |
// 开启loading | |
Toast.loading({ | |
message: '加载中...', | |
forbidClick: true, // 禁止背景点击( 节流防抖操作 ) | |
loadingType: 'spinner', | |
duration: 0 //不会自动消失 | |
}) | |
return config; | |
}, function (error) { | |
// 对请求错误做些什么 | |
return Promise.reject(error); | |
}); | |
// 添加响应拦截器 | |
instance.interceptors.response.use(function (response) { | |
const res = response.data | |
if (res.status !== 200) { | |
// 显示错误信息 | |
Toast(res.message) | |
return Promise.reject(res.message) | |
}else{ | |
Toast.clear() | |
} | |
// 对响应数据做点什么 | |
return res | |
}, function (error) { | |
// 对响应错误做点什么 | |
return Promise.reject(error) | |
}) | |
//3. 导出实例 | |
export default instance |
通过上面的工具类封装,我们所有的请求口都会先经过 请求和响应拦截器, 然后再放行,同时, 我们配置了baseURL
基地址,这样以后项目中使用直接使用对应的url就行了。 接下来就是我们login模块的请求封装了, 如果前面封装的是所有的,但是每个模块的请求也需要进行封装才能方便使用。 所以我们将所有的请求都封装到了api
模块中, 然后在api/login.js
中再封装我们的登录模块的请求。
// 登录相关的接口请求 | |
//1. 获取图形验证码 | |
import request from '@/utils/request' | |
export const getPicCode = ()=> { | |
return request.get('/captcha/image') | |
} | |
//2. 获取短信验证码的请求接口 | |
//3. 点击登录请求接口 |
实现图形验证码回显
注意,我们获取图形验证码需要一进入登录页面就需要显示出来, 所以在views/login/index.vue
中需要在created(){}
方法中实现, 同时, 如果用户看不清图形验证码想要点击图形验证码换一张的时候,我们也需要提供对应的方法来进行实现
<!-- 点击重新切换验证码 --> | |
<img :src="picUrl" @click="getPicCode()" alt=""> | |
async created() { | |
// 通过调用方法来实现图片验证码的显示 | |
this.getPicCode() | |
}, |
通过上述得到的验证码应该是一个 res.data里面的base64
就是我们图片的地址, 我们需要将其拿出来渲染到页面上, 所以就需要参数来接收请求得到的内容。 所以就需要定义变量
export default{ | |
data() { | |
return { | |
//1. 设置获取图形验证码的参数 | |
piccode: '', // 用户输入的图形验证码 | |
picKey: '', //请求传入图形验证码的唯一标识 | |
picUrl: '', // 存储请求渲染的图片地址 | |
} | |
} | |
} |
同时在页面中渲染出来
// 获取图形验证码 | |
async getPicCode(){ | |
// 使用自己封装的axios来使用, 这样就不会污染原始的axios请求 | |
// 将所有的请求全部放到api模块去实现 | |
const res = await getPicCode() | |
this.picUrl = res.data.base64 | |
this.picKey = res.data.key | |
}, | |
<div class="form-item"> | |
<input class="inp" maxlength="5" v-model="picCode" placeholder="请输入图形验证码" type="text"> | |
<!-- 点击重新切换验证码 --> | |
<img :src="picUrl" @click="getPicCode()" alt=""> | |
</div> |
通过上述一系列的code, 我们就实现了获取图形验证码, 并且回显到页面上。 既然得到的验证码, 那么接下就可以根据用户输入的手机号发送短信了。
功能实现之短信验证码
在实现短信验证码之前, 我们先联想一下, 如果用户一直点击获取验证码, 但是就是不登录,那么我们服务器虽然不会受影响, 但是一条短信一分钱, 如果被不法分子攻击网站,获取短信验证码没有限制, 那么不到一小时,你可能就被攻击到欠XX云100000元了, 所以有些限制是必须有的,我们通过短信验证码的倒计时可以缓冲, 然后后台再对敏感的手机号做限流处理,这样对于网站的防护就上了一个档次,避免了大部分的攻击和恶意注册。
短信验证码的倒计时提醒
实现效果相信大家都见过,这里我们就直接上实现步骤了。 实现思路就是通过定时器来实现, 最后到时间就删除定时器。
- 首先, 设置三个属性作为设置定时器的属性
data() { | |
return { | |
//设置 获取短信验证码 倒计时 | |
totalSecond: 60, // 总秒数 | |
second: 60, // 倒计时的秒数 | |
timer: null // 定时器 id | |
} | |
}, |
- 在我们使用的地方通过点击触发定时器 并且设置定时器的显示文字(离开和显示倒计时)
<div class="form-item"> | |
<input class="inp" placeholder="请输入短信验证码" type="text"> | |
<button @click="getCode"> | |
{{ second === totalSecond ? '获取验证码' : second + `秒后重新发送`}} | |
</button> | |
</div> |
- 定时器的js逻辑
// 获取短信验证码 | |
async getCode () { | |
// 校验号码和图形验证码 | |
if(!this.validFn()){ | |
return | |
} | |
// 开启定时器 | |
if (!this.timer && this.second === this.totalSecond) { | |
// 发送获取短信验证码的请求 | |
const res = await getMsCode(this.picCode, this.picKey, this.mobile) | |
// 这里其实可以省略, 因为我们已经配置了响应拦截器 | |
if(res.status != 200 ) { | |
this.$toast('图形验证码错误') | |
return | |
} | |
// 开启倒计时 | |
this.timer = setInterval(() => { | |
this.second-- | |
if (this.second < 1) { | |
clearInterval(this.timer) | |
this.timer = null // 重置定时器id | |
this.second = this.totalSecond // 归位 | |
} | |
}, 1000) | |
// 发送请求,获取验证码 | |
this.$toast('发送成功,请注意查收') | |
} | |
}, |
- 离开页面。 消除定时器
destroyed () { | |
clearInterval(this.timer) | |
} |
校验信息
在发送短信验证码之前我们需要做校验手机号和图形延展面的操作. 这个再之前已经提过了, 这里因为逻辑需要我们再来一边
// 校验手机号 和 图形验证码输入是否正确 | |
validFn(){ | |
if (!/^1[3-9]\d{9}$/.test(this.mobile)) { | |
this.$toast('请输入正确的手机号') | |
return false | |
} | |
// 这个逻辑有问题。 随便输入4位都可以通过 | |
if (!/^\w{4}$/.test(this.picCode)) { | |
this.$toast('请输入正确的图形验证码') | |
return false | |
} | |
return true | |
}, |
点击发送验证码调用的方法getCode()
//2. 在发送短信验证码的时候进行调用请求 | |
// 获取短信验证码 | |
async getCode () { | |
if(!this.validFn()){ | |
return | |
} | |
// 发送短信验证码 并且启动倒计时提醒 | |
if (!this.timer && this.second === this.totalSecond) { | |
// 逻辑... | |
} | |
}, |
获取短信验证码逻辑
接口信息
js逻辑
在api/login.js
模块, 实现获取短信验证码
//1. 在api/login.js中 进行写请求的逻辑 | |
export const getMsCode = (captchaCode,captchaKey,mobile) =>{ | |
// 按照接口文档的要求, 需要传惨 | |
return request.post('/captcha/sendSmsCaptcha',{ | |
form:{ | |
captchaCode, | |
captchaKey, | |
mobile | |
} | |
}) | |
} |
因为根据请求接口, 调用请求的时候需要传入参数, 这样才能够发送验证码。 所以需要在login/index.vue
中定义属性来接收用户输入的数据
data() { | |
return { | |
//1. 设置获取图形验证码的参数 | |
piccode: '', // 用户输入的图形验证码 | |
picKey: '', //请求传入图形验证码的唯一标识 | |
mobile: '', // 手机号 | |
} | |
}, |
实现获取验证码逻辑
//2. 在发送短信验证码的时候进行调用请求 | |
// 获取短信验证码 | |
async getCode () { | |
if(!this.validFn()){ | |
return | |
} | |
if (!this.timer && this.second === this.totalSecond) { | |
// 发送获取短信验证码的请求 | |
const res = await getMsCode(this.picCode, this.picKey, this.mobile) | |
if(res.status != 200 ) { | |
this.$toast('图形验证码错误') | |
return | |
} | |
console.log("短信验证码", res) | |
// 开启倒计时 | |
this.timer = setInterval(() => { | |
this.second-- | |
if (this.second < 1) { | |
clearInterval(this.timer) | |
this.timer = null // 重置定时器id | |
this.second = this.totalSecond // 归位 | |
} | |
}, 1000) | |
// 发送请求,获取验证码 | |
this.$toast('发送成功,请注意查收') | |
} | |
}, | |
功能实现之封装接口实现登录
接口信息
实现思路
api/login.js
提供登录 Api 函数
//3. 点击登录请求接口 | |
export const loginClick = ( isParty,mobile,partyData,smsCode) => { | |
return request.post('/passport/login',{ | |
form: { | |
isParty, | |
mobile, | |
partyData, | |
smsCode | |
} | |
}) | |
} |
login/index.vue
中通过点击事件调用请求
注意, 需要绑定 短信验证码。 并且还需要传参
// 需要接受数据的属性 | |
data() { | |
return { | |
//1. 设置获取图形验证码的参数 | |
piccode: '', // 用户输入的图形验证码 | |
picKey: '', //请求传入图形验证码的唯一标识 | |
picUrl: '', // 存储请求渲染的图片地址 | |
//2. 设置 获取短信验证码 倒计时 | |
totalSecond: 60, // 总秒数 | |
second: 60, // 倒计时的秒数 | |
timer: null, // 定时器 id | |
// 设置接收输入框的内容,并且使用v-model进行绑定 | |
mobile: '', // 手机号 | |
picCode: '' ,// 图形验证码 | |
//3. 设置点击登录接口需要的参数 | |
isParty: false, // 是否存在第三方用户信息boolean | |
// 手机号上面有接收 | |
partyData:{}, // 三方登录信息,默认为:{} 可选 | |
smsCode: '', // 短信验证码, 测试环境验证码为:246810 | |
} | |
}, | |
// 点击登录的js逻辑 | |
async loginFn() { | |
if (!this.validFn()) { | |
return | |
} | |
if (!/^\d{6}$/.test(this.smsCode)) { | |
this.$toast('请输入正确的手机验证码') | |
return | |
} | |
//2. 调用请求信息 | |
const res = await loginClick(this.isParty, this.mobile, this.partyData, this.smsCode) | |
console.log(res) | |
console.log("登录成功") | |
//3. 路由转发 | |
this.$router.push('/') | |
this.$toast('登录成功') | |
} |
vuex持久化存储登录凭证
对于上述我们实现的登录模块,一旦我们刷新浏览器, 那么登录的信息瞬间就消失了, 用户就得重新登录, 所以我们需要持久化存储登录凭证, 同时登录凭证还需要作为公共信息, 因为在其他模块 比如支付或者购物车模块, 都是需要用户输入登录信息才能够执行的。所以就需要从全局获取登录凭证 ,有了登录凭证才能够登录。 下面就是我们使用vuex来实现登录凭证的存储
vuex管理登录权证信息存储
- token 存入 vuex 的好处,易获取,响应式
- vuex 需要分模块 => user 模块
- 创建
store/modules/user.js
模块 - 创建模板数据
// 用户信息模块的公共数据模块 | |
export default { | |
namespaced: true, | |
// 存储数据 | |
state() { | |
return { | |
// 暂时提供的数据 | |
userInfo: { | |
token: '', | |
userId: '' | |
} | |
} | |
}, | |
// 从state中筛选出符合条件的一些数据(必须要有返回值, 并且第一个参数必须是state) | |
getters: { | |
}, | |
// 对象中存放的是修改state的方法(所有的第一个参数必须是state, 然后才是形参) | |
mutations: { | |
setUserInfo(state, obj){ | |
state.userInfo = obj | |
} | |
}, | |
// actions负责进行异步操作, 一般需要调用mutation中的方法 | |
actions: { | |
}, | |
} |
- 在
store/index.js
中挂载user模块
import Vue from 'vue' | |
import Vuex from 'vuex' | |
import user from './modules/user' | |
Vue.use(Vuex) | |
export default new Vuex.Store({ | |
state: { | |
}, | |
getters: { | |
}, | |
mutations: { | |
}, | |
actions: { | |
}, | |
// 挂载模块 | |
modules: { | |
user | |
} | |
}) |
- 页面中进行调用
在login/index.vue
中进行调用
// 1. 导入mapMutations | |
import { mapMutations } from 'vuex' | |
// 2. 在methods中使用 | |
methods: { | |
...mapMutations('user', ['setUserInfo']), | |
//3. 调用请求信息, 并且存储 | |
loginFn(){ | |
const res = await loginClick(this.isParty, this.mobile, this.partyData, this.smsCode) | |
console.log(res) | |
//2.1 将登录信息存储到state中 | |
this.setUserInfo(res.data) | |
} | |
} | |
调用的注意事项:
vuex的持久化处理
目标:封装 storage 存储模块,利用本地存储,进行 vuex 持久化处理 问题1:vuex 刷新会丢失,怎么办?
// 将token存入本地 | |
localStorage.setItem('hm_shopping_info', JSON.stringify(xxx)) |
在utils/storage.js
中
// 持久化存储信息 | |
const INFO_KEY = 'hm_shopping_info' | |
// 获取个人信息 | |
export const getInfo = () => { | |
const result = localStorage.getItem(INFO_KEY) | |
return result ? JSON.parse(result) : { | |
token: '', | |
userId: '' | |
} | |
} | |
// 设置个人信息 | |
export const setInfo = (info) => { | |
localStorage.setItem(INFO_KEY, JSON.stringify(info)) | |
} | |
// 移除个人信息 | |
export const removeInfo = () => { | |
localStorage.removeItem(INFO_KEY) | |
} |
修改之前的store/modules/user.js
中的内容
// 用户信息模块的公共数据模块 | |
import { getInfo, setInfo } from "@/utils/storage" | |
export default { | |
namespaced: true, | |
// 存储数据 | |
state() { | |
return { | |
// 即使获取的内容为空, 他也会帮我们创建一个新的空对象 | |
userInfo: getInfo() | |
} | |
}, | |
// 从state中筛选出符合条件的一些数据(必须要有返回值, 并且第一个参数必须是state) | |
getters: { | |
}, | |
// 对象中存放的是修改state的方法(所有的第一个参数必须是state, 然后才是形参) | |
mutations: { | |
setUserInfo(state, obj){ | |
state.userInfo = obj | |
setInfo(obj) | |
} | |
}, | |
// actions负责进行异步操作, 一般需要调用mutation中的方法 | |
actions: { | |
}, | |
} |
路由导航守卫
对于有些模块需要登录凭证, 但是有些模块又不需要, 因为我们是实现的商城项目 ,所以登录凭证只有在用户进入购物车或者个人信息模块的时候使用。 其他模块直接放行即可。 所以这里就引入了路由导航守卫, 用来实现请求的过滤,对于需要登录才能访问的需要跳转到登录模块
官网地址: 全局前置守卫
**路由导航守卫 **
- 所有的路由一旦被匹配到,都会先经过全局前置守卫
- 只有全局前置守卫放行,才会真正解析渲染组件,才能看到页面内容
router.beforeEach((to, from, next) => { | |
// 1. to 往哪里去, 到哪去的路由信息对象 | |
// 2. from 从哪里来, 从哪来的路由信息对象 | |
// 3. next() 是否放行 | |
// 如果next()调用,就是放行 | |
// next(路径) 拦截到某个路径页面 | |
}) |
官网内容
因此, 我们需要对于那些需要访问权限的页面增加守卫前置
在router/index.js
中就可以定义我们路由的内容
const router = new VueRouter({ | |
// 1. 配置路由规则 | |
routes: [ | |
] | |
}) | |
// 定义数组存放需要用户登录才能访问的页面 | |
const authUrl = ['/pay','/myorder'] | |
// 配置全局导航守卫 | |
router.beforeEach((to , from, next) => { | |
// 1. to 往哪里去, 到哪去的路由信息对象 | |
// 2. from 从哪里来, 从哪来的路由信息对象 | |
// 3. next() 是否放行 | |
// 如果next()调用,就是放行 | |
// next(路径) 拦截到某个路径页面 | |
// 从全局数据中查看是否存在token = store.state.user.userInfo.token | |
// 是否是我们用户需要登录才能访问的页面 | |
// 如果不是, 那么就直接 next() 放行 | |
// 通过getters封装我们获取token的请求 | |
const token = store.getters.getToken | |
if(!authUrl.includes(to.path)){ | |
next() | |
return | |
} | |
// 是需要登录才能访问的页面, 拦截请求, 并且跳转到登录页面 | |
if(token){ // 是否存在token | |
next() | |
}else{ | |
next('/login') | |
} | |
}) | |
export default router |
在全局的store存放数据模块的store/index.js
中配置获取token, 这样上面的全局导航守卫中想要获取token就可以直接通过getters获取, 而不是通过原生的store.state.user.userInfo.token
。
getters: { | |
// 配置getters 直接获取token | |
getToken(state){ | |
return state.user.userInfo.token | |
} | |
}, |