接着上一篇看下VUE动态权限控制的前端实现。
总述:App.vue与main.js为入口,api为接口,router为路由,store为全局常(变)量,views为页面。
一、环境配置config:
1、index.js:
'use strict'
// Template version: 1.2.6
// see http://vuejs-templates.github.io/webpack for documentation.
const path = require('path')
module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
// Various Dev Server settings
host: 'localhost', // can be overwritten by process.env.HOST
port: 9528, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
autoOpenBrowser: true,
errorOverlay: true,
notifyOnErrors: false,
poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
// Use Eslint Loader?
// If true, your code will be linted during bundling and
// linting errors and warnings will be shown in the console.
useEslint: true,
// If true, eslint errors and warnings will also be shown in the error overlay
// in the browser.
showEslintErrorsInOverlay: false,
/**
* Source Maps
*/
// https://webpack.js.org/configuration/devtool/#development
devtool: 'cheap-source-map',
// If you have problems debugging vue-files in devtools,
// set this to false - it *may* help
// https://vue-loader.vuejs.org/en/options.html#cachebusting
cacheBusting: true,
// CSS Sourcemaps off by default because relative paths are "buggy"
// with this option, according to the CSS-Loader README
// (https://github.com/webpack/css-loader#sourcemaps)
// In our experience, they generally work as expected,
// just be aware of this issue when enabling this option.
cssSourceMap: false,
},
build: {
// Template for index.html
index: path.resolve(__dirname, '../dist/index.html'),
// Paths
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
/**
* You can set by youself according to actual condition
* You will need to set this if you plan to deploy your site under a sub path,
* for example GitHub pages. If you plan to deploy your site to https://foo.github.io/bar/,
* then assetsPublicPath should be set to "/bar/".
* In most cases please use '/' !!!
*/
assetsPublicPath: '/vueAdmin-template/', // If you are deployed on the root path, please use '/'
/**
* Source Maps
*/
productionSourceMap: false,
// https://webpack.js.org/configuration/devtool/#production
devtool: '#source-map',
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report
}
}
2、开发环境dev.env.js:
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
BASE_API: '"http://localhost:8080/usermanager/"',
})
3、生产环境prod.env.js:
'use strict'
module.exports = {
NODE_ENV: '"production"',
BASE_API: '"http://localhost:8080/usermanager/"',
}
配置后,执行npm run dev命令,则是启动测试环境;执行npm run build则是启动正式环境。
二、准备工作:
1、封装utils:
(1)auth.js:
import Cookies from 'js-cookie'
const TokenKey = 'Admin-Token'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}
(2)request.js ajax请求util:
import axios from 'axios'
// 配置session跨域
axios.defaults.withCredentials = true
import { Message, MessageBox } from 'element-ui'
import store from '../store'
import { getToken } from '@/utils/auth'
// 创建axios实例
const service = axios.create({
baseURL: process.env.BASE_API, // api的base_url
timeout: 5000 // 请求超时时间
})
// request拦截器
service.interceptors.request.use(config => {
if (store.getters.token) {
config.headers['X-Token'] = getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
return config
}, error => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
})
// respone拦截器
service.interceptors.response.use(
response => {
/**
* code为非20000是抛错 可结合自己业务进行修改
*/
const res = response.data
if (res.code !== 200) {
Message({
message: res.message,
type: 'error',
duration: 5 * 1000
})
// 50008:非法的token; 50012:其他客户端登录了; 50014:Token 过期了;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
store.dispatch('FedLogOut').then(() => {
location.reload()// 为了重新实例化vue-router对象 避免bug
})
})
}
return Promise.reject('error')
} else {
return response.data
}
},
error => {
console.log('err' + error)// for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service
2、接口api:
login.js:
import request from '@/utils/request'
export function login(userName, pwd) {
return request({
url: '/home/login',
method: 'post',
params: {
userName,
pwd
}
})
}
export function getInfo(token) {
return request({
url: '/user/getUserRoles',
method: 'get'
})
}
export function logout() {
return request({
url: '/user/logout',
method: 'post'
})
}
// 获取用户姓名
export function getUserName(token) {
return request({
url: '/user/getUserName',
method: 'get'
})
}
3、路由:
import Vue from 'vue'
import Router from 'vue-router'
// in development-env not use lazy-loading, because lazy-loading too many pages will cause webpack hot update too slow. so only in production use lazy-loading;
// detail: https://panjiachen.github.io/vue-element-admin-site/#/lazy-loading
Vue.use(Router)
/* Layout */
import Layout from '../views/layout/Layout'
export const constantRouterMap = [
{ path: '/404', component: () => import('@/views/404'), hidden: true },
{ path: '/login', component: () => import('@/views/login/index'), hidden: true },
{
path: '/',
component: Layout,
redirect: '/dashboard',
name: '首页',
icon: '首页',
hidden: true,
children: [{
path: '/dashboard',
component: () => import('@/views/dashboard/index')
}]
}
]
/**
* hidden: true if `hidden:true` will not show in the sidebar(default is false)
* alwaysShow: true if set true, will always show the root menu, whatever its child routes length
* if not set alwaysShow, only more than one route under the children
* it will becomes nested mode, otherwise not show the root menu
* redirect: noredirect if `redirect:noredirect` will no redirct in the breadcrumb
* name:'router-name' the name is used by (must set!!!)
* meta : {
title: 'title' the name show in submenu and breadcrumb (recommend set)
icon: 'svg-name' the icon show in the sidebar,
}
**/
export const asyncRouterMap = [
// { path: '/login', component: () => import('@/views/login/index'), hidden: true },
{
path: '/module',
redirct: '/module/index',
name: 'module',
component: Layout,
meta: { title: '测试', authority: ['wtyy-cs'] },
noDropdown: true,
children: [
{
path: 'index',
name: 'index',
component: () => import('@/views/module/index'),
meta: { title: '测试首页', authority: ['wtyy-cs'], keepAlive: false }
},
{
path: 'detail',
name: 'detail',
// hidden: true,
component: () => import('@/views/module/detail'),
meta: { title: '测试详情', authority: ['wtyy-cs'], keepAlive: false }
}
]
},
{
path: '/table',
redirct: '/table/index',
name: 'table',
component: Layout,
meta: { title: 'table页', authority: ['wtyy-table'] },
noDropdown: true,
children: [
{
path: 'index',
name: 'index',
component: () => import('@/views/table/index'),
meta: { title: 'table首页', authority: ['wtyy-table'], keepAlive: false }
}
]
}
]
export default new Router({
// mode: 'history', // 后端支持可开
scrollBehavior: () => ({ y: 0 }),
routes: constantRouterMap
})
4、store:
(1)app.js:
import Cookies from 'js-cookie'
const app = {
state: {
sidebar: {
opened: !+Cookies.get('sidebarStatus'),
withoutAnimation: false
},
device: 'desktop'
},
mutations: {
TOGGLE_SIDEBAR: state => {
if (state.sidebar.opened) {
Cookies.set('sidebarStatus', 1)
} else {
Cookies.set('sidebarStatus', 0)
}
state.sidebar.opened = !state.sidebar.opened
state.sidebar.withoutAnimation = false
},
CLOSE_SIDEBAR: (state, withoutAnimation) => {
Cookies.set('sidebarStatus', 1)
state.sidebar.opened = false
state.sidebar.withoutAnimation = withoutAnimation
},
TOGGLE_DEVICE: (state, device) => {
state.device = device
}
},
actions: {
ToggleSideBar: ({ commit }) => {
commit('TOGGLE_SIDEBAR')
},
CloseSideBar({ commit }, { withoutAnimation }) {
commit('CLOSE_SIDEBAR', withoutAnimation)
},
ToggleDevice({ commit }, device) {
commit('TOGGLE_DEVICE', device)
}
}
}
export default app
(2)permission.js:
import { asyncRouterMap, constantRouterMap } from '@/router/index'
/**
* 通过meta.authority判断是否与当前用户权限匹配
* @param authorities
* @param route
*/
function hasPermission(authorities, route) {
if (route.meta && route.meta.authority) {
return authorities.some(authority => route.meta.authority.indexOf(authority) >= 0)
} else {
return true
}
}
/**
* 递归过滤异步路由表,返回符合用户角色权限的路由表
* @param asyncRouterMap
* @param authorities
*/
function filterAsyncRouter(asyncRouterMap, authorities) {
const accessedRouters = asyncRouterMap.filter(route => {
if (hasPermission(authorities, route)) {
if (route.children && route.children.length) {
route.children = filterAsyncRouter(route.children, authorities)
}
return true
}
return false
})
return accessedRouters
}
const permission = {
state: {
routers: constantRouterMap,
addRouters: []
},
mutations: {
SET_ROUTERS: (state, routers) => {
state.addRouters = routers
state.routers = constantRouterMap.concat(routers)
}
},
actions: {
GenerateRoutes({ commit }, data) {
return new Promise(resolve => {
const { authorities } = data
let accessedRouters
if (authorities.indexOf('admin') >= 0) {
accessedRouters = asyncRouterMap
} else {
accessedRouters = filterAsyncRouter(asyncRouterMap, authorities)
}
commit('SET_ROUTERS', accessedRouters)
resolve()
})
}
}
}
export default permission
constantRouterMap为固定的路由,登录进来都可以看得到的,asyncRouterMap为动态路由,在后端可配置。
(3)用户相关user.js:
import { login, logout, getInfo, getUserName } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
const user = {
state: {
token: getToken(),
name: '',
avatar: '',
roles: []
},
mutations: {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
SET_ROLES: (state, roles) => {
state.roles = roles
}
},
actions: {
// 登录
Login({ commit }, user) {
const userName = user.userName
const pwd = user.password
return new Promise((resolve, reject) => {
login(userName, pwd).then(response => {
const data = response.data
setToken(data.token)
commit('SET_TOKEN', data.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// 获取用户权限信息
GetInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
console.info('res' + response)
const data = response.data
if (data && data.length > 0) { // 验证返回的roles是否是一个非空数组
commit('SET_ROLES', data)
} else {
reject('getInfo: roles must be a non-null array !')
}
resolve(response)
}).catch(error => {
reject(error)
})
})
},
// 拉取用户姓名GetUserName
GetUserName({ commit, state }) {
return new Promise((resolve, reject) => {
getUserName(state.token).then(response => {
console.info('姓名res' + response.data)
const data = response.data
commit('SET_NAME', data)
resolve(response)
}).catch(error => {
reject(error)
})
})
},
// 登出
LogOut({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
removeToken()
resolve()
}).catch(error => {
reject(error)
})
})
},
// 前端 登出
FedLogOut({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
removeToken()
resolve()
})
}
}
}
export default user
用户具有的全局属性:token登录标志(值为后端的sessionId)、name用户名、roles权限(数组)。
三、入口分析:
1、入口main.js文件:
import Vue from 'vue'
import 'normalize.css/normalize.css'// A modern alternative to CSS resets
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import locale from 'element-ui/lib/locale/lang/zh-CN' // lang i18n
import '@/styles/index.scss' // global css
import App from './App'
import router from './router'
import store from './store'
import '@/icons' // icon
import '@/permission' // permission control
import VueJsonp from 'vue-jsonp'
Vue.use(VueJsonp)
Vue.use(ElementUI, { locale })
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
可以看到用到了我们自定义的permission,所以在一开始就会读取permission.js:
2、permission.js:
import router from './router'
import store from './store'
import NProgress from 'nprogress' // Progress 进度条
import 'nprogress/nprogress.css'// Progress 进度条样式
import { Message } from 'element-ui'
import { getToken } from '@/utils/auth' // 验权
const whiteList = ['/login'] // 不重定向白名单
router.beforeEach((to, from, next) => {
NProgress.start()
if (getToken()) {
if (to.path === '/login') {
next({ path: '/' })
NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it
} else {
if (store.getters.roles.length === 0) {
// 1、拉取用户姓名
store.dispatch('GetUserName').then(res => {
})
// 2、拉取用户权限信息
store.dispatch('GetInfo').then(res => {
// 从后端获取的权限
const authorities = res.data
// 前端路由加载动态权限
store.dispatch('GenerateRoutes', { authorities }).then(() => { // 生成可访问的路由表
// alert('store.getters.addRouters' + store.getters.addRouters.length)
router.addRoutes(store.getters.addRouters)// 添加动态路由
next({ ...to })// hack方法 确保addRoutes已完成
})
}).catch((err) => {
store.dispatch('FedLogOut').then(() => {
Message.error(err || 'Verification failed, please login again')
next({ path: '/' })
})
})
} else {
next()
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next('/login')
NProgress.done()
}
}
})
router.afterEach(() => {
NProgress.done() // 结束Progress
})
没有token,说明没有登录过,判断访问路径是否在白名单内,在则跳转,不在则跳转到登录页面;
有token,说明登录过,访问路径如果是登录,则跳转到首页;否则判断是否有权限(roles数组是否为空),有则直接跳转,否则调用store拉取用户基本信息,包括用户名和权限。
三、页面:
1、dashboard/index:
name:{{name}}
roles:{{role}}
2、login/index:
登录
取消