我的理解:動態路由不一樣於常見的靜態路由,能夠根據不一樣的「因素」而改變站點路由列表。常見的動態路由大都是用來實現:多用戶權限系統不一樣用戶展現不一樣導航菜單。javascript
Vue
項目實現動態路由的方式大致可分爲兩種:html
第一種方式在不少 Vue UI Admin
上都實現了,能夠去讀一下他們的源碼理解具體的實現思路,這裏就不過多展開。 第二種方式如今來講也比較常見了,由於近期項目正好用到因此單獨講一下,這裏我使用的方案是利用 Vue Router
的一些特性實現後端主導的動態路由。前端
官網解釋vue
這裏咱們主要藉助全局前置守衛的「前置」特性,在頁面加載前將當前用戶所用到的路由列表注入到 Router
實例中,注入使用到的方法則是下面的 router.addRoutes
方法。java
官網解釋vue-router
router.addRoutes
方法能夠爲 Router
實例動態添加路由規則,恰好爲咱們實現動態路由提供了注入方法。數據庫
官網解釋segmentfault
懶加載這個功能不是動態路由的必要功能,但既然提供了這一特性,因此就直接在項目中使用了。後端
前端代碼實現基本靜態路由,例如:登陸頁路由,服務器錯誤頁路由等(這裏有一個坑,後面講)。數據庫存儲所有動態路由信息。api
數據庫如何存儲動態路由信息? 我選擇的方案是現將路由引用的對象字符串化,再將路由列表轉化爲 JSON
格式傳輸給後端,經後端處理後存儲到數據庫裏。總之在先後端進行傳遞的是 JSON
格式的路由列表信息。
如何將路由中引用的對象字符串化? 我遇到的實際問題是:使用的 UI
組件提供了佈局方案,須要引用佈局組件並在子路由處引用具體頁面。 我選擇的解決方案是:區別對待須要引用佈局組件的 component
屬性,使用簡短字符串代替佈局組件,使用文件路徑字符串代替頁面引入。 具體實現能夠看後面的代碼實例。
1-判斷用戶是否登陸 1.1-若未登陸,跳轉至登陸頁面 1.2-若已經登陸,判斷是否已獲取路由列表 1.2.1-若未獲取,從後端獲取、解析並保存到 Vuex
中 1.2.2-若已獲取,跳轉至目標頁面
這裏我沒作太多考察,直接將取到數據存儲到了 Vuex
中,在實際項目應用的過程當中應考慮數據存儲的安全性。
如何實現路由列表解析?
JSON
格式的路由信息解析爲 JavaScript
列表對象;filter
方法實現解析函數,經過 component
判斷是否爲佈局組件;component
字符串;loadView
函數加載對應的具體頁面;這一步就很簡單了,將解析好的路由列表經過 router.addRoutes
方法添加到 Router
實例中便可。
// router/index.js
import Vue from 'vue'
import store from '@/store'
import Router from 'vue-router'
import { getToken } from '@/lib/util'
Vue.use(Router)
// 定義靜態路由
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({
routes: staticRoutes,
mode: 'history'
})
// 定義全局前置守衛(裏面有兩個坑要注意)
router.beforeEach((to, from, next) => {
// 經過自定義方法獲取用戶 token 用來判斷用戶登陸狀態
const token = getToken()
if (!token && to.name !== LOGIN_PAGE_NAME) {
// 若是沒有登陸並且前往的頁面不是登陸頁面,跳轉到登陸頁
next({ name: LOGIN_PAGE_NAME })
} else if (!token && to.name === LOGIN_PAGE_NAME) {
// 若是沒有登陸並且前往的頁面是登陸頁面,跳轉到登陸頁面
// 這裏有一個坑,必定要注意這一步和上一步得分開寫
// 若是把前兩步判斷合併爲 if (!token) next({ name:login })
// 則會造成登陸頁面無限刷新的錯誤,具體成因後面解釋
next()
} else {
// 若是登陸了
if (!store.state.app.hasGetRoute) {
// 若是沒有獲取路由信息,先獲取路由信息然後跳轉
store.dispatch('getRouteList').then(() => {
router.addRoutes(store.state.app.routeList)
// 這裏也是一個坑,不能使用簡單的 next()
// 若是直接使用 next() 刷新後會一直白屏
next({ ...to, replace: true })
})
} else {
// 若是已經獲取路由信息,直接跳轉
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) => {
// 用字符串模板實現動態 import 從而實現路由懶加載
return () => import(`@/view/${viewPath}`)
}
const filterAsyncRouter = (routeList) => {
return routeList.filter((route) => {
if (route.component) {
if (route.component === 'Main') {
// 若是 component = Main 說明是佈局組件
// 將真正的佈局組件賦值給它
route.component = Main
} else {
// 若是不是佈局組件就只能是頁面的引用了
// 利用懶加載函數將實際頁面賦值給它
route.component = loadView(route.component)
}
// 判斷是否存在子路由,並遞歸調用本身
if (route.children && route.children.length) {
route.children = filterAsyncRouter(route.children)
}
return true
}
})
}
export default {
state: {
routeList: [],
token: getToken(),
hasGetRoute: false
},
mutations: {
setRouteList(state, data) {
// 先將 JSON 格式的路由列表解析爲 JavaScript List
// 再用路由解析函數解析 List 爲真正的路由列表
state.routeList = filterAsyncRouter(JSON.parse(data))
// 修改路由獲取狀態
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()
})
})
}
}
}
複製代碼
這個問題的解決方案在「實現代碼」中已經提到了,只須要在判斷登陸狀態的時候注意不要將兩種未登陸狀態混爲一談便可。但這樣治標不治本,由於一樣的問題能夠由不一樣形式的代碼致使,那致使問題的緣由是什麼那?然咱們慢慢分析:
咱們先假設不當心把兩種未登陸的狀態混在一塊兒判斷:
if (!token) {
next({ name: LOGIN_PAGE_NAME })
}
複製代碼
這裏的 next({ name: LOGIN_PAGE_NAME })
方法會再一次激活全局前置守衛,從而致使再一次進入判斷並觸發 next({ name: LOGIN_PAGE_NAME })
,如此遞歸調用下去,頁面就會卡主而且不斷刷新。
實現這一目的的方案也在代碼示例中展現了:
const loadView = (viewPath) => {
return () => import(`@/view/${viewPath}`)
}
複製代碼
這裏是運用了一個 JavaScript 不太經常使用的特性:字符串模板,使用此特性讓不支持字符串拼接的 import
操做可以實現動態 import
不一樣的模塊。
這應該是本方案中最多見的一個錯誤之一,其原意是不少人在建立「基本靜態路由」的時候回把 404 頁面的路由也加入在裏面,從而致使頁面加載初期動態路由尚未加入到路由實例中,匹配範圍最廣的 404 頁面就會跳出來。解決方法就是將 404 頁面的路由也加入到動態路由中。
形成這一問題的緣由有不少,我這裏遇到的問題是使用 參考文章3 解決的,但具體原理我還沒弄清楚,等我作一下研究再來更新。
形成這一問題的緣由很簡單:由於頁面刷新的時候路由信息還沒加載進來,因此根本沒有標題信息可供加載。可是我還沒找到比較好的解決方案,一樣等我研究一下再更新。