Vue Router 實現動態路由和常見問題解決方案

何爲動態路由

我的理解:動態路由不一樣於常見的靜態路由,能夠根據不一樣的「因素」而改變站點路由列表。常見的動態路由大都是用來實現:多用戶權限系統不一樣用戶展現不一樣導航菜單。javascript

如何利用Vue Router 實現動態路由

Vue 項目實現動態路由的方式大致可分爲兩種:html

  1. 前端將所有路由規定好,登陸時根據用戶角色權限來動態展現路由;
  2. 路由存儲在數據庫中,前端經過接口獲取當前用戶對應路由列表並進行渲染;

第一種方式在不少 Vue UI Admin 上都實現了,能夠去讀一下他們的源碼理解具體的實現思路,這裏就不過多展開。 第二種方式如今來講也比較常見了,由於近期項目正好用到因此單獨講一下,這裏我使用的方案是利用 Vue Router 的一些特性實現後端主導的動態路由。前端

使用到的功能特性

Vue Router 全局前置守衛

官網解釋vue

這裏咱們主要藉助全局前置守衛的「前置」特性,在頁面加載前將當前用戶所用到的路由列表注入到 Router 實例中,注入使用到的方法則是下面的 router.addRoutes 方法。java

Vue Router router.addRoutes 實例方法

官網解釋vue-router

router.addRoutes 方法能夠爲 Router 實例動態添加路由規則,恰好爲咱們實現動態路由提供了注入方法。數據庫

Vue Router 路由懶加載

官網解釋segmentfault

懶加載這個功能不是動態路由的必要功能,但既然提供了這一特性,因此就直接在項目中使用了。後端

具體思路

基礎信息準備

前端代碼實現基本靜態路由,例如:登陸頁路由,服務器錯誤頁路由等(這裏有一個坑,後面講)。數據庫存儲所有動態路由信息。api

數據庫如何存儲動態路由信息? 我選擇的方案是現將路由引用的對象字符串化,再將路由列表轉化爲 JSON 格式傳輸給後端,經後端處理後存儲到數據庫裏。總之在先後端進行傳遞的是 JSON 格式的路由列表信息。

如何將路由中引用的對象字符串化? 我遇到的實際問題是:使用的 UI 組件提供了佈局方案,須要引用佈局組件並在子路由處引用具體頁面。 我選擇的解決方案是:區別對待須要引用佈局組件的 component 屬性,使用簡短字符串代替佈局組件,使用文件路徑字符串代替頁面引入。 具體實現能夠看後面的代碼實例。

利用全局前置守衛對路由信息進行判斷

1-判斷用戶是否登陸 1.1-若未登陸,跳轉至登陸頁面 1.2-若已經登陸,判斷是否已獲取路由列表 1.2.1-若未獲取,從後端獲取、解析並保存到 Vuex 中 1.2.2-若已獲取,跳轉至目標頁面

這裏我沒作太多考察,直接將取到數據存儲到了 Vuex 中,在實際項目應用的過程當中應考慮數據存儲的安全性。

如何實現路由列表解析?

  1. JSON 格式的路由信息解析爲 JavaScript 列表對象;
  2. 利用列表對象的 filter 方法實現解析函數,經過 component 判斷是否爲佈局組件;
  3. 若爲佈局組件,使用佈局組件代替 component 字符串;
  4. 若爲具體頁面,使用 loadView 函數加載對應的具體頁面;

利用 router.addRoutes 方法動態添加路由

這一步就很簡單了,將解析好的路由列表經過 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 頁面就會跳出來。解決方法就是將 404 頁面的路由也加入到動態路由中。

動態路由刷新後變空白頁

形成這一問題的緣由有不少,我這裏遇到的問題是使用 參考文章3 解決的,但具體原理我還沒弄清楚,等我作一下研究再來更新。

動態路由頁面刷新時 Title 不穩定

形成這一問題的緣由很簡單:由於頁面刷新的時候路由信息還沒加載進來,因此根本沒有標題信息可供加載。可是我還沒找到比較好的解決方案,一樣等我研究一下再更新。

參考

  1. 大師兄:Vue 動態路由的實現……
  2. Vue Router 文檔頁面
  3. rambo:vue router 動態路由 刷新後變空白頁
相關文章
相關標籤/搜索