# 登录模块
# 回顾
- 关于参数传递参数props需要设置为true,才可以再组件中通过props得到路径参数
- 接口路径传参和路由的参数传递
- 熟悉项目的业务场景
- 初始化项目
- Vant基本用法
- 移动端适配(原理:基于Rem)
- 配置Vant相关适配插件
- postcss-pxtorem 把px单位转换为rem单位
- 插件二:根据屏幕的尺寸计算HTML的font-size基准值并进行设置(计算公式=屏幕尺寸/份数)
- 整体路由配置
- 首页基本布局
- 全局样式设置
- 封装token的操作
- 封装并重构store的模块化
- 封装通用的接口调用方法
- 基本封装
- 请求拦截器
- 响应拦截器
# 封装请求API业务模块
目标:封装登录接口的业务模块
- 封装登录接口业务模块
import request from '@/utils/request.js'
// 登录验证
export const login = (data) => {
return request({
method: 'post',
url: 'app/v1_0/authorizations',
data: data
})
}
- 组件中调用业务模块方法
import { login } from '@/api/login.js'
// 处理异步任务
actions: {
// 实现登录的action
async login (context, payload) {
try {
// 调用登录接口
const ret = await login(payload)
// 调用mutation,更新用户信息
context.commit('updateUserInfo', ret.data)
if (ret && ret.data && ret.data.token) {
// 登录成功
return true
}
} catch (e) {
console.log(e)
// 告诉页面组件登录失败了
return false
}
}
},
# 页面基本布局
目标:实现登录页面基本布局
<div class="page-login">
<van-nav-bar title="登 录"></van-nav-bar>
<van-cell-group>
<van-field label="手机号" placeholder="请输入手机号" />
<van-field label="验证码" placeholder="请输入验证码">
<van-button class="p5" slot="button" size="mini" type="primary">
发送验证码
</van-button>
</van-field>
</van-cell-group>
<div class="btn_box">
<van-button type="info" block round>登 录</van-button>
</div>
</div>
# 实现基本登录功能
目标:实现登录的基本功能
- 表单数据绑定
- 提交按钮事件绑定
- 触发Action
import { mapActions } from 'vuex'
methods: {
...mapActions('login', ['toLogin']),
async handleLogin () {
// 处理登录流程
const ret = await this.toLogin(this.loginForm)
if (ret) {
// 登录成功
this.$router.push('/')
} else {
// 登录失败
this.$toast.fail('手机号或验证码错误')
}
}
}

# 表单验证
目标:能够自己实现表单验证(vant本身不支持表单验证)
- 绑定错误消息
:error-message="errorMsg.mobile"
<van-cell-group>
<van-field @blur='validate("mobile")' :error-message='mobileError' v-model='loginForm.mobile' label="手机号" placeholder="请输入手机号" />
<van-field @blur='validate("code")' :error-message='codeError' v-model='loginForm.code' label="验证码" placeholder="请输入验证码">
<van-button class="p5" slot="button" size="mini" type="primary">
发送验证码
</van-button>
</van-field>
</van-cell-group>
<div class="btn_box">
<van-button type="info" @click="handleLogin" block round>登 录</van-button>
</div>
- @blur 失去焦点,做校验。
data () {
return {
mobileError: '',
codeError: '',
// 表单数据
loginForm: {
mobile: '',
code: ''
}
}
},
methods: {
...mapActions('login', ['toLogin']),
validate (type) {
// 获取手机号或者验证码的值
const value = this.loginForm[type]
if (type === 'mobile') {
// 验证手机号
if (!value) {
// 手机号非空验证
this.mobileError = '手机号不能为空'
} else if (!/^1[3-9]\d{9}$/.test(value)) {
// 手机号格式验证
this.mobileError = '手机号格式错误'
} else {
// 验证通过
this.mobileError = ''
}
} else if (type === 'code') {
// 验证码
if (!value) {
// 手机号非空验证
this.codeError = '验证码不能为空'
} else if (!/^\d{6}$/.test(value)) {
// 验证码格式验证
this.codeError = '验证码格式错误'
} else {
// 验证通过
this.codeError = ''
}
}
},
async handleLogin () {
// 实现表单验证
this.validate('mobile')
this.validate('code')
if (!(this.mobileError === '' && this.codeError === '')) {
// 其中之一没有验证通过,终止请求动作
return
}
// 处理登录流程
const ret = await this.toLogin(this.loginForm)
if (ret) {
// 登录成功
this.$router.push('/')
} else {
// 登录失败
this.$toast.fail('手机号或验证码错误')
}
}
}
当然也可以使用插件,vee-validate http://vee-validate.logaretm.com/
# 登录按钮提示
目标:实现登录按钮提示效果(防止重复提交)
- 通过vant按钮的loading属性实现提示效果
- 点击按钮时开启加载状态
- 完成操作后关闭加载状态,并且表单验证失败时也要关闭加载状态
<van-button :loading='loading' @click='handleLogin' type="info" block>登录</van-button>
# 路由导航守卫
目标:通过导航守卫控制登录权限
- 配置路由的导航守卫,统一验证路由权限
// 导航守卫
// 白名单列表(没有登录也可以访问)
const whiteList = ['/home', '/video']
router.beforeEach((to, from, next) => {
const userInfo = store.state.login.userInfo
if (to.path === '/login') {
// 访问登录页面
next()
} else if (userInfo && userInfo.token && userInfo.refresh_token) {
// 访问非登录页面,并且有token
next()
} else if (whiteList.includes(to.path)) {
// 判断访问的路由路径是否在白名单里面
next()
} else {
// 访问非登录页面,但是没有token
next('/login')
}
})
# token续签问题
目标:处理token失效问题
token是什么时候生成的?第一次登陆成功后服务器生成并返回给客户端,客户端需要缓存token(服务器不存储普通的token)
如果服务器不存储token,那么服务器如何验证token是否有效?服务器可以解析token进行验证
如果token的有效期是1个小时,服务器是否可以再30分钟的时候,作废客户端的token?做不到
但是,现在我们希望随时可以作废客户端的token
所以引入了refresh_token 的机制(双token)
现在需要理解refresh_token的运行机制:
- 普通token有效期设置的比较短(5分钟)
- 为什么这个token的有效性设置这么短?保证安全
- refresh_token有效期比较长(14天)
- 如果这个token丢失了,别人是否可以无限刷新有效的普通token?理论上是可以的
- 那么,服务器是否应该有一种机制:可以随时作废refresh_token?有
- 服务器其实会把refresh_token存在数据库中,当客户端通过refresh_token申请普通token时,服务器会查询数据库中是否有这个有效的refresh_token,如果有,就返回正常的普通token,如果没有,那么就不会返回有效的普通token。
- 如果普通token过期了,那么可以通过refresh_token申请一个新的普通token
- 如果没有refresh_token,那么当普通token过期后,客户端就会跳转到登录页,那么频繁让用户去登录,体验不好,所以可以通过refresh_token自动更新一个新的有效的普通token,从而可以正常获取业务数据。
- 通过频道接口调用营造一个测试环境
src/api/api-channel.js
/*
封装频道获取接口
*/
import request from '@/utils/request.js'
export const getAllChannels = () => {
return request({
method: 'get',
url: 'user/channels'
})
}
// 文件的路径 src/views/home/Index.vue
<script>
import { getAllChannels } from '@/api/api-channel.js'
export default {
name: 'Home',
created () {
getAllChannels()
}
}
</script>
- token有效期分析
token用于访问需要身份认证的普通接口,有效期2小时refresh_token用于在token过期后,获取新的用户token,有效期14天
- 功能实现
/*
通用的接口调用方法
*/
import axios from 'axios'
import store from '@/store/index.js'
import router from '@/router/index.js'
import JSONBIGINT from 'json-bigint'
// import { createNamespacedHelpers } from 'vuex'
// createNamespacedHelpers的参数是模块的名称
// const { mapState } = createNamespacedHelpers('login')
// mapState(['userInfo']).userInfo
const baseURL = 'http://api-toutiao-web.itheima.net/'
// 创建专门的axios请求实例对象
const instance = axios.create({
baseURL: baseURL,
// 配置返回数据的格式
transformResponse: [(data) => {
// data参数表示后端返回的原始数据
try {
return JSONBIGINT.parse(data)
} catch (e) {
console.log(e)
// 这里表示如果转换错误,那么就返回原始数据(不做转换)
return data
}
}]
})
// 请求拦截器
instance.interceptors.request.use((config) => {
// 统一添加请求头(登录成功后添加)
// store中的状态 -> 模块名称 --> 状态属性
// store.state.login.userInfo
const userInfo = store.state.login.userInfo
if (userInfo && userInfo.token) {
// 登录成功,添加请求头
config.headers.authorization = 'Bearer ' + userInfo.token
}
return config
}, (err) => {
return Promise.reject(err)
})
// 响应拦截器
instance.interceptors.response.use((response) => {
return response.data
}, async (err) => {
if (err.response.status === 401) {
try {
// 获取用户的token信息
const userInfo = store.state.login.userInfo
// token失效了,通过refreshtoken申请一个新的token
const ret = await axios.put(baseURL + 'app/v1_0/authorizations', null, {
headers: {
authorization: 'Bearer ' + userInfo.refresh_token
}
})
// 替换之前的token,触发mutation
userInfo.token = ret.data.data.token
store.commit('login/updateUserInfo', userInfo)
// 把刚才失败的请求用新的token再重新发送
return instance(err.config)
} catch (e) {
console.log(e)
// 跳转到登录页面,删除用户信息
store.commit('login/deleteUserInfo')
router.push('/login')
}
}
return Promise.reject(err)
})
// 封装通用的接口调用方法
export default (options) => {
return instance({
method: options.method || 'get',
url: options.url || '#',
data: options.data,
params: options.params,
headers: options.headers
})
}