零.前置相关内容
1.Toast组件的使用
使用场景
在用户输入手机号后,需要给出提示,若输入正确:“发送成功,请注意查收!”,
若输入不正确:“请输入正确的手机号”
使用步骤
- Toast轻提示组件的注册安装,步骤略(同NavBar)
- 使用方式1----导入调用(组件或非组件内都可以):
Toast("提示内容")
- 使用方式2----通过this直接调用(组件内):
this.$toast("提示内容")
,
这种方法的本质是把方法注册挂载到了Vue原型上:Vue.prototype.$toast=xxx
常用用法
- 文字提示:Toast(‘提示内容’)
- 加载提示:
Toast.loading(){
message:'加载中',
forbidClick:true
}
- 成功/失败提示:Toast.success和Toast.fail
2.为什么要对axios进行二次封装?
省流
使用axios请求后端接口,一般都会对axios进行一些配置,比如配置基础地址,请求&响应拦截器等
所以在项目开发中,都会对axios进行基本的二次封装,单独封装到一个request模块中,便于维护使用
步骤(以获取图形验证码为例)
- 新建封装文件
utils/request.js
import axios from 'axios'
const instance = axios.create({// 创建一个axios实例instance
baseURL: 'http://smart-shop.itheima.net/index.php?s=/api', // 注意操作文档上的基础地址是否变动
timeout: 1000
})
// 请求拦截器:发送请求之前做些什么
instance.interceptors.request.use(config => {
return config// 成功的回调
}, error => {
return Promise.reject(error)// 失败的回调
})
// 响应拦截器:接收返回数据之前做些什么
instance.interceptors.response.use(res => {
return res.data// axios返回数据默认多包一层data,提前剥掉
}, err => {
return Promise.reject(err)
})
// 导出实例
export default instance
- 在页面中试调用(需提前查看阅读文档,
获取请求方式,请求路径和请求参数
)
import request from '@/utils/request.js'
data () {
return {
picUrl: '',
picKey: ''
}
},
async created () {
// const res = await request.get('/captcha/image')
//console.log('获取到的数据:', res)//获取到data,message,status三个属性
const { data: { base64, key } } = await request.get('/captcha/image') // 把需要的res对象解构出来
this.picUrl = base64
this.picKey = key
},
- 用获取到的数据动态渲染登录页面:
<div class="form-item">
<input type="text" class="inp" maxlength="5" placeholder="请输入图形验证码">
<!-- <img src="https://img0.baidu.com/it/u=1838409809,2846524470&fm=253&fmt=auto&app=138&f=JPEG?w=443&h=236"> -->
<img :src="picUrl">
</div>
3.为什么以及如何将请求封装成api接口?
在中大型项目中,最好把请求封装成方法,统一存放到api模块,与页面分离
这么做的理由是:
在上例中,若在页面中类似的请求多了起来,整个页面会充斥着请求的代码,可阅读性较差,而且相同的请求也没有被复用,更没有被统一管理
统一放到api模块后:
将实现请求和页面的分离,复用相同请求并对请求统一管理
步骤:(优化上例)
- 新建请求模块并封装:把之前在页面中发送的请求放在新文件
src/api/login.js
中封装起来
import request from "@/utils/request"
export const getPicCode=()=>{//起一个方法名,以与后面同页面的其他请求做区分
return request.get('/captcha/image');//必须要return,不然获取不到结果
}
- 在页面中
Login/index.js
按需导入并调用
import { getPicCode } from '@/api/login.js'
async created () {
const { data: { base64, key } } = await getPicCode()
this.picUrl = base64
this.picKey = key
},
}
- 优化:考虑到在created中将不止发送一次数据请求,可以把方法写在methods中然后再在created中调用
import { getPicCode } from '@/api/login.js'
created () {
this.getPicCodeMethods()
},
methods: {
async getPicCodeMethods () {
const { data: { base64, key } } = await getPicCode()
this.picUrl = base64
this.picKey = key
}
},
一.登录页静态布局
0.login页分为上下两部分
上面–导航条:使用vant的NavBar作为通用组件,(在几乎每个页面都使用)
下面–主题部分:自定义
1.NavBar组件的使用
- 在官网上查用法(属性和属性值)
- 在utils/vant-ui.js中按需引入
import { NavBar } from 'vant'
Vue.use( NavBar )
- 在Login页中使用
<van-nav-bar title="会员登录" left-arrow @click-left="$router.go(-1)" />
2.登录页主体部分
<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 type="text" class="inp" maxlength="11" placeholder="请输入手机号码">
</div>
<div class="form-item">
<input type="text" class="inp" maxlength="5" placeholder="请输入图形验证码">
<img src="https://img0.baidu.com/it/u=1838409809,2846524470&fm=253&fmt=auto&app=138&f=JPEG?w=443&h=236">
</div>
<div class="form-item">
<input type="text" class="inp" maxlength="5" placeholder="请输入短信验证码">
<button>获取验证码</button>
</div>
</div>
<div class="login-btn">登录</div>
</div>
</div>
</template>
效果:
二.图片验证码相关功能
图片验证码的本质是后台返回的照片,目的是强制人和机器之间的交互,抵御自动化攻击(如:避免批量请求获取短信)
1.封装API接口:获取图片验证码
2.对文本框输入的手机号码和图片验证码进行正则校验
//step1:分别在手机号和图形验证码的input框中,使用v-model双向绑定在data中声明的变量moblie和picCode
//step2:在函数isVaild中分别对两个变量进行正则校验,当它们格式错误时返回false
/*login/index.vue/methods*/
isValid () { // 正则校验:手机号和图形验证码
// 1.手机号的正则:以1开头,第2位3`9,一共11位数字
if (!/^1[3-9]\d{9}$/.test(this.mobile)) {
this.$toast('请输入正确的手机号!')
return false
}
// 2.图形验证码的正则:4个字符
if (!/^\w{4}/.test(this.picCode)) {
this.$toast('请输入正确的图形验证码!')
return false
}
// 其他情况则通过
return true
},
//step3:调用isValid函数
//在哪里调用?==>点击发送短信,或点击登录按钮时调用(后续内容)
三.短信验证码相关功能
1.实现倒计时效果:点击"获取验证码",开始倒计时59s
业务要求:
- 输入的手机号和图形验证码要正确
- 1分钟内倒计时只会触发一次
- 按钮的文字不是固定的,倒计时开始后变成"重新获取59s"
思路:
对"获取验证码"按钮绑定点击事件getCode,声明三个新变量totalSec,nowSec,timer
,分别代表总秒数,当前秒数和定时器的名字
判断:当前timer定时器并未开启,且总秒数=当前秒数时,开启定时器
让nowSec每秒自减,当它的值小于等于0时清空定时器并初始化变量
// 点击开启倒计时
getCode () {
if(!this.isVaild()) return//手机号和图形验证码格式不对,不继续往下了
if (!this.timer && this.totalSec === this.nowSec) {
this.$toast('短信发送成功,请注意查收!')
this.timer = setInterval(() => {// 1分钟内倒计时只会触发一次
console.log(this.nowSec--)
if (this.nowSec <= 0) {
clearInterval(this.timer)
this.timer = null
this.nowSec = this.totalSec
}
}, 1000)
}
优化:"获取验证码"按钮的文本从写死变成动态渲染
<button @click="getCode">{{this.nowSec==this.totalSec?'获取验证码':`重新获取${this.nowSec}`}}</button>
优化:为避免性能浪费,加入业务逻辑—当用户离开登录页时,关闭定时器
destroyed () {
clearInterval(this.timer)
}
2.封装API接口:获取短信验证码
- 查看接口文档,确定请求方式,请求路径,请求参数
- 封装请求
//api/login.js
......
//获取短信验证码
export const getMsgCode=(captchaCode, captchaKey, mobile)=>{
return request.post('/captcha/sendSmsCaptcha',{
form:{
captchaCode, //图形验证码
captchaKey, //图形验证码key
mobile //手机号
}
)
}
- 页面调用
import { getPicCode, getMsgCode } from '@/api/login.js'
...
methods:{
async getMsgCodeMethods(){//在getCode方法中调用
const res=await getMsgCode(this.picCode,this.picKey,this.mobile);
}
}
.
四.登录相关功能
1.封装API接口:验证码登录请求
- 查看接口文档,确定请求路径,请求方式,请求参数
此处为啥图形验证码不用传?
为什么只要手机号mobile和短信验证码smsCode进行校验?
图形验证码已经传过了(在点击获取短信验证码之前,图片验证码要填对),用于获取短信验证码的时候
----只要能拿到短信验证码就说明图形验证码已经验证过了
- 封装请求
//api/login.js
....
// 验证码登录请求
export const codeLogin = (mobile, smsCode) => {
return request.post('/passport/login', {
form: {
// 无关参数--给默认值
isParty: false,
partyData: {},
// 有关参数--传参
mobile,
smsCode
}
})
}
- 页面调用
import { getPicCode, getMsgCode, codeLogin } from '@/api/login.js'
...
// 页面调用:验证码登录
async codeLoginMethods () {
const res = await codeLogin(this.mobile, this.smsCode)
console.log('接收的用户登录信息res:', res.data)
},
//在哪里调用?
//后续:点击登录按钮时调用:this.codeLoginMethods()
踩坑:res.data里面什么也没有.原因:短信验证码的参数名定义为smsCode ,调用时写成了msgCode
正确输出:userId
和token
2.点击登录:登录前校验+获取登录用户数据+登录成功后跳转
/*输入短信验证码的文本框双向绑定smsCode*/
// 点击登录
login () {
if (!this.isVaild()) return // 正则校验:手机号和图形验证码
if (!/^\d{6}$/.test(this.smsCode)) { // 正则校验:短信验证码
this.$toast('请输入正确的短信验证码')
return
}
this.codeLoginMethods()// 调用方法存入用户信息:获取到了token和userId,后面有用
this.$router.push('/')//登录成功后跳转
}
Tips:校验阶段data中的变量写上默认值,免得每次验证都手动输入手机号和验证码
五.优化
1.使用响应拦截器对接口的错误提示统一处理
为什么在响应拦截器处理错误?
在短信验证码功能和点击登录功能等业务功能的实现中,错误情况没有处理
当用户发送的请求被响应(响应1,响应2,响应3…)回来的时候,在被业务代码处理之前,
响应拦截器作为返回数据的第一个数据流转站,可以在里面统一处理错误,才会交还给业务代码
如何处理?
// 响应拦截器:接收返回数据之前做些什么
instance.interceptors.response.use(res => {
// 统一处理错误:对状态码做出判断
if (res.data.status !== 200) {
Toast(res.data.message)// 1.给提示
return Promise.reject(res.data.message)// 2.抛出一个错误
} else {
return res.data// axios返回数据默认多包一层data,提前剥掉
}
}, err => {
return Promise.reject(err)
})
2.使用vuex存储登录认证的信息
最后获取到的用户的token和userId,应该用vuex构建user模块,以一个对象的形式存起来
- 创建
store/modules/user.js
子模块
export default {
namespaced:true,//声明命名空间
state(){
return{
userInfo:{
userId:'',
token:''
}
}
},
mutations:{
setUserInfo(state,obj){
state.userInfo=obj
}
}
}
- 在
store/index.js
中引入子模块user.js
import user from '@/store/modules/user'
...
modules:{
user
}
- 调用,将在页面中获取到的
res.data
存到userInfo
中
Login/index.vue
// 页面调用:验证码登录
async codeLoginMethods () {
const res = await codeLogin(this.mobile, this.smsCode)
// console.log('接收的用户登录信息res:', res.data)//返回一个对象,有userId和token属性
this.$store.commit('user/getUserInfo', {
token: res.data.token,
userId: res.data.userId
})
console.log(this.$store.state.user.userInfo)// 存入成功
},
3.使用storage对vuex数据进行持久化处理
现阶段vuex数据刷新就会丢失,这不符合项目需求,需要将token存本地
做法是创建storage模块,封装本地存储的增删改查的操作
- step1:封装
store/modules/storage.js
模块
const infoKey=''//声明存入对象
// 存
export const setInfo=(info)=>{
localStorage.setItem('infokey',JSON.stringifg(info))
}
// 取
export const getItem=()=>{
const result = localStorage.getItem(infoKey)
return result?JSON.parse(result):{userId:'',token:''}
}
// 删
const const removeItem=()=>{
localstorage.removeItem(infoKey)
}
- step2:在
modules/user.js
中调用
//按需导入
import { setInfo , getInfo } from '@store/modules/storage'
...
state(){
return{
// userInfo: {
// userId: '',
// token: ''
// }
userInfo: getInfo()
}
},
mutations: {
setUserInfo (state, obj) {
state.userInfo = obj// 存入vuex
setInfo(obj)// 存入本地
}
}
4.使用Toast.loading()
添加loading效果
使用loading提示优化页面,可以避免请求的时候出现因为网络问题而没有响应
的情况,此外它还有这些优化效果:
- 节流处理:防止用户在一次请求后,数据还没回来之前,多次点击,发送无效请求
- 友好提示:告知用户在加载中,优化用户体验
loading提示可以在多个场景中使用(添加购物车,用户结算,点击登录等),因此可以在拦截器中设置loading效果
- 在请求拦截器中,每次请求,就打开loading
- 在响应拦截器中,每次响应,就关闭loading
// utils.request.js 完整代码
import axios from 'axios'
import { Toast } from 'vant'
// 创建一个axios实例instance
const instance = axios.create({
// 注意操作文档上的基础地址是否变动
baseURL: 'http://smart-shop.itheima.net/index.php?s=/api',
timeout: 1000,
headers: { platform: 'H5' }// 状态码返回500的时候添加该请求头
})
// 请求拦截器:发送请求之前做些什么
instance.interceptors.request.use(config => {
// 开启loading,禁止背景点击
Toast.loading({
message: '加载中...',
forbidClick: true, // 禁止背景点击
loadingType: 'spinner', // 图标
duration: 0// 不会自动消失
})
return config// 成功的回调
}, error => {
return Promise.reject(error)// 失败的回调
})
// 响应拦截器:接收返回数据之前做些什么
instance.interceptors.response.use(res => {
// 统一处理错误:对状态码做出判断
if (res.data.status !== 200) { // 状态码错误
Toast(res.data.message)// 1.给提示
return Promise.reject(res.data.message)// 2.抛出一个错误
} else { // 状态码正确
// 关闭loading
Toast.clear()
}
return res.data// axios返回数据默认多包一层data,提前剥掉
}, err => {
return Promise.reject(err)
})
// 导出实例
export default instance
5.使用全局前置守卫实现对页面的访问拦截
场景
商城页面中大部分页面都能进行游客访问,仅在需要登录才能进行的操作,会提示并跳转到登录页.但在需要用户双概述才能访问的页面比如支付页,订单页等,游客不能进入该页面.需要做拦截处理
思路
- 所有的路由一旦被匹配到,都会经过全局前置守卫
- 只有全局前置守卫放行,才会真正解析渲染组件,才能看到页面内容
如何判断拦截还是放行?
用户是否有登录权证token
跳转路由时,触发全局前置守卫(判断:是否访问权限页面)
否--直接放行
是--判断:是否有token?
有--直接放行
无--拦截到登录
语法
官网位置:https://v3.router.vuejs.org/zh/guide/advanced/navigation-guards.html
router.beforeEach((to,from,next)=>{
//1.to:往哪里去,到哪去的路由信息对象
//2.from:从哪里来,从哪来的路由信息对象
//3.next():是否放行
- next()调用--放行
- next(路径)--拦截:转到某个路径页面
})
代码实现
- router/index.js
const authUrls = ['/pay', '/myorder']// 定义一个数组用于存放权限页面
// 全局前置守卫
router.beforeEach((to, from, next) => {
// 判断:访问的是权限页面吗?
if (!authUrls.includes(to.path)) {
next()// 直接放行
return
}
// 访问权限页面时
// 判断:有token吗?
const token = store.state.user.userInfo.token
// const token = store.getters.token
console.log('获取token', token)
if (token) { // 有:放行
next()
} else { // 没有:拦截并转到登录页
next('/login')
}
})
效果验证
- 未登录时:地址栏输入pay页地址,回车后会自动转到登录页
- 登录时(localStorage本地存储中有token数据):输入pay页地址可以顺利进入