引言
vue-element-admin是vue生态圈中,最火的一个后台管理框架。基于vue和element-ui实现。
这篇文章主要会讲解登陆的流程以及我认为这个框架的厉害的东西:动态路由,之前看代码的时候,总想着一个登陆搞那么麻烦,后面仔细品味发现原来一个小小的登陆功能涉及到了这么多的东西。
准备工作
目录结构
了解一个框架之前,先要从目录结构入手(这里直接引用花裤衩大佬的目录结构)。目录结构的了解能够更加清楚模块功能的划分。
├── src # 源代码
│ ├── api # 所有的api请求
│ ├── assets # 主题 字体等静态资源
│ ├── components # 全局公用组件
│ ├── directive # 全局指令
│ ├── filters # 全局 filter过滤器
│ ├── icons # 项目所有 svg icons
│ ├── layout # 全局 layout
│ ├── router # 路由
│ ├── store # 全局 store管理
│ ├── styles # 全局样式
│ ├── utils # 全局公用方法
│ ├── vendor # 公用vendor
│ ├── views # views 所有页面
│ ├── App.vue # 入口页面
│ ├── main.js # 入口文件 加载组件 初始化等
│ └── permission.js # 权限管理
├── tests # 测试
├── .env.xxx # 环境变量配置
├── .eslintrc.js # eslint 配置项
├── .babelrc # babel-loader 配置
├── .travis.yml # 自动化CI配置
├── vue.config.js # vue-cli 配置
├── postcss.config.js # postcss 配置
└── package.json # package.json
复制代码
permission.js
在登陆这个流程中,permission.js这个是最重要的一环,其实这个文件就是路由的全局钩子(beforeEach和afterEach),全局钩子的意思就是每次跳转的时候可以根据情况进行拦截,不让它进行跳转。使用场景最常见的就是有些页面需要用户登陆之后才能访问,就可以在beforeEach中校验用户是否登陆来进行相对应的拦截处理。下面会详细的讲解permission.js的内容。
util / auth.js
这个文件主要就是设置token到cookie中的操作封装。
router
这个是路由中的一些设置,理解这个后面看组件Sidebar、TagViews将会事半功倍。
/**
hidden: true 是否隐藏于Sidebar侧边栏
alwaysShow: true 是否显示在根菜单
redirect: noRedirect Breadcrumb中重定向的path
name: 'router-name' 用于keep-alive的Name
meta: {
roles: ['admin', 'editor'], 当前路由的访问所需要权限
title: 'title', Sidebar和Breadcrumb的title
icon: 'svg-name', Sibebar的icon
noCache: true 是否设置不缓存
breadcrumb: true 是否显示在Breadcrumb上
activeMenu: '/example/list' Sidebar高亮时的显示path
}
**/
复制代码
其他的一些我就没有介绍了,比如说封装好axios的request.js,还有把请求封装成api,这些可以自行去了解。
view / login / index.vue
省略了一些的细枝末节,直接从点击登陆之后发生了一系列事情开始讲起,第一个就是handleLogin方法。
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true
this.$store.dispatch('user/login', this.loginForm)
.then(() => {
this.$router.push({
path: this.redirect || '/',
query: this.otherQuery
})
this.loading = false
})
.catch(() => {
this.loading = false
})
} else {
console.log('error submit!!')
return false
}
})
}
复制代码
可以看到这个方法很简单,就是利用validate方法进行表单验证,验证通过则使用this.$store.dispatch调用user/login方法并传递这个表单的数据,然后有一个.then()方法,方法里面有this.$router.push(...),可能有同学就会有疑惑了,this.redirect和this.otherQuery是啥,用一句话来概括就是:我从哪里跳到/login页面,登陆之后我就返回到哪里。
这个user/login是什么呢?一起来揭开它神秘的面纱。
user / login
先解析这个之前,先来补充一点vuex基础知识:
vuex中使用namespaced:true开启命名空间,调用mutations或者调用actions,则是模块名 相对应的方法名。
另外actions是异步的,action处理函数之后返回的Promise进行相对应的处理。
// user.js
// 不开启命名空间
const actions = {
login(){}
}
export default { actions };
this.$store.dispatch('login');
// user.js
// 开启命名空间
const actions = {
login(){}
}
export default { actions, namespaced: true }; // 注意!开启命名空间
this.$store.dispatch('user/login'); // 模块名user 方法名 login
复制代码
上面所说的user/login,则就是user模块中的login方法,核心代码就如下:
login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({ username: username.trim(), password: password }).then(response => {
const { data } = response // 解构出data
commit('SET_TOKEN', data.token) // 更新store里面的token
setToken(data.token) // token保存到cookie
resolve()
}).catch(error => {
reject(error)
})
})
}
复制代码
一句话来概括:登陆验证,登陆成功之后,分别保存token到vuex、cookie中。
这里完成之后就会回到之前的view/login/index.vue,当user/login调用完成之后,则会进行.then(...)方法,就是一个路由跳转的过程(this.$router.push(...))。
Permission.js
这个文件就如同前面介绍所说,路由的全局钩子,动态路由的实现这里相当于是一个入口。来看核心的实现代码。
router.beforeEach(async(to, from, next) => {
// 从cookie中取得token
const hasToken = getToken()
// 如果有token 也就是已经登陆的情况下
if (hasToken) {
// 并且要前往的路径是'/login' 则返回 '/'
if (to.path === '/login') {
next({ path: '/' })
} else {
// 从store中取得用户的 roles, 也就是用户的权限 并且用户的权限数组必须有一个以上
const hasRoles = store.getters.roles && store.getters.roles.length > 0
// 有权限则直接进入
if (hasRoles) {
next()
} else {
// 没有权限的话
try {
// 获取用户信息
const { roles } = await store.dispatch('user/getInfo')
// 生成可访问路由
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// 将可访问路由添加到路由上
router.addRoutes(accessRoutes)
// 进入路由
next({ ...to, replace: true })
} catch (error) {
// 如果出现异常 清空路由
await store.dispatch('user/resetToken')
// Message提示错误
Message.error(error || 'Has Error')
// 跳到login页面重新登陆
next(`/login?redirect=${to.path}`)
}
}
}
} else {
// 没有token 也就是没有登陆的情况下
// 判断是否是白名单(也就是说不需要登陆就可以访问的路由)
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
// 其他的一路给我跳到login页面 老老实实的进行登陆
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
复制代码
注释已经写的明明白白了,这个思路其实使用的特别多,就是利用全局钩子进行访问的拦截,如果没有登陆的话,跳转到登陆页面进行登陆。
但是花裤衩大佬的这个有一点点不同,可以看到他将登陆和获取用户信息分成了两步,原因就是保证用户信息是最新的。第二个是在获取用户信息之后返回的roles生成可访问的路由,也就是这两步实现了动态路由。
用一句话来概括:是否登陆?没有就给我老老实实登陆。是否有用户信息?没有就给我获取用户信息,并且生成可访问路由然后利用addRoutes进行添加。
这两步都是actions:user/getInfo,permission/generateRoutes。
可以看到这是vuex中的actions提交,可能有些小伙伴会有点困惑,为什么有的请求直接写在api文件夹,有些就放在actions里面?这个问题:我的理解就是,如果返回的数据要保存在vuex中的话,可以直接使用actions,原因是action里面可以提交mutation改变store的状态,可以少写一些代码,同时思路更加清晰,如果返回的数据只需要在当前页面使用的话,并不需要保存到vuex中,那就直接用写在api文件夹中引用即可。当然这个是我的个人理解,如有错误请望指出。
user / getInfo
首先我们看第一个user/getInfo。
如上面所说,这是user模块中的getInfo方法,来看核心代码。
getInfo({ commit , state }) {
return new Promise((resolve, reject) => {
// 调用getInfo接口
getInfo(state.token).then(response => {
const { data } = response // 解构出data
if (!data) { // 进行数据校验
reject('Verification failed, please Login again.')
}
// 解构出需要保存的值
const { roles, name, avatar, introduction } = data
// roles权限数组至少有一个权限
if (!roles || roles.length <= 0) {
reject('getInfo: roles must be a non-null array!')
}
// 保存到vuex
commit('SET_ROLES', roles)
commit('SET_NAME', name)
commit('SET_AVATAR', avatar)
commit('SET_INTRODUCTION', introduction)
// 将 data 返回
resolve(data)
}).catch(error => {
reject(error)
})
})
}
复制代码
这个就是调用getInfo接口获取用户信息并且保存到vuex中,为了严谨性,进行相对应的数据校验,然后把data返回。
用一句话来概括:获取用户信息,并保存用户信息。
permission/generateRoutes
我们来看第二个actions,这个是动态路由中的重要一步,生成可访问路由,根据当前用户的权限数组,和路由中可访问的权限数组,进行匹配生成。
// 判断是否有权限
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
// roles.some => Array.some 相当于是只要有一个满足就为true
// 判断用户的权限于当前路由访问所需要的权限是否有一个满足
// 比如说用户权限为 ['one','two'] 当前路由访问所需要权限为 ['two','three'] 那么就说明当前用户可以访问这个路由
return roles.some(role => route.meta.roles.includes(role))
} else {
// 默认是可访问的
return true
}
}
// 生成可访问路由
export 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
}
// 为什么要写这里呢,因为后面的Sidebar组件与这个环环相扣
const mutations = {
SET_ROUTES: (state, routes) => {
// 添加的路由
state.addRoutes = routes
// 将vuex中的路由进行更新
state.routes = constantRoutes.concat(routes)
}
}
const actions = {
generateRoutes({ commit }, roles) {
return new Promise(resolve => {
let accessedRoutes
// 如果roles包含 'admin' 直接可以全部访问
if (roles.includes('admin')) {
accessedRoutes = asyncRoutes || []
} else {
// 利用 filterAsyncRoutes 过滤出可访问的路由
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
}
// 保存可访问的路由到store中
commit('SET_ROUTES', accessedRoutes)
// 将可访问路由返回
resolve(accessedRoutes)
})
}
}
复制代码
欧克,注释已经全部写好了,这个就是动态路由中重要一步,慢慢品味发现也不难,越看越觉得666。
用一句话来概括:根据得到的用户权限生成可以访问的路由。
动态路由的实现
router.addRoutes(accessRoutes)
复制代码
???,没呢?
对的!动态路由的核心代码就这一句话,短小精悍,其他的都是为了完成动态路由做的一些 "准备工作" ,user/getInfo获取用户信息得到用户的roles权限数组,返回user/generateRoutes生成可访问路由,就是这么神奇的一步,实现了动态路由。
写到这里基本流程就走完了,但是还有两个注意点。
注意
为什么项目开启默认就是登陆页面
想必通过之前的讲解也应该知道了permission.js的作用了,全局路由钩子,每次路由跳转都要调用全局路由钩子(谁让它是全局的呢),原因就是当页面加载首页时也会经过全局路由钩子,而permission.js判断当前用户有没有登陆,没有登陆就直接跳转到/login页面进行登陆把,就是这么任性,啦啦啦。
动态路由显示不出来
有些小伙伴可能会改这个框架的代码,但是发现显示不出来?这也是我遇到过的一个坑,需要注意的是permission.js里面有两个条件:第一个是否登陆?第二个是否获取用户信息(判断是否获取用户信息是根据roles)?有些小伙伴可能是登陆的时候就把用户信息获取了,但是!!没有更新用户roles权限数组,所以就一直会获取用户信息,从而导致显示不出来。
完结,撒花
其实到这里整个登陆流程就已经结束了,可以看到花裤衩大佬想的很多,用户信息的获取和用户登陆的分离,根据用户权限生成可以访问的路由并添加,这些思路我觉得很厉害,还有这么多组件可以用,哈哈哈哈。
好的,我们来总结一下:
- Login/index.vue点击登陆 提交user/login的actions。
- user/login进行登陆验证,登陆成功之后保存token到vuex和cookie中。
- 回到Login/index.vue跳转路由,这时就到了permission.js
- permission.js进行判断是否登陆,是否有用户信息?没有用户信息就获取用户信息,并且保存到vuex,然后根据用户信息中的roles生成可访问路由,并通过addRoutes进行添加。
看到这里,相信你对这个框架的登陆流程已经有一定的了解了,自己再去理一遍把,啦啦啦啦,那下面这一张流程图相信你也可以看懂。