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%;
width: 100%;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$sideBarWidth});
transition: width 0.28s;
}
.hideSidebar .fixed-header {
width: calc(100% - 54px);
}
.mobile .fixed-header {
width: 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>