Vue實現動態路由及登錄&404頁面跳轉控制&頁面刷新空白解決方案 by:授客 QQ:1033553122 開發環境 Win 10 Vue 2.9.6 node-v10.15.3-x64.msi 下載地址: https://nodejs.org/en/ 代碼片段(router/index.js) 說 ...
Vue實現動態路由及登錄&404頁面跳轉控制&頁面刷新空白解決方案
by:授客 QQ:1033553122
開發環境
Win 10
Vue 2.9.6
node-v10.15.3-x64.msi
下載地址:
代碼片段(router/index.js)
說明:代碼中動態路由的獲取是通過解析菜單資源獲取的
import Vue from "vue";
import Router from "vue-router";
import store from "@/store";
import Index from "@/views/Index";
import api from "@/common/network/api";
import Login from "@/views/Login";
import NotFound from "@/views/Error/404";
import Cookies from "js-cookie";
Vue.use(Router);
// 靜態路由
conststaticRoute = [
{
path: "/",
name: "Index",
component: Index
},
{
path: "/login",
name: "登錄",
component: Login
},
{
path: "/error/404",
name: "notFound",
component: NotFound
}
];
const router = new Router({
mode: "history", // 去掉 http://localhost:8080/#的#
routes: staticRoute
});
/*vue是單頁應用,刷新時,重新創建實例,需要重新載入的動態路由,不然匹配不到路由,出現頁面空白的情況*/
router.beforeEach((to, from, next) => {
// let userId = sessionStorage.getItem("userId") // 登錄界面登錄成功之後,會把用戶信息保存在會話 // 關閉瀏覽器tab標簽頁,重新打開一個tab頁,重新訪問該站點,這時會開啟一個新的會話,原先登錄後保存的userId丟失
let token = Cookies.get("token"); // 僅登錄情況才存在token
if (to.path === "/login") {
// 如果是訪問登錄界面,如果token存在,代表已登錄過,跳轉到主頁
if (token) {
next({ path: "/" });
} else {
// 否則,跳轉到登錄頁面
next();
}
} else {
if (to.meta.requireAuth) {
// 如果訪問非登錄界面,且路由需要登錄
if (!token) {
// 用戶token不存在,代表未登錄,跳轉登錄
next({
path: "/login",
query: { redirect: to.fullPath } // 把要跳轉的路由path作為參數,登錄成功後跳轉到該路由
});
} else {
// 用戶已登錄,添加動態菜單和路由後直接跳轉
addDynamicMenuAndRoutes(to, from, next);
// 註釋掉一下代碼是addDynamicMenuAndRoutes函數中axios非同步請求獲取菜單,請求還沒返回結果就開始執行next()函數,這樣會導致重覆請求菜單資源,特別是登錄的時候,會發送兩次請求,解決方案就是把以下註釋掉的代碼放到動態添加菜單和路由方法里執行
//next()
//if (to.matched.length == 0) {
// router.push(to.path)
//}
}
} else {
// 不需要登錄,添加動態菜單和路由後,直接跳轉
addDynamicMenuAndRoutes(to, from, next);
}
}
});
/**
* 載入動態菜單和路由
*/
function addDynamicMenuAndRoutes(userName, to, from, next) {
if (store.state.app.menuRouteLoaded) {
console.log("動態菜單和路由已經存在.");
next();
return;
}
//優先從本地sessionStorage獲取
let navMenuData = sessionStorage.getItem("navMenuData");
if (navMenuData) {
navMenuData = JSON.parse(navMenuData);
// 獲取動態路由
let dynamicRoutes = getDynamicRoutes(navMenuData);
// 設置獲取的路由全部為根路由(path值為 "/")下的子路由
// 這裡,根據靜態路由配置可知router.options.routes[0]為根路由
router.options.routes[0].children = [].concat(dynamicRoutes);
// 這裡為啥不把 * 匹配放到靜態路由的最後面,是因為如果放置在靜態路由最後面,作為一級路由,當url同前面的路由都不匹配時,會匹配到 *,這樣一來,刷新頁面時,由於還沒載入動態路由,預期和動態路由匹配的url,會匹配到靜態路由的 *,然後跳轉404頁面。
if (router.options.routes[router.options.routes.length - 1].path != "*") {
router.options.routes = router.options.routes.concat([
{
path: "*",
name: "notFound",
component: NotFound
}
]);
}
// 添加路由,讓路由生效
router.addRoutes(router.options.routes);
// 存儲導航菜單list數據
store.commit("setNavMenu", navMenuData);
// 設置菜單為已載入狀態
store.commit("setMenuRouteLoadStatus", true);
next();
if (to.matched.length == 0) {
router.push(to.path);
}
} else {
// 本地sessionStorage獲取不到,從伺服器端讀取
api.menu
.getNavMenuData()
.then(res => {
// 獲取動態路由
let dynamicRoutes = getDynamicRoutes(res.data);
// 添加路由
router.options.routes[0].children = [].concat(dynamicRoutes);
// 如果要添加為一級路由,則按如下方式拼接路由
// router.options.routes = staticRoute.concat(dynamicRoutes)
// 註意,以下寫法會導致添加的路由不起作用,為錯誤的寫法
// let otherVar=staticRoute.concat(dynamicRoutes)
// router.addRoutes(otherVar); //添加的路由不起作用
if (
router.options.routes[router.options.routes.length - 1].path != "*"
) {
router.options.routes = router.options.routes.concat([
{
path: "*",
name: "notFound",
component: NotFound
}
]);
}
router.addRoutes(router.options.routes); //會產生重覆路由,控制台會有warn提示,但是不影響,vue-router會自動去重,
// 存儲導航菜單list數據
sessionStorage.setItem("navMenuData", JSON.stringify(res.data));
store.commit("setNavMenu", res.data);
// 設置菜單為已載入狀態
store.commit("setMenuRouteLoadStatus", true);
next(); /* 註意:路由匹配是在router.addRoutes之前完成的,所以,即便使用router.addRoutes添加動態路由,也會出現to.matched.length也會等於0的情況,即沒匹配到路由,所以to.matched.length等於0的情況下,再次執行router.push(to.path),這樣,再次觸發beforeEach函數調用)*/
if (to.matched.length == 0) {
router.push(to.path);
}
})
.then(res => {
// 保存用戶許可權標識集合
})
.catch(function(res) {
console.log(res);
});
}
}
/**
* 獲取動態(菜單)路由配置
* @param {*} menuList菜單列表
* @param {*} routes遞歸創建的動態(菜單)路由
*/
function getDynamicRoutes(menuList = [], parentRoute = []) {
for (var i = 0; i < menuList.length; i++) {
var route = {}; // 存放路由配置
if (menuList[i].url && /\S/.test(menuList[i].url)) {
// url不為空,且包含任何非空白字元
route = {
path: menuList[i].url,
component: null,
name: menuList[i].name,
children: [],
meta: {
icon: menuList[i].icon,
index: menuList[i].id,
requireAuth: menuList[i].requireAuth // 可選值true、false 添加該欄位,表示進入這個路由是需要登錄的
}
};
try {
// 根據菜單URL動態載入vue組件,這裡要求vue組件須按照url路徑存儲
// 如url="sys/user",則組件路徑應是"@/views/sys/user.vue",否則組件載入不到
let array = [];
if (menuList[i].url.startsWith("/")) {
// 如果url 以 "/"打頭,所以要先去掉左側的 "/",否則組件路徑會出現 @/views//sys/user的情況
array = menuList[i].url.substring(1).split("/");
} else {
array = menuList[i].url.split("/");
}
let url = ""; // 存放url對應的組件路徑
// 組件所在目錄及組件名稱第一個字母大寫,所以需要替換
for (let i = 0; i < array.length; i++) {
url +=
array[i].substring(0, 1).toUpperCase() +
array[i].substring(1) +
"/";
}
url = url.substring(0, url.length - 1); // 去掉最右側的 '/'
route["component"] = resolve => require([`@/views/${url}`], resolve);
} catch (e) {
console.log("根據菜單URL動態載入vue組件失敗:" + e);
}
if (menuList[i].children && menuList[i].children.length >= 1) {
getDynamicRoutes(menuList[i].children, route["children"]);
}
} else {
if (menuList[i].children && menuList[i].children.length >= 1) {
getDynamicRoutes(menuList[i].children, parentRoute);
}
}
if (JSON.stringify(route) != "{}") {
parentRoute.push(route);
}
}
return parentRoute;
}
export default router;
代碼片段(src/Login.vue)
methods: {
login() {
let userInfo = {
account:this.loginForm.account,
password:this.loginForm.password,
captcha:this.loginForm.captcha
};
this.$api.login.login(userInfo)
.then(res=> {
if (res.success) {
Cookies.set("token", res.data.token); // 保存token到Cookie
sessionStorage.setItem("userName", userInfo.account); // 保存用戶信息到本地會話
this.$store.commit("setMenuRouteLoadStatus", false); // 重置導航菜單載入狀態為false
if (JSON.stringify(this.$route.query) != "{}") { // 不需要跳轉到登錄前的頁面
this.$router.push(this.$route.query.redirect); // 登錄成功,跳轉之前頁面
} else {
this.$router.push("/")
}
} else {
this.$message({
message:res.msg,
type:"error"
});
}
this.loading = false;
})
.catch(res=> {
this.$message({
message:res.message,
type:"error"
});
});
},
菜單數據
data: [{
id: 1,
createBy: null,
createTime: null,
lastUpdateBy: null,
lastUpdateTime: null,
parentId: 0,
parentName: null,
name: "首頁",
url: "/home",
perms: null,
requireAuth: true,
type: 0,
icon: "fa fa-home fa-lg",
orderNum: 1,
level: 0,
children: [{
id: 2,
createBy: null,
createTime: null,
lastUpdateBy: null,
lastUpdateTime: null,
parentId: 1,
parentName: null,
name: "首頁二級菜單1",
url: "",
perms: null,
requireAuth: true,
type: 1,
icon: "fa fa-home fa-lg",
orderNum: 1,
level: 0,
children: [{
id: 3,
createBy: null,
createTime: null,
lastUpdateBy: null,
lastUpdateTime: null,
parentId: 2,
parentName: null,
name: "首頁三級菜單1",
url: "",
perms: null,
requireAuth: true,
type: 1,
icon: "fa fa-home fa-lg",
orderNum: 1,
level: 0,
children: [{
id: 4,
createBy: null,
createTime: null,
lastUpdateBy: null,
lastUpdateTime: null,
parentId: 3,
parentName: null,
name: "首頁四級菜單1",
url: "/home/level4Menu1",
perms: null,
requireAuth: true,
type: 0,
icon: "fa fa-home fa-lg",
orderNum: 1,
level: 0,
children: []
}]
},
{
id: 5,
createBy: null,
createTime: null,
lastUpdateBy: null,
lastUpdateTime: null,
parentId: 2,
parentName: null,
name: "首頁三級菜單2",
url: "/home/level3Menu2",
perms: null,
requireAuth: true,
type: 0,
icon: "fa fa-home fa-lg",
orderNum: 2,
level: 0,
children: []
}
]
&n