获取用户资料接口和token注入
目标
封装获取用户资料的资料信息
上小节中,我们完成了头部菜单的基本布局,但是用户的头像和名称并没有,我们需要通过接口调用的方式获取当前用户的资料信息
获取用户资料接口
在src/api/user.js
中封装获取用户资料的方法
/** * 获取用户的基本资料 * * **/ export function getUserInfo() { return request({ url: '/sys/profile', method: 'post' }) }
我们忽略了一个问题!我们的headers参数并没有在这里传入,为什么呢
headers中的Authorization相当于我们开门(调用接口)时钥匙(token)
,我们在打开任何带安全权限的门的时候都需要钥匙(token)
如图
每次在接口中携带钥匙(token)
很麻烦,所以我们可以在axios拦截器中统一注入token
统一注入token src/utils/request.js
service.interceptors.request.use(config => { // 在这个位置需要统一的去注入token if (store.getters.token) { // 如果token存在 注入token config.headers['Authorization'] = `Bearer ${store.getters.token}` } return config // 必须返回配置 }, error => { return Promise.reject(error) })
本节任务
: 完成获取用户资料接口和token注入
封装获取用户资料的action并共享用户状态
目标
: 在用户的vuex模块中封装获取用户资料的action,并设置相关状态
用户状态会在后续的开发中,频繁用到,所以我们将用户状态同样的封装到action中
封装获取用户资料action action
src/store/modules/user.js
import { login, getUserInfo } from '@/api/user' // 获取用户资料action async getUserInfo (context) { const result = await getUserInfo() // 获取返回值 context.commit('setUserInfo', result) // 将整个的个人信息设置到用户的vuex数据中 return result // 这里为什么要返回 为后面埋下伏笔 }
同时,配套的我们还进行了关于用户状态的mutations方法的设计
初始化state state
const state = { token: getToken(), // 设置token初始状态 token持久化 => 放到缓存中 userInfo: {} // 定义一个空的对象 不是null 因为后边我要开发userInfo的属性给别人用 userInfo.name }
userInfo为什么我们不设置为null,而是设置为 {}
因为我们会在getters
中引用userinfo的变量,如果设置为null,则会引起异常和报错
设置和删除用户资料 mutations
// 设置用户信息 setUserInfo(state, userInfo) { state.userInfo = { ...userInfo } // 用 浅拷贝的方式去赋值对象 因为这样数据更新之后,才会触发组件的更新 }, // 删除用户信息 reomveUserInfo(state) { state.userInfo = {} }
同学们,我们将所有的资料设置到了userInfo这个对象中,如果想要取其中一个值,我们还可以在getters中建立相应的映射
因为我们要做映射,如果初始值为null,一旦引用了getters,就会报错
建立用户名的映射 src/store/getters.js
const getters = { sidebar: state => state.app.sidebar, device: state => state.app.device, token: state => state.user.token, name: state => state.user.userInfo.username // 建立用户名称的映射 } export default getters
到现在为止,我们将用户资料的action => mutation => state => getters 都设置好了, 那么我们应该在什么位置来调用这个action呢 ?
别着急,先提交代码,下个小节,我们来揭晓答案
提交代码
本节任务
封装获取用户资料的action并共享用户状态
权限拦截处调用获取资料action
目标
在权限拦截处调用aciton
权限拦截器调用action
在上小节中,我们完成了用户资料的整个流程,那么这个action在哪里调用呢?
用户资料有个硬性要求,必须有token
才可以获取,那么我们就可以在确定有token的位置去获取用户资料
由上图可以看出,一旦确定我们进行了放行,就可以获取用户资料
调用action src/permission.js
if(!store.state.user.userInfo.userId) { await store.dispatch('user/getUserInfo') }
如果我们觉得获取用户id的方式写了太多层级,可以在vuex中的getters中设置一个映射 src/store/getters.js
userId: state => state.user.userInfo.userId // 建立用户id的映射
代码就变成了
if (!store.getters.userId) { // 如果没有id这个值 才会调用 vuex的获取资料的action await store.dispatch('user/getUserInfo') // 为什么要写await 因为我们想获取完资料再去放行 }
此时,我们可以通过dev-tools工具在控制台清楚的看到数据已经获取
最后一步,只需要将头部菜单中的名称换成真实的用户名即可
获取头像接口合并数据
头像怎么办?
我们发现头像并不在接口的返回体中(接口原因),我们可以通过另一个接口来获取头像,并把头像合并到当前的资料中
封装获取用户信息接口 src/api/user.js
/** * * * 获取用户的基本信息 现在写它 完全是为了显示头像 * **/ export function getUserDetailById(id) { return request({ url: `/sys/user/${id}` }) }
这个接口需要用户的userId,在前一个接口处,我们已经获取到了,所以可以直接在后面的内容去衔接
import { login, getUserInfo, getUserDetailById } from '@/api/user' // 获取用户资料action async getUserInfo(context) { const result = await getUserInfo() // result就是用户的基本资料 const baseInfo = await getUserDetailById(result.userId) // 为了获取头像 const baseResult = { ...result, ...baseInfo } // 将两个接口结果合并 // 此时已经获取到了用户的基本资料 迫不得已 为了头像再次调用一个接口 context.commit('setUserInfo', baseResult) // 提交mutations // 加一个点睛之笔 这里这一步,暂时用不到,但是请注意,这给我们后边会留下伏笔 return baseResult }
为了更好地获取头像,同样可以把头像放于getters中
staffPhoto: state => state.user.userInfo.staffPhoto // 建立用户头像的映射
此时,我们的头像和名称已经获取到了,可以直接将之前的假数据换成真正的头像和名称
用户名 layout/components/Navbar.vue
...mapGetters([ 'sidebar', 'name', 'staffPhoto' ]) <img :src="staffPhoto" class="user-avatar"> <span class="name">{ { name }}</span>
通过设置,用户名已经显示,头像依然没有显示,这是因为虽然有地址,但是地址来源是私有云,目前已经失效,所以需要额外处理下图片的异常
至于处理图片的异常,我们在下一节中,可采用自定义指令的形式来进行处理
本节任务
:实现权限拦截处调用获取资料action
自定义指令-解决异常图片情况
目标
: 通过自定义指令的形式解决异常图片的处理
自定义指令
注册自定义指令
Vue.directive('指令名称', { // 会在当前指令作用的dom元素 插入之后执行 // options 里面是指令的表达式 inserted: function (dom,options) { } })
自定义指令可以采用统一的文件来管理
src/directives/index.js
,这个文件负责管理所有的自定义指令
首先定义第一个自定义指令 v-imagerror
export const imageerror = { // 指令对象 会在当前的dom元素插入到节点之后执行 inserted(dom, options) { // options是 指令中的变量的解释 其中有一个属性叫做 value // dom 表示当前指令作用的dom对象 // dom认为此时就是图片 // 当图片有地址 但是地址没有加载成功的时候 会报错 会触发图片的一个事件 => onerror dom.onerror = function() { // 当图片出现异常的时候 会将指令配置的默认图片设置为该图片的内容 // dom可以注册error事件 dom.src = options.value // 这里不能写死 } } }
在main.js完成自定义指令全局注册
然后,在main.js
中完成对于该文件中所有指令的全局注册
import * as directives from '@/directives' // 注册自定义指令 // 遍历所有的导出的指令对象 完成自定义全局注册 Object.keys(directives).forEach(key => { // 注册自定义指令 Vue.directive(key, directives[key]) })
针对上面的引入语法 import * as 变量
得到的是一个对象{ 变量1:对象1,变量2: 对象2 ... }
, 所以可以采用对象遍历的方法进行处理
指令注册成功,可以在navbar.vue
中直接使用了
<img v-imageerror="defaultImg" :src="staffPhoto" class="user-avatar">
data() { return { defaultImg: require('@/assets/common/head.jpg') } },
本节任务
:实现一个自定义指令,解决图片加载异常的问题
实现登出功能
目标
:实现用户的登出操作
登出仅仅是跳到登录页吗?
不,当然不是,我们要处理如下
同样的,登出功能,我们在vuex中的用户模块中实现对应的action
登出action src/store/modules/user.js
// 登出的action logout(context) { // 删除token context.commit('removeToken') // 不仅仅删除了vuex中的 还删除了缓存中的 // 删除用户资料 context.commit('removeUserInfo') // 删除用户信息 }
头部菜单调用action src/layout/components/Navbar.vue
async logout() { await this.$store.dispatch('user/logout') // 这里不论写不写 await 登出方法都是同步的 this.$router.push(`/login`) // 跳到登录 }
注意
我们这里也可以采用vuex中的模块化引入辅助函数
import { mapGetters, createNamespacedHelpers } from 'vuex' const { mapActions } = createNamespacedHelpers('user') // 这是的mapAction直接对应模块下的action辅助函数 methods: { ...mapActions(['lgout']), }
增加action src/store/modules/user.js
import router from '@/router' ... // 登出的action logout(context) { // 删除token context.commit('removeToken') // 不仅仅删除了vuex中的 还删除了缓存中的 // 删除用户资料 context.commit('removeUserInfo') // 删除用户信息 }, // 登出 lgout(context){ context.dispatch('logout') router.push(`/login`) // 跳到登录 }
以上代码,实际上直接对user模块下的action进行了引用,
提交代码
本节任务
: 实现登出功能
Token失效的主动介入
目标
: 处理当token失效时业务
主动介入token处理的业务逻辑
开门的钥匙不是一直有效的,如果一直有效,会有安全风险,所以我们尝试在客户端进行一下token的时间检查
具体业务图如下
流程图转化代码
流程图转化代码 src/utils/auth.js
const timeKey = 'hrsaas-timestamp-key' // 设置一个独一无二的key // 获取时间戳 export function getTimeStamp() { return Cookies.get(timeKey) } // 设置时间戳 export function setTimeStamp() { Cookies.set(timeKey, Date.now()) }
src/utils/request.js
import axios from 'axios' import store from '@/store' import router from '@/router' import { Message } from 'element-ui' import { getTimeStamp } from '@/utils/auth' const TimeOut = 3600 // 定义超时时间 const service = axios.create({ // 当执行 npm run dev => .evn.development => /api => 跨域代理 baseURL: process.env.VUE_APP_BASE_API, // npm run dev => /api npm run build => /prod-api timeout: 5000 // 设置超时时间 }) // 请求拦截器 service.interceptors.request.use(config => { // config 是请求的配置信息 // 注入token if (store.getters.token) { // 只有在有token的情况下 才有必要去检查时间戳是否超时 if (IsCheckTimeOut()) { // 如果它为true表示 过期了 // token没用了 因为超时了 store.dispatch('user/logout') // 登出操作 // 跳转到登录页 router.push('/login') return Promise.reject(new Error('token超时了')) } config.headers['Authorization'] = `Bearer ${store.getters.token}` } return config // 必须要返回的 }, error => { return Promise.reject(error) }) // 响应拦截器 service.interceptors.response.use(response => { // axios默认加了一层data const { success, message, data } = response.data // 要根据success的成功与否决定下面的操作 if (success) { return data } else { // 业务已经错误了 还能进then ? 不能 ! 应该进catch Message.error(message) // 提示错误消息 return Promise.reject(new Error(message)) } }, error => { Message.error(error.message) // 提示错误信息 return Promise.reject(error) // 返回执行错误 让当前的执行链跳出成功 直接进入 catch }) // 是否超时 // 超时逻辑 (当前时间 - 缓存中的时间) 是否大于 时间差 function IsCheckTimeOut() { var currentTime = Date.now() // 当前时间戳 var timeStamp = getTimeStamp() // 缓存时间戳 return (currentTime - timeStamp) / 1000 > TimeOut } export default service
本节注意
:我们在调用登录接口的时候 一定是没有token的,所以token检查不会影响登录接口的调用
同理,在登录的时候,如果登录成功,我们应该设置时间戳
// 定义login action 也需要参数 调用action时 传递过来的参数 // async 标记的函数其实就是一个异步函数 -> 本质是还是 一个promise async login(context, data) { // 经过响应拦截器的处理之后 这里的result实际上就是 token const result = await login(data) // 实际上就是一个promise result就是执行的结果 // axios默认给数据加了一层data // 表示登录接口调用成功 也就是意味着你的用户名和密码是正确的 // 现在有用户token // actions 修改state 必须通过mutations context.commit('setToken', result) // 写入时间戳 setTimeStamp() // 将当前的最新时间写入缓存 }
提交代码
有主动处理就有被动处理,也就是后端告诉我们超时了,我们被迫做出反应,如果后端接口没有做处理,主动介入就是一种简单的方式
本节任务
:完成token超时的主动介入
Token失效的被动处理
目标
: 实现token失效的被动处理
除了token的主动介入之外,我们还可以对token进行被动的处理,如图
token超时的错误码是10002
代码实现 src/utils/request.js
error => { // error 信息 里面 response的对象 if (error.response && error.response.data && error.response.data.code === 10002) { // 当等于10002的时候 表示 后端告诉我token超时了 store.dispatch('user/logout') // 登出action 删除token router.push('/login') } else { Message.error(error.message) // 提示错误信息 } return Promise.reject(error) }
无论是主动介入还是被动处理,这些操作都是为了更好地处理token,减少错误异常的可能性
本节任务
Token失效的被动处理