SideBar加载其实就是根据传入的路由表进行判断,源码中组件多了点,笔记放到后续单独整理
权限加载及自定义布局
加载过程
页面加载过程考虑涉及了页面的加载、菜单加载、用户权限问题,所以先考虑问题如下:
- 用户登陆成功后,通过NProgess拦截获取用户token,判断是否有token
- 获取用户登陆信息,获取用户的roles,进入路由表拉取用户菜单(动态的权限菜单)
- 有权限后进入首页(布局页面),布局中的Sidebar根据获取的有路有权限的菜单进行动态生成
改造router
将router中的路由分为静态无需权限路由和动态加载的权限路由,同时将返回默认页定位到dashBoard的首页。其中路由先按照要求将meta下的属性都进行配置。这个在后期测试时发现一个问题。
当页面加载完后,使用新的浏览器TAB页打开一个动态加载的菜单时,页面并没有加载,并返回到了404页面。但浏览器地址栏的URL是正确且存在的。
经过仔细比较vue-element-admin的源码后,发现了关于404的代码注释。且404的路由是放到了动态加载的路由的最后面
// 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true }
经过查看vue-router的文档发现官方的说明如下:
当使用通配符路由时,请确保路由的顺序是正确的,也就是说含有通配符的路由应该放在最后。路由 { path: '*' } 通常用于客户端 404 错误。如果你使用了History 模式,请确保正确配置你的服务器。
添加JS-COOKIE
设计思路如下:用户登录后将token存储到cookie中,用户登陆后加载所具有权限的菜单,此时用户角色需要通过api来获取,前端不存储任何角色信息
安装
yarn add js-cookie
修改用户登陆,把用户token放入cookie。在utils下建立auth.js用来操作cookie。此处遇到了同源 同协议 同Domain但不同端口的cookie问题,后续需要看看如何处理
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);
}
修改用户登陆,增加获取用户信息的API
用户登陆和获取用户信息的方法在vuex中完成,修改stroe>modules>user.js如下:
import router from "@/router";
import { setToken, getToken } from "@/utils/auth";
import { getInfo, login } from "@/api/user";
const state = {
userInfo: {
name: "",
token: getToken(),
password: "",
roles: []
}
};
const actions = {
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.userInfo.token)
.then(response => {
const { data } = response;
if (!data) {
reject("Verification failed, please Login again.");
}
const { roles, name, token } = data.userInfo;
// roles must be a non-empty array
if (!roles || roles.length <= 0) {
reject("getInfo: roles must be a non-null array!");
}
commit("SET_ROLES", roles);
commit("SET_NAME", name);
commit("SET_TOKEN", token);
resolve(data);
})
.catch(error => {
reject(error);
});
});
},
submitlogin({ commit }, { payload }) {
const { username, password } = payload;
return new Promise((resolve, reject) => {
login(username.trim(), password)
.then(response => {
if (response.data.userInfo.token != "error") {
commit("SET_ROLES", response.data.userInfo.roles);
commit("SET_NAME", response.data.userInfo.name);
commit("SET_TOKEN", response.data.userInfo.token);
setToken(response.data.userInfo.token);
resolve();
router.push("/");
} else {
console.log(response.data.userInfo.token);
resolve();
router.push("/404");
}
})
.catch(error => {
reject(error);
});
});
}
};
const mutations = {
SET_TOKEN: (state, token) => {
state.userInfo.token = token;
},
SET_NAME: (state, name) => {
state.userInfo.name = name;
},
SET_ROLES: (state, roles) => {
state.userInfo.roles = roles;
}
};
export default {
namespaced: true,
state,
mutations,
actions
};
同时,将Mock使用的api请求抽取出来单独存放,在src目录下新建api目录,建立user.js文件用来向后端请求,src>api>user.js文件如下:
import request from "@/utils/request";
export function getInfo(token) {
return request({
url: "/api/user/login",
method: "get",
params: { token: token }
});
}
export function login(username, password) {
return request({
url: "/api/user/login",
method: "post",
data: { name: username, password: password }
});
}
此时用户角色的登陆修改完毕,下面来根据token判断用户是否登陆。
权限拦截
安装nprogress进度条
yarn add nprogress
在src下新建permission.js,先通过cookie获取用户token,如果有token,在通过getter从store中获取用户的roles,用户有权限,则继续请求。如果store中取的roles为空,则通过user.js中的action再次获取一下,并根据取到的角色加载响应的路由表。这里需要vue-router的导航守卫,并结合nprogress的进度条来完成。permission.js代码如下:
import router from "./router";
import store from "./store";
import NProgress from "nprogress"; // progress bar
import "nprogress/nprogress.css"; // progress bar style
import { getToken } from "@/utils/auth"; // get token from cookie
NProgress.configure({ showSpinner: false }); // NProgress Configuration
const whiteList = ["/user/login", "/auth-redirect"]; // no redirect whitelist
router.beforeEach(async (to, from, next) => {
// start progress bar
NProgress.start();
// determine whether the user has logged in
const hasToken = getToken();
if (hasToken) {
if (to.path === "/user/login") {
// if is logged in, redirect to the home page
next({ path: "/" });
NProgress.done();
} 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 { userInfo } = await store.dispatch("user/getInfo");
const { roles } = userInfo;
// 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");
console.log({ error: error, mes: "Has error" });
next(`/user/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(`user/login?redirect=${to.path}`);
NProgress.done();
}
}
});
router.afterEach(() => {
// finish progress bar
NProgress.done();
});
代码基本都是element-vue-admin中的源码,个人测试的地方修改的很小。在获取用role的时候使用了store中的getter,这里是配置了getter.js并注册到vuex中。
在src>store>getter.js中,同时也配置了权限相关的参数。代码如下:
const getters = {
roles: state => state.user.userInfo.roles,
permission_routes: state => state.permission.routes
};
export default getters;
修改src>store>index.js文件。这里没有使用源码的自动注册,后期如果代码量大了在修改。
import Vue from "vue";
import Vuex from "vuex";
import user from "./modules/user";
import permission from "./modules/permission";
import settings from "./modules/settings";
import getters from "./getters";
Vue.use(Vuex);
export default new Vuex.Store({
state: {},
mutations: {},
actions: {},
modules: { user, permission, settings },
getters
});
权限的获取
根据上面src>permission.js中的代码逻辑,其中在获取完用户的角色后需要根据角色获取路由表中的具有权限的路由,所以在vuex中增加方法来处理用户路由表的动态加载。在src>store>modules中新建permission.js文件,并注册。permission.js如下。总体逻辑比较清晰,根据用户的角色,然后与路由表中的meta元素中的roles进行判断,最后通过SET_ROUTES组合路由表
import { asyncRoutes, constantRoutes } from "@/router";
/**
* Use meta.role to determine if the current user has permission
* @param roles
* @param route
*/
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
return roles.some(role => route.meta.roles.includes(role));
} else {
return true;
}
}
/**
* Filter asynchronous routing tables by recursion
* @param routes asyncRoutes
* @param roles
*/
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) {
console.log({ permission: "store permission", roles: roles });
return new Promise(resolve => {
let accessedRoutes;
if (roles.includes("admin")) {
accessedRoutes = asyncRoutes || [];
} else {
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
}
commit("SET_ROUTES", accessedRoutes);
resolve(accessedRoutes);
});
}
};
export default {
namespaced: true,
state,
mutations,
actions
};
创建页面布局
上面已经获取了
页面布局思路为:
- AppMain 右侧主要显示区域
- Navbar 右侧顶部标题、个人设置等
- Sidebar 左侧菜单页面
- TagsView 右侧NavBar下面,用户打开的标签页
- RightPanel 右侧setting个性化的区域
涉及的布局的主要代码在src>layouts下。layout中的index.vue为总体的布局框架,通过router加载component的
<template>
<div :class="classObj" class="app-wrapper">
<sidebar class="sidebar-container" />
<div :class="{ hasTagsView: needTagsView }" class="main-container">
<div :class="{ 'fixed-header': fixedHeader }">
<navbar />
<tags-view v-if="needTagsView" />
</div>
<app-main />
<right-panel v-if="true"> </right-panel>
</div>
</div>
</template>
<script>
import AppMain from "./components/AppMain";
import Navbar from "./components/NavBar";
import Sidebar from "./components/Sidebar";
import TagsView from "./components/TagsView";
import RightPanel from "@/components/RightPanel";
import { mapState } from "vuex";
export default {
name: "Layout",
components: { AppMain, Navbar, Sidebar, TagsView, RightPanel },
computed: {
...mapState({
showSettings: state => state.settings.showSettings,
needTagsView: state => state.settings.tagsView,
fixedHeader: state => state.settings.fixedHeader
}),
classObj() {
return {
};
}
}
};
</script>
<style lang="scss" scoped>
@import "~@/styles/mixin.scss";
@import "~@/styles/variables.scss";
@import "~@/styles/sidebar.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
100%;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: 9;
calc(100% - #{$sideBarWidth});
transition: width 0.28s;
}
.hideSidebar .fixed-header {
calc(100% - 54px);
}
.mobile .fixed-header {
100%;
}
</style>
在element-vue-admin中的源码,首页加载有个resize的过程,这里省略了。同时直接引入了siderbar的样式。RightPanel是右侧自定义的功能区。暂时只是把组件添加上了。Navbar、TagsView也暂时用组件占位,没添加内容。
AppMain
AppMain.vue在src>layouts>components下。代码比较简单,暂时去掉了cacheViews的配置,省略了样式部分
<template>
<section class="app-main">
<transition name="fade-transform" mode="out-in">
<router-view :key="key" />
</transition>
</section>
</template>
<script>
export default {
name: "AppMain",
computed: {
key() {
return this.$route.path;
}
}
};
</script>