用全家桶认证用户
对 Vue 来说,全家桶就像天赋系统——刚开始的时候,你注意不到它的存在;但是当项目规模大到一定程度,合理使用全家桶会让你事半功倍。
平时浏览开发论坛的时候,经常看到有同学提问:如何做用户身份校验?其实这个时候,最合适的方案就是组合使用 Vuex + Vue-router。
Vuex
首先,推荐大家读一下官方文档:状态管理。
好,现在假设你已经读完了。简单来说,Vuex 负责保存数据,并且把数据输出到其它 Vue 组件中,供它们使用。使用 Vuex 有以下优势:
- 数据统一,可以避免在每个组件里都维护一份数据
- Vuex 支持响应式
- 组件直接从 Vuex 获取数据,而不是从父组件接收,组件之间解耦,也方便我们对组件进行独立测试
使用 Vuex 的方式很简单,大家直接看 Vuex 官方文档 即可。
Vue-Router
还请先读官方文档:路由。
Vue-Router 的作用在于帮我们组织单页应用。这个应该不需要太多解释。
用户认证系统的设计
这里我们拿后台产品做例子。一般来说,后台产品对用户的要求有:
- 用户必须登录
- 用户操作时先鉴权
- 鉴权失败跳转到登录页
首先,用户登录保存 session 的动作,直接用 HTTP header 的 set-cookie
就行。很多同学这个地方还费劲吧啦的用 localStorage,然后用 axios 拦截器给每个请求加 token,其实完全没有必要,安全性也没有任何优势。
我们可以把登录过程直接放到 Vuex.Store 里,接着定义路由:
// store.js
export default new Vuex.Store({
state: {
user: null,
},
mutations: {
setUser(state, user) {
state.user = user;
},
},
actions: {
login({commit}, data) {
service.login(data)
.then(user => {
commit('setUser', user);
return user;
});
},
},
});
// router.js
export default new VueRouter({
routes: [
{
path: '/',
name: 'home',
},
{
path: '/login',
name: 'login',
},
],
});
应用初始化的时候,先检查当前用户的登录状态——其实这个检查最好做成心跳脚本,不断检查,方便在用户 session 失效的时候跳出,避免错误。
// app.vue
export default {
beforeMount() {
checkUser()
.then(() => {
// 已登录
})
.catch(() => {
// 未登录,跳到登录页
this.$router.replace({
name: 'login',
});
});
},
}
然后在登录页面,登录成功之后跳转到默认页面:
// login.vue
export default {
methods: {
doLogin() {
this.$store.dispatch('login', this.formData)
.then(() => {
// 登录成功,跳转到首页。其实这里可以记录下跳转前的 pathname,登录成功跳过去,比始终跳首页要好
this.$router.replace({
name: 'home',
});
})
.catch(error => {
// 登录失败,处理一下
});
},
},
};
其它页面,需要用到用户信息,直接用 Vuex.mapState
就好:
<template lang="pug">
Welcome, {{user.name}}
</template>
<script>
import {mapState} from 'vuex';
export default {
computed: {
...mapState(['user']),
},
}
</script>
至于鉴权,因为 sessionId 写在 cookie 里,本身就是 HTTP 请求的一部分,不需要特别处理。不过我们可以拦截请求,如果触发 401,就跳到登录页。这里以 axios 为例:
const http = axios.create();
http.interceptors.response.use(response => {
return response;
}, error => {
const {status} = error;
if (statue === 401) {
// 因为 router 路由绑在 vue 根组件上,axios 不好触及,这里就可以用到后面的“全局事件总线”
bus.emit('401', error);
}
return Promise.reject(error);
});