通过前端项目Mall-project (https://github.com/Ray2310/MallProject)使得对于vue技术的实现有了大致的了解和使用。 这里我将具体到一个模块的完成, 从而实现对于vue技术在登录模块下的各个方面的细致讲解。
首先,我们按照vue的思想, 通过组件的形式来完成对于项目的code。 因此按照项目的UI图 以及 登录模块的接口文档, 我们将项目划分为以下内容来进行将解

项目UI图
image.png

页面布局之顶部导航

image.png
顶部导航栏, 我们可以通过使用vant中的组件来实现,这样大大减少了code的工作量
首先我们通过使用vant组件库的按需导入, 从而实现压缩项目体积, 提升了项目的加载速度和性能, 同时也可以提升网络请求。
所以这里采用按需导入而不是全部导入。

组件导入实现步骤

  1. 创建utils/vant-ui.js作为专门封装vant组件的js模块, 我们只需要再main.js中导入即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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)
1
2
3
// main.js
// 导入vent中的需要的组件
import '@/utils/vant-ui'
  1. 在页面布局模块views/login/index.vue使用导入的需要的组件NavBar
1
2
3
4
5
6
7
8
9
10
11
12
13
  <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组件库, 有的是我们自己封装实现。 下面就是页面布局的主体部分。就是通过我们自己封装的组件。
image.png

封装组件实现主题部分

其实这个模块也是可以复用的, 下次也就是改改里面的内容即可, 所以这也就是人们常说code就是ctrl+C/V了, 因为coder追求的就是极致的便捷、快速。但是这对于初学者我认为还是不够友好的,因为还没有明白原理便开始CV, 那么也只是咀嚼别人吃过的, 没有自己的思想味道。
回归正题….
主题部分也是在views/login/index.vue中实现的, 只是用了不同的盒子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
	<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位。或者说输入的内容经过我们后端的校验发现是错的,那么我们前端也需要进行提示

校验手机号和图形验证码

1
2
3
4
5
6
7
8
9
10
11
12
13
    // 校验手机号 和 图形验证码输入是否正确
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这个组件就可以进行提示, 所以按照上面的组件导入思路,我们就可以实现下面这样的效果。
image.png
我们进行需要的时候可以直接使用Toast('提示的内容')来实现,而他的本质其实是通过

将方法, 注册挂载到了Vue原型上this.$toast('提示内容')

功能实现之图形验证码

在获取图形验证码之前,我们需要对请求进行封装, 因为随着项目开发的深入, 代码随着堆积成山, 如果不进行封装维护, 那么就会形成别人口中的“始(shi)山代码” ,所以为了我们项目的可维护性,我们需要对请求进行封装

封装所有的请求及其login模块的请求

utils/request.js模块 ,我们将所有的请求都封装到这里, 这样就便于项目的维护
这些内容都可以通过参考axios官网来实现。

请求响应的封装

响应拦截器是咱们拿到数据的 第一个 “数据流转站”,可以在里面统一处理错误,只要不是 200 默认给提示,抛出错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 封装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
2
3
4
5
6
7
8
// 登录相关的接口请求
//1. 获取图形验证码
import request from '@/utils/request'
export const getPicCode = ()=> {
return request.get('/captcha/image')
}
//2. 获取短信验证码的请求接口
//3. 点击登录请求接口

实现图形验证码回显

注意,我们获取图形验证码需要一进入登录页面就需要显示出来, 所以在views/login/index.vue中需要在created(){}方法中实现, 同时, 如果用户看不清图形验证码想要点击图形验证码换一张的时候,我们也需要提供对应的方法来进行实现

1
2
3
4
5
6
7
  <!--  点击重新切换验证码 -->
<img :src="picUrl" @click="getPicCode()" alt="">

async created() {
// 通过调用方法来实现图片验证码的显示
this.getPicCode()
},

通过上述得到的验证码应该是一个 res.data里面的base64就是我们图片的地址, 我们需要将其拿出来渲染到页面上, 所以就需要参数来接收请求得到的内容。 所以就需要定义变量

1
2
3
4
5
6
7
8
9
10
export default{  
data() {
return {
//1. 设置获取图形验证码的参数
piccode: '', // 用户输入的图形验证码
picKey: '', //请求传入图形验证码的唯一标识
picUrl: '', // 存储请求渲染的图片地址
}
}
}

同时在页面中渲染出来

1
2
3
4
5
6
7
8
// 获取图形验证码
async getPicCode(){
// 使用自己封装的axios来使用, 这样就不会污染原始的axios请求
// 将所有的请求全部放到api模块去实现
const res = await getPicCode()
this.picUrl = res.data.base64
this.picKey = res.data.key
},
1
2
3
4
5
<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元了, 所以有些限制是必须有的,我们通过短信验证码的倒计时可以缓冲, 然后后台再对敏感的手机号做限流处理,这样对于网站的防护就上了一个档次,避免了大部分的攻击和恶意注册。

短信验证码的倒计时提醒

实现效果相信大家都见过,这里我们就直接上实现步骤了。
实现思路就是通过定时器来实现, 最后到时间就删除定时器。
image.png

  1. 首先, 设置三个属性作为设置定时器的属性
1
2
3
4
5
6
7
8
data() {
return {
//设置 获取短信验证码 倒计时
totalSecond: 60, // 总秒数
second: 60, // 倒计时的秒数
timer: null // 定时器 id
}
},
  1. 在我们使用的地方通过点击触发定时器 并且设置定时器的显示文字(离开和显示倒计时)
1
2
3
4
5
6
<div class="form-item">
<input class="inp" placeholder="请输入短信验证码" type="text">
<button @click="getCode">
{{ second === totalSecond ? '获取验证码' : second + `秒后重新发送`}}
</button>
</div>
  1. 定时器的js逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 获取短信验证码
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('发送成功,请注意查收')
}
},
  1. 离开页面。 消除定时器
1
2
3
4
destroyed () {
clearInterval(this.timer)
}

校验信息

在发送短信验证码之前我们需要做校验手机号和图形延展面的操作. 这个再之前已经提过了, 这里因为逻辑需要我们再来一边

1
2
3
4
5
6
7
8
9
10
11
12
13
// 校验手机号 和 图形验证码输入是否正确
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()

1
2
3
4
5
6
7
8
9
10
11
12
//2. 在发送短信验证码的时候进行调用请求
// 获取短信验证码
async getCode () {
if(!this.validFn()){
return
}
// 发送短信验证码 并且启动倒计时提醒
if (!this.timer && this.second === this.totalSecond) {
// 逻辑...
}
},

获取短信验证码逻辑

接口信息

image.png

js逻辑

api/login.js模块, 实现获取短信验证码

1
2
3
4
5
6
7
8
9
10
11
12
//1. 在api/login.js中 进行写请求的逻辑
export const getMsCode = (captchaCode,captchaKey,mobile) =>{
// 按照接口文档的要求, 需要传惨
return request.post('/captcha/sendSmsCaptcha',{
form:{
captchaCode,
captchaKey,
mobile
}
})
}

因为根据请求接口, 调用请求的时候需要传入参数, 这样才能够发送验证码。 所以需要在login/index.vue中定义属性来接收用户输入的数据

1
2
3
4
5
6
7
8
9
data() {
return {
//1. 设置获取图形验证码的参数
piccode: '', // 用户输入的图形验证码
picKey: '', //请求传入图形验证码的唯一标识
mobile: '', // 手机号

}
},

实现获取验证码逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//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('发送成功,请注意查收')
}
},


功能实现之封装接口实现登录

接口信息

image.png

实现思路

  1. api/login.js 提供登录 Api 函数
1
2
3
4
5
6
7
8
9
10
11
//3. 点击登录请求接口
export const loginClick = ( isParty,mobile,partyData,smsCode) => {
return request.post('/passport/login',{
form: {
isParty,
mobile,
partyData,
smsCode
}
})
}
  1. login/index.vue中通过点击事件调用请求

注意, 需要绑定 短信验证码。 并且还需要传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 需要接受数据的属性
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管理登录权证信息存储

  1. token 存入 vuex 的好处,易获取,响应式
  2. vuex 需要分模块 => user 模块

image.png

  1. 创建store/modules/user.js模块
  2. 创建模板数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 用户信息模块的公共数据模块
export default {
namespaced: true,
// 存储数据
state() {
return {
// 暂时提供的数据
userInfo: {
token: '',
userId: ''
}
}
},
// 从state中筛选出符合条件的一些数据(必须要有返回值, 并且第一个参数必须是state)
getters: {
},

// 对象中存放的是修改state的方法(所有的第一个参数必须是state, 然后才是形参)
mutations: {
setUserInfo(state, obj){
state.userInfo = obj
}
},
// actions负责进行异步操作, 一般需要调用mutation中的方法
actions: {
},


}

  1. store/index.js中挂载user模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
}
})

  1. 页面中进行调用

login/index.vue中进行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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)
}

}


调用的注意事项:image.png

vuex的持久化处理

目标:封装 storage 存储模块,利用本地存储,进行 vuex 持久化处理
问题1:vuex 刷新会丢失,怎么办?

1
2
// 将token存入本地 
localStorage.setItem('hm_shopping_info', JSON.stringify(xxx))

image.png
utils/storage.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 持久化存储信息
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中的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 用户信息模块的公共数据模块
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: {
},


}

image.png

路由导航守卫

对于有些模块需要登录凭证, 但是有些模块又不需要, 因为我们是实现的商城项目 ,所以登录凭证只有在用户进入购物车或者个人信息模块的时候使用。 其他模块直接放行即可。
所以这里就引入了路由导航守卫, 用来实现请求的过滤,对于需要登录才能访问的需要跳转到登录模块
image.png

官网地址: 全局前置守卫

**路由导航守卫 **

  1. 所有的路由一旦被匹配到,都会先经过全局前置守卫
  2. 只有全局前置守卫放行,才会真正解析渲染组件,才能看到页面内容
1
2
3
4
5
6
7
8
router.beforeEach((to, from, next) => {
// 1. to 往哪里去, 到哪去的路由信息对象
// 2. from 从哪里来, 从哪来的路由信息对象
// 3. next() 是否放行
// 如果next()调用,就是放行
// next(路径) 拦截到某个路径页面
})

官网内容

image.png

因此, 我们需要对于那些需要访问权限的页面增加守卫前置
image.png
router/index.js中就可以定义我们路由的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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

1
2
3
4
5
6
getters: {
// 配置getters 直接获取token
getToken(state){
return state.user.userInfo.token
}
},