【vue-element-admin】 登录流程代码解析备忘

-1 背景

新工作内容是运维与开发公司自研的大数据平台,自研的平台是基于datax-web改造而来。去GitHub查询repo得知datax-web的前端datax-web-ui项目是由vue-element-admin 改造的。这里记录下代码方便自己学习。

0 前言

这一篇先写vue-element-admin 登录流程,vue-element-admin 使用的是UI toolkit是element-ui,官方文档点击此处。

下面是原作者的编写思路,掘金原帖

进入正题,做后台项目区别于做其它的项目,权限验证与安全性是非常重要的,可以说是一个后台项目一开始就必须考虑和搭建的基础核心功能。我们所要做到的是:不同的权限对应着不同的路由,同时侧边栏也需根据不同的权限,异步生成。这里先简单说一下,我实现登录和权限验证的思路。

登录:当用户填写完账号和密码后向服务端验证是否正确,验证通过之后,服务端会返回一个token,拿到token之后(我会将这个token存贮到cookie中,保证刷新页面后能记住用户登录状态),前端会根据token再去拉取一个 user_info 的接口来获取用户的详细信息(如用户权限,用户名等等信息)。
权限验证:通过token获取用户对应的 role,动态根据用户的 role 算出其对应有权限的路由,通过 router.addRoutes 动态挂载这些路由。

上述所有的数据和操作都是通过vuex全局管理控制的。(补充说明:刷新页面后 vuex的内容也会丢失,所以需要重复上述的那些操作)接下来,我们一起手摸手一步一步实现这个系统。

1 登录流程

1.1 src/views/login/index.vue

前端登录界面view是src/views/login/index.vue,handleLogin是核心方法,该方法就做了两件事情:发送登录请求进行路由跳转

watch: {
  $route: {
    handler: function(route) {
      const query = route.query
      if (query) {
        this.redirect = query.redirect
        this.otherQuery = this.getOtherQuery(query)
      }
    },
    immediate: true
  }
},
methods: {
	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
	    }
	  })
	},
	 getOtherQuery(query) {
	 	//	枚举所有键名,reduce将所有非redirect的内容存到累加器acc,最后返回累加器
      return Object.keys(query).reduce((acc, cur) => {
        if (cur !== 'redirect') {
          acc[cur] = query[cur]
        }
        return acc
      }, {})
    }
}
  • validate 是 element form的 method
  • this.$store.dispatch('user/login', this.loginForm) 调用的是 src/store/modules/user.js 里的login action,this.loginForm是form参数
  • this.$router.push({ path: this.redirect || '/', query: this.otherQuery }) this.redirect 是重定向的路径,如果没有就跳转到/路径,登录的参数是this.otherQuery。this.redirect 和 this.otherQuery 是由watch监听的,
    • watch进行监听,$route是当前路由对象,handler是回调函数,该函数描述了监听到路由变化后要做的事。
    • query对象中redirect字段存着重定向的路径(http://ip:port/#/login?redirect=%2Fdatax%2Fdependency%2FdependencyInfo redirect=后面就是this.redirect的值
    • this.getOtherQuery(query)方法会获取route.query中非redirect的其他查询条件
1.2.1 src/store/modules/user.js

这个方法的目的是发送promise请求给后端,根据username和password换token,将token存在vuex和cookie中

import { login, logout, getInfo } from '@/api/user'
const actions = {
  // user login
  login({ commit }, userInfo) {
    const { username, password } = userInfo
    return new Promise((resolve, reject) => {
      login({ username: username.trim(), password: password }).then(response => {
      // vuex存储返回的data(token)
        const { data } = response
        commit('SET_TOKEN', data.token)
        //token还会存到Cookie中
        setToken(data.token)
        //promise 将pending态改为resolve态
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },
}
  • promise 发送的login方法的地址在@/api/user,打开该文件可以发现url: '/vue-element-admin/user/login'
1.2.2 src/api/user.js

这个文件就是1.2.1 promise发送的真正后端api地址,这边导入的request实际上就是自行封装的axios模块,里面配置了request前置拦截器和响应拦截器

import request from '@/utils/request'

export function login(data) {
  return request({
    url: '/vue-element-admin/user/login',
    method: 'post',
    data
  })
}
1.2.3 src/utils/request.js

请求前置拦截器 作用是,每次发请求前检查vuex里是否有token,有的话把token配置进请求头中,统一配置方便管理。带上token是后续所有请求后端都需要token验证身份。

// request interceptor
service.interceptors.request.use(
  config => {
    // do something before request is sent

    if (store.getters.token) {
      // let each request carry token
      // ['X-Token'] is a custom headers key
      // please modify it according to the actual situation
      config.headers['X-Token'] = getToken()
    }
    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

response interceptor,主要是对自定义响应码的处理

 response => {
    const res = response.data

    // if the custom code is not 20000, it is judged as an error.
    if (res.code !== 20000) {
      Message({
        message: res.message || 'Error',
        type: 'error',
        duration: 5 * 1000
      })

      // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
        // to re-login
        MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
          confirmButtonText: 'Re-Login',
          cancelButtonText: 'Cancel',
          type: 'warning'
        }).then(() => {
          store.dispatch('user/resetToken').then(() => {
            location.reload()
          })
        })
      }
      return Promise.reject(new Error(res.message || 'Error'))
    } else {
      return res
    }
  },
1.3.1 src/permission.js

下面开始介绍路由部分,路由部分和请求一样,也有前后拦截器,这里是router.beforeEachrouter.afterEach。当然配置了路由白名单

beforeEach:根据是否存在token判断,没token就重定向到login页面;有token的情况下,主要工作是根据用户的角色权限进行动态路由挂载。动态路由挂载会导致不同角色的用户进入系统的侧边栏内容不一样。

和登录一样,发送的请求先找vuex的模块下对应文件,例如获取对应角色的路由就是去src/store/modules/permission.js
user/getInfo 这个请求比较简单,就是抓下vuex里有没有token,根据token抓当前用户的信息。

afterEach:beforeeach每次会启动一个虚假的进度条组件nprogress,aftereach里关闭掉

const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist

router.beforeEach(async(to, from, next) => {
  // start progress bar
  NProgress.start()

  // set page title
  document.title = getPageTitle(to.meta.title)

  // determine whether the user has logged in
  const hasToken = getToken()

  if (hasToken) {
    if (to.path === '/login') {
      // if is logged in, redirect to the home page
      next({ path: '/' })
      NProgress.done() // hack: https://github.com/PanJiaChen/vue-element-admin/pull/2939
    } else {
      // determine whether the user has obtained his permission roles through getInfo
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      if (hasRoles) {
        next()
      } else {
        try {
          // get user info
          // note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
          const { roles } = await store.dispatch('user/getInfo')

          // generate accessible routes map based on roles
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)

          // dynamically add accessible routes
          router.addRoutes(accessRoutes)

          // hack method to ensure that addRoutes is complete
          // set the replace: true, so the navigation will not leave a history record
          next({ ...to, replace: true })
        } catch (error) {
          // remove token and go to login page to re-login
          await store.dispatch('user/resetToken')
          Message.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  } else {
    /* has no token*/

    if (whiteList.indexOf(to.path) !== -1) {
      // in the free login whitelist, go directly
      next()
    } else {
      // other pages that do not have permission to access are redirected to the login page.
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})

router.afterEach(() => {
  // finish progress bar
  NProgress.done()
})
  • beforeeach 整个是个async/await异步方法,await右边的方法一般都是promise对象
  • next({ …to, replace: true }):next()是放行,next(参数)中断当前导航,执行新的导航,next({ …to, replace: true })解决死循环白屏问题
1.3.2 src/store/modules/permission.js

这个js文件主要工作·就是根据用户的角色获得对应的路由菜单。
vue-element-admin的侧边栏菜单和路由是映射在一起的,再动态挂载路由。你只需要修改路由文件src/router/index.js 和用户角色 和本文件的挂载逻辑就可以根据用户角色动态挂载~

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
}

const state = {
  routes: [],
  addRoutes: []
}

const mutations = {
  SET_ROUTES: (state, routes) => {
    state.addRoutes = routes
    state.routes = constantRoutes.concat(routes)
  }
}

const actions = {
  generateRoutes({ commit }, roles) {
    return new Promise(resolve => {
      let accessedRoutes
      if (roles.includes('admin')) {
        accessedRoutes = asyncRoutes || []
      } else {
        accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
      }
      commit('SET_ROUTES', accessedRoutes)
      resolve(accessedRoutes)
    })
  }
}
  • generateRoutes 是核心方法,根据角色是否是admin进行asyncRoutes的过滤,asyncRoutes是动态路由。动态路由和静态路由配置文件src/router/index.js
  • filterAsyncRoutes 根据角色过滤路由的递归方法,因为菜单下面有些存在children也需要判断
2 回顾

路由挂载后,就会真正进行路由的跳转。根据重定向的结果跳转重定向页面or首页,流程结束。
核心文件其实就是

  • 登录就(黄底色是自定义逻辑修改)
    • index.vue(前端发送表单参数)
    • @/store/modules/user.js(具体方法逻辑)
    • @/utils/request 全局请求拦截,主要是response自定义响应码操作
    • @/api/user url请求地址
  • 路由就修改
    • @/permission.js 全局路由拦截
    • @/store/modules/permission.js 具体动态路由挂载策略
    • @/router/index.js 配置静态和动态路由表
动态路由挂载
前端登录
login.vue
发送请求
路由跳转
发送请求过程
全局请求拦截
vuex的login方法
路由跳转过程
全局路由拦截
进行重新定向或跳转

2 知识点

  • axios、ajax、promise和async/await
    • XMLHttpRequest(xhr):浏览器提供的js对象,用于请求服务器资源
    • jquery的ajax:基于xhr对象进行了封装的库
    • promise:es6提出的解决异步编程的一种方案,通过链式调用来解决ajax的回调地狱
    • axios:基于promise的http 库(Axios is a popular promise-based HTTP client )
    • async/await:es8提出的解决异步操作的方案。是promise的语法糖。
    • 需要强调:promise只是异步操作的接口规范,ajax则是一种具体异步操作方法

你可能感兴趣的:(总结,vue.js,前端,javascript)