系列文章:
Vue源码系列(硬核)
本项目中登录和权限控制功能的实现,主要参考了vue-elemen-admin,这部分是比较核心的功能,涉及到的模块比较多,笔者在研究vue-elemen-admin的源码时花费了很多时间,也踩了很多坑,所以这篇文章也可以作为新手入坑vue-elemen-admin源码的一个上手教程,相比较于花裤衩大佬的原版教学会更新手向,更细致一些,希望能帮到大家。
登录
获取token
项目中使用token进行用户验证,并将token保存在sessionStorage中(不能只保存在vuex中,因为页面刷新后vuex中的信息会丢失)。
当用户点击登录时触发以下操作,从而获取token
methods: {
handleLogin() {
// ...
// 自定义表单验证部分
// ...
this.$store
.dispatch('user/login', this.userData)
.then(() => {
// 用于用户登出后重新登录时,将页面重新定向到退出时的页面
// 在src/views/layout/components/navbar/ToolBox.vue的handleLogout中会记录用户退出时所在的路径
// this.$router.push(`/login?redirect=${this.$route.fullPath}`)
this.$router.push(this.$route.query.redirect || '/')
})
.catch(() => {
this.$message({
type: 'error',
message: '登录失败,请重新尝试'
})
})
}
}
action
// src/store/modules/user.js
login({ commit }, userData) {
const { username, password } = userData
return new Promise((resolve, reject) => {
// userLogin接口,该接口只返回一个token
userLogin({
username: username.trim(),
password: password,
})
.then(({ data: { token } }) => {
// 设置state中的token
commit('SET_TOKEN', token)
// 设置sessionStorage中的token
setToken(token)
resolve()
})
.catch((error) => {
reject(error)
})
})
}
在每次请求时均需要携带token,因此可以使用axios请求拦截器为每次请求添加token。
// src/utils/request.js
service.interceptors.request.use(
(config) => {
// 从sessionStorage中获取token
const token = getToken()
if (token) {
config.headers.Authoration = token
}
return config
},
(err) => {
console.log(err)
return Promise.reject(err)
}
)
获取用户信息
用户登录成功后会进入首页,此时会触发beforeEach钩子,这部分代码在src/permission.js中,总体逻辑如下
router.beforeEach(async (to, from, next) => {
const token = getToken()
if (token) {
// 如果有token,说明已经登录了,不能再进入登录页
if (to.path === 'login') {
next({ path: '/ '})
} else {
// ...
// 判断有无用户信息
// ...
}
} else {
// WHITE_LIST无需登录就能访问的页面白名单,比如'/login'
if (WHITE_LIST.includes(to.path)) {
next()
} else {
// 如果不在白名单内且没有登录,进入login页面,并且携带重定向的路径
next(`/login?redirect=${to.path}`)
}
}
})
在进入其他页面前需要获取用户的权限,从而确定用户可以访问的页面
在beforeEach中会请求getUserInfo来获取用户信息,在本项目中会获取用户对应权限roles(数组)和用户的name,并根据用户权限来生成路由,因此可以根据vuex中是否有roles来判断是否已经请求过getUserInfo
判断有无用户信息的代码如下
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
// 如果已经获取过用户信息,直接放行
next()
} else {
const { roles } = await store.dispatch('user/getUserInfo')
// ...
// 动态生成路由
// ...
}
action
// src/store/modules/user.js
getUserInfo({ commit, state }) {
return new Promise((resolve, reject) => {
// 请求getUserInfo接口
getUserInfo(state.token)
.then(({ data }) => {
if (!data) {
reject(new Error('请重新登录'))
}
const { roles, name } = data
if (!roles || roles.length < 1) {
reject(new Error('权限错误'))
}
// 设置roles和name
commit('SET_ROLES', roles)
commit('SET_NAME', name)
resolve(data)
})
.catch((error) => {
reject(error)
})
})
}
权限
前面已经获取了用户的roles字段,并且我们可以在路由表中定义每个路由的访问权限,通过遍历该路由表来确定用户有权限的路由,并利用router.addRoute来动态添加路由(vue-element-admin中采用router.addRoutes,而新版本vue-router推荐使用router.addRoute)。
路由设置
router
├── modules
├── asyncRoutes.js // 异步加载的路由模块,需要根据权限动态添加
├── constantRoutes.js // 所有权限均能访问的通用路由
└── index.js
asyncRoutes中的404路由要放在最后,否则会出现问题
在每个路由对象中,使用meta来定义该路由的访问权限
{
path: 'page',
component: () => import('@/views/permission/PagePermission'),
name: 'PagePermission',
meta: {
title: '页面权限',
roles: ['admin'],
}
}
而src/router/index.js的代码如下所示
import Vue from 'vue'
import VueRouter from 'vue-router'
import constantRoutes from './constantRoutes'
Vue.use(VueRouter)
// 写成一个函数原因在用户退出部分会提到
const createRouter = () =>
new VueRouter({
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes,
})
const router = createRouter()
export default router
src/permission.js
前面已经讲解了beforeEach中的部分代码,接下来将解释路由的动态添加部分
try {
// 获取用户权限
const { roles } = await store.dispatch('user/getUserInfo')
// 生成可访问的异步路由
const accessedRoutes = await store.dispatch(
'permission/generateRoutes',
roles
)
// 挂载异步路由,没有使用废弃的addRoutes
accessedRoutes.forEach((r) => router.addRoute(r))
// 使用next()将无法成功跳转
next({ ...to, replace: true })
} catch (err) {
// 发生错误则返回登录页
await store.dispatch('user/resetToken')
Message.error(err.message || 'Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
next('/')或者next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。
因此,在登录后会触发两次beforeEach钩子,如果你在这个钩子中有一些log的话,可能会发现这个有些诡异的现象.
此外,这里还有一个小问题,在较高版本的vue-router中,以上代码会有一个报错
Uncaught (in promise) Error: Redirected when going from "/login" to "/index" via a navigation guard
当使用
router-link组件时,这些失败都不会打印出错误。然而,如果你使用router.push或者router.replace的话,可能会在控制台看到一条 "Uncaught (in promise) Error" 这样的错误,后面跟着一条更具体的消息。让我们来了解一下如何区分导航故障。
这是由于vue-router3.1.0版本后,push和replace方法会返回一个promise,导航中断导致产生一个未捕获的错误,可用如下方法解决
// src/router/index.js
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location, onResolve, onReject) {
if (onResolve || onReject)
return originalPush.call(this, location, onResolve, onReject)
return originalPush.call(this, location).catch((err) => err)
}
src/store/modules/permission.js
接下来讲解如何获取可访问的异步路由,也就是permission/generateRoutes这个action的实现原理
// src/store/modules/permission.js
const actions = {
generateRoutes({ commit }, roles) {
return new Promise((resolve) => {
let accessedRoutes
// 如果用户是admin,则可以访问所有异步路由
if (roles.includes('admin')) {
accessedRoutes = asyncRoutes || []
} else {
// 否则过滤出用户可访问的异步路由
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
}
// 更新state
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
},
}
filterAsyncRoutes递归获取了有权限访问的异步路由,类似于多叉树的dfs
function filterAsyncRoutes(routes, roles) {
const res = []
routes.forEach((route) => {
const tmp = { ...route }
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
return roles.some((r) => route.meta.roles.includes(r))
}
// 没有meta字段或者meta内没有roles字段,说明所有权限均可访问
return true
}
用户退出
在用户退出时需要重置路由,在src/router/index.js中导出一个方法,并在用户退出时调用
export function resetRouter() {
// 创建一个新的router对象,该对象的routes只有constantRoutes
const newRouter = createRouter()
// 将当前的router重置为初始状态
router.matcher = newRouter.matcher
}
用户退出后将router重置,用户重新登录时会触发beforeEach钩子,从而重新添加异步路由,如果不重置路由,则用户重新登录时会有一个警告
Duplicate named routes definition,这是由于router中已经存在了这个路由,登录时重新添加了一遍所导致的。但是当根据路由生成菜单栏时并不会产生错误,因为本项目中菜单栏时根据用户能够访问的路由表生成的,与router对象无关。