Vue
項目實現(xiàn)動態(tài)路由的方式大體可分為兩種:
- 前端將全部路由規(guī)定好邪码,登錄時根據(jù)用戶角色權限來動態(tài)展示路由邑飒;
- 路由存儲在數(shù)據(jù)庫中携茂,前端通過接口獲取當前用戶對應路由列表并進行渲染米母;
第一種方式在很多Vue UI Admin
上都實現(xiàn)了茂腥,可以去讀一下他們的源碼理解具體的實現(xiàn)思路,這里就不過多展開。第二種方式現(xiàn)在來說也比較常見了,因為近期項目正好用到所以單獨講一下欺栗,這里我使用的方案是利用Vue Router
的一些特性實現(xiàn)后端主導的動態(tài)路由。
使用到的功能特性
Vue Router 全局前置守衛(wèi)
這里我們主要借助全局前置守衛(wèi)的「前置」特性,在頁面加載前將當前用戶所用到的路由列表注入到Router
實例中迟几,注入使用到的方法則是下面的router.addRoutes
方法消请。
Vue Router router.addRoutes 實例方法
router.addRoutes
方法可以為Router
實例動態(tài)添加路由規(guī)則,剛好為我們實現(xiàn)動態(tài)路由提供了注入方法类腮。
Vue Router 路由懶加載
懶加載這個功能不是動態(tài)路由的必要功能臊泰,但既然提供了這一特性,所以就直接在項目中使用了蚜枢。
具體思路
基礎信息準備
前端代碼實現(xiàn)基本靜態(tài)路由缸逃,例如:登錄頁路由,服務器錯誤頁路由等(這里有一個坑厂抽,后面講)需频。數(shù)據(jù)庫存儲全部動態(tài)路由信息。
數(shù)據(jù)庫如何存儲動態(tài)路由信息修肠?我選擇的方案是現(xiàn)將路由引用的對象字符串化贺辰,再將路由列表轉(zhuǎn)化為JSON
格式傳輸給后端户盯,經(jīng)后端處理后存儲到數(shù)據(jù)庫里嵌施。總之在前后端進行傳遞的是JSON
格式的路由列表信息莽鸭。
如何將路由中引用的對象字符串化吗伤?我遇到的實際問題是:使用的UI
組件提供了布局方案,需要引用布局組件并在子路由處引用具體頁面硫眨。我選擇的解決方案是:區(qū)別對待需要引用布局組件的component
屬性足淆,使用簡短字符串代替布局組件,使用文件路徑字符串代替頁面引入礁阁。具體實現(xiàn)可以看后面的代碼實例巧号。
利用全局前置守衛(wèi)對路由信息進行判斷
判斷用戶是否登錄 ——》若未登錄,跳轉(zhuǎn)至登錄頁面 ——》若已經(jīng)登錄姥闭,判斷是否已獲取路由列表 ——》若未獲取丹鸿,從后端獲取、解析并保存到Vuex
中 ——》若已獲取棚品,跳轉(zhuǎn)至目標頁面
這里我沒做太多考察靠欢,直接將取到數(shù)據(jù)存儲到了Vuex
中,在實際項目應用的過程中應考慮數(shù)據(jù)存儲的安全性铜跑。
如何實現(xiàn)路由列表解析门怪?
- 將
JSON
格式的路由信息解析為JavaScript
列表對象; - 利用列表對象的
filter
方法實現(xiàn)解析函數(shù)锅纺,通過component
判斷是否為布局組件掷空; - 若為布局組件,使用布局組件代替
component
字符串; - 若為具體頁面坦弟,使用
loadView
函數(shù)加載對應的具體頁面疼电; - 利用 router.addRoutes 方法動態(tài)添加路由
這一步就很簡單了,將解析好的路由列表通過router.addRoutes
方法添加到Router
實例中即可减拭。
簡單的實現(xiàn)代碼
// router/index.js
import Vue from 'vue'
import store from '@/store'
import Router from 'vue-router'
import { getToken } from '@/lib/util'
Vue.use(Router)
// 定義靜態(tài)路由
const staticRoutes = [
{
path: '/login',
name: 'login',
meta: {
title: '登錄頁面',
hideInMenu: true
},
component: () => import('@/view/login/login.vue')
},
{
path: '/401',
name: 'error_401',
meta: {
hideInMenu: true
},
component: () => import('@/view/error-page/401.vue')
},
{
path: '/500',
name: 'error_500',
meta: {
hideInMenu: true
},
component: () => import('@/view/error-page/500.vue')
}
]
// 定義登錄頁面名稱(為了方便理解才定義的)
const LOGIN_PAGE_NAME = 'login'
// 實例化 Router 對象
const router = new Router({
staticRoutes,
mode: 'history'
})
// 定義全局前置守衛(wèi)(里面有兩個坑要注意)
router.beforeEach((to, from, next) => {
// 通過自定義方法獲取用戶 token 用來判斷用戶登錄狀態(tài)
const token = getToken()
if (!token && to.name !== LOGIN_PAGE_NAME) {
// 如果沒有登錄而且前往的頁面不是登錄頁面蔽豺,跳轉(zhuǎn)到登錄頁
next({ name: LOGIN_PAGE_NAME })
} else if (!token && to.name === LOGIN_PAGE_NAME) {
// 如果沒有登錄而且前往的頁面是登錄頁面,跳轉(zhuǎn)到登錄頁面
// 這里有一個坑拧粪,一定要注意這一步和上一步得分開寫
// 如果把前兩步判斷合并為 if (!token) next({ name:login })
// 則會形成登錄頁面無限刷新的錯誤修陡,具體成因后面解釋
next()
} else {
// 如果登錄了
if (!store.state.app.hasGetRoute) {
// 如果沒有獲取路由信息,先獲取路由信息而后跳轉(zhuǎn)
store.dispatch('getRouteList').then(() => {
router.addRoutes(store.state.app.routeList)
// 這里也是一個坑可霎,不能使用簡單的 next()
// 如果直接使用 next() 刷新后會一直白屏
next({ ...to, replace: true })
})
} else {
// 如果已經(jīng)獲取路由信息魄鸦,直接跳轉(zhuǎn)
next()
}
}
})
export default router
// store/index.js
import router from '@/router'
import Main from '@/components/main'
import { getToken } from '@/lib/util'
import { getRoute } from '@/api/app'
const loadView = (viewPath) => {
// 用字符串模板實現(xiàn)動態(tài) import 從而實現(xiàn)路由懶加載
return () => import(`@/view/${viewPath}`)
}
const filterAsyncRouter = (routeList) => {
return routeList.map((route) => {
if (route.component) {
if (route.component === 'Main') {
// 如果 component = Main 說明是布局組件
// 將真正的布局組件賦值給它
route.component = Main
} else {
// 如果不是布局組件就只能是頁面的引用了
// 利用懶加載函數(shù)將實際頁面賦值給它
route.component = loadView(route.component)
}
// 判斷是否存在子路由,并遞歸調(diào)用自己
if (route.children && route.children.length) {
route.children = filterAsyncRouter(route.children)
}
}
})
}
export default {
state: {
routeList: [],
token: getToken(),
hasGetRoute: false
},
mutations: {
setRouteList(state, data) {
// 先將 JSON 格式的路由列表解析為 JavaScript List
// 再用路由解析函數(shù)解析 List 為真正的路由列表
state.routeList = filterAsyncRouter(JSON.parse(data))
// 修改路由獲取狀態(tài)
state.hasGetRoute = true
}
},
atcions: {
getRouteList({ state, commit }) {
return new Promise((resolve) => {
const token = state.token
getRoute({ token }).then((res) => {
let data = res.data.data
// 注意這里取出的是 JSON 格式的路由列表
commit('setRouteList', data)
resolve()
})
})
}
}
}
常見問題
頁面卡在登錄頁面而且不斷刷新
這個問題的解決方案在「實現(xiàn)代碼」中已經(jīng)提到了癣朗,只需要在判斷登錄狀態(tài)的時候注意不要將兩種未登錄狀態(tài)混為一談即可拾因。但這樣治標不治本,因為同樣的問題可以由不同形式的代碼導致旷余,那導致問題的原因是什么那绢记?然我們慢慢分析:
我們先假設不小心把兩種未登錄的狀態(tài)混在一起判斷:
if (!token) {
next({ name: LOGIN_PAGE_NAME })
}
這里的next({ name: LOGIN_PAGE_NAME })
方法會再一次激活全局前置守衛(wèi),從而導致再一次進入判斷并觸發(fā)next({ name: LOGIN_PAGE_NAME })
正卧,如此遞歸調(diào)用下去蠢熄,頁面就會卡主并且不斷刷新。
動態(tài)路由配合路由懶加載
實現(xiàn)這一目的的方案也在代碼示例中展示了:
const loadView = (viewPath) => {
return () => import(`@/view/${viewPath}`)
}
這里是運用了一個 JavaScript 不太常用的特性:字符串模板炉旷,使用此特性讓不支持字符串拼接的import
操作能夠?qū)崿F(xiàn)動態(tài)import
不同的模塊签孔。
動態(tài)路由刷新后 404
這應該是本方案中最常見的一個錯誤之一,其原意是很多人在創(chuàng)建「基本靜態(tài)路由」的時候回把 404 頁面的路由也加入在里面窘行,從而導致頁面加載初期動態(tài)路由還沒有加入到路由實例中饥追,匹配范圍最廣的 404 頁面就會跳出來。解決方法就是將 404 頁面的路由也加入到動態(tài)路由中罐盔。
動態(tài)路由刷新后變空白頁
造成這一問題的原因有很多但绕,我這里遇到的問題是使用 vue-router addRouter添加動態(tài)路由,第一次跳轉(zhuǎn)正常翘骂,刷新后變空白壁熄?
let flag = 0
router.beforeEach((to, from, next) => {
if(to.path !== '/login'){
if(flag == 0){
console.log(flag)
let privileges = ['list','edit'];
let permission_routes = routeUtil.GenerateRoutes(privileges);
router.addRoutes(permission_routes);
flag++
next({ ...to, replace: true })
}else{
next()
}
}else{
next();
}
})
動態(tài)路由頁面刷新時 Title 不穩(wěn)定
造成這一問題的原因很簡單:因為頁面刷新的時候路由信息還沒加載進來,所以根本沒有標題信息可供加載碳竟。