1. 程式人生 > 其它 >Vue.js_Vue Router 4.x 動態路由解決重新整理空白

Vue.js_Vue Router 4.x 動態路由解決重新整理空白

問題描述:

基於對 Vue Router 3.x 沒有改變前,我們常規的實現一定,在 store 中根據獲取的使用者許可權,對路由進行過濾並返回,然後到路由守衛的地方,使用 addRoutes 動態新增路由。但是在 Vue Router 4.x 以後對這部分進行了修改。
修改點:

  1. 刪除API addRoutes
  2. 改用API addRoute,新增API removeRoute,下附官方該 API 的說明:

也就是說這兩個API是我們實現動態路由的關鍵,但是按照官方的說明及以往的開發經驗,最終我出錯了,動態路由頁面重新整理空白。分析下問題原因:

動態路由實現常規思路

路由

  1. 建立 ./src/router
    目錄
  2. 新建 index.ts 檔案用於書寫通用的路由 routescreateRouter 物件
// ./src/router/index.ts

import { createWebHistory, createRouter } from 'vue-router'
import GuardEach from './guard'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    { path: '/*', redirect: '/'},
    { path: '/404', name: '404', component: () => import('@/views/common/404.vue') },
    { path: '/login', name: 'Login', component: () => import('@/views/common/login.vue') },
    { path: '/', name: 'Home', redirect: '/home', component: () => import('@/views/layout/index.vue'),
      children: [
        { path: '/home', name: 'Home-Index', component: () => import('@/views/home/index.vue') }
      ]
    }
  ]
})
GuardEach(router)

export default router
  1. 新建 async 目錄,用於按業務模組存放你的非同步路由 routes,當然瞭如果的的動態路由實在少,你可以可以寫在 index.ts 中匯出,或者一個檔案搞定,這裡演示我們就比如他是一個 ts 檔案
// ./src/router/async.ts

import type { RouteRecordRaw } from 'vue-router'

export const Member: Readonly<RouteRecordRaw[]> = [
  { path: '/member', name: 'Member', redirect: '/member/list', meta: { code: 1 },
    component: () => import('@/views/layout/index.vue'),
    children: [
      { path: 'list', name: 'Member-List', meta: { code: 2 } component: () => import('@/views/member/list.vue') },
      { path: 'item', name: 'Member-Item', meta: { code: 3 } component: () => import('@/views/member/item.vue') }
    ]
  }
]
  1. 新建 guard.ts 檔案,用於書寫路由守衛邏輯(大部分程式設計師習慣將其放至在 ./src/permission.ts中,這個看個人喜好,無關痛癢)
// ./src/router/guard.ts

import { userStore } from '@/stores'
import type { Router } from 'vue-router'

let hasInitAuth = true
export default function (router: Router) {
  router.beforeEach((to, from, next) => {
    const user = userStore()

    const authRoutes = user.getAuthRoutes(router.options.routes) || []
    if (hasInitAuth) {
      authRoutes.forEach((item: any) => router.addRoute(item))
      hasInitAuth = false
      router.push({ ...to, replace: true })
    }

    next()
  })
}
  1. 這裡為了處理路由和選單,我新增了一個檔案 extend.ts,用於存放一些擴充套件的函式(僅供參考)
// ./src/router/extend.ts

import type { RouteRecordRaw } from 'vue-router'

// 生成有許可權的路由表
export function createAuthRoutes(asyncRoutes: Readonly<RouteRecordRaw[]>, authCode: Number) {
  return asyncRoutes.filter((s) => {
    const code = s.meta?.code
    const child = s.children
    if (child && child.length > 0) createAuthRoutes(child, authCode)
    return !code || (code && authCode)
  })
}

// 生成有許可權的導航選單
export function createNavMenus(allRoutes: Readonly<RouteRecordRaw[]>) {
  function mapItem(data: Readonly<RouteRecordRaw[]>): any[] {
    return data.map((s) => {
      let children = []
      if (s.children && s.children.length > 0) {
        children = mapItem(s.children) || []
      }
      return {
        title: s.meta?.name as string,
        children: children.length === 0 ? undefined : children
      }
    })
  }

  function filterItem(data: Readonly<RouteRecordRaw[]>): any[] {
    return data.filter((s) => {
      if (s.children && s.children.length > 0) {
        s.children = filterItem(s.children)
      }
      return true
    })
  }
  return filterItem(mapItem(allRoutes))
}

stores

  1. 建立一個模組或一個檔案,用來實現對 ./src/router/async/**/*.ts 的所有 routes 們進行許可權的過濾,以及右側選單的生成,最終你需要在你的 state 中儲存兩個值 authAsyncRoutes(有許可權的動態路由列表),authNavMenus(有許可權的導航選單列表)
  2. 根據你的業務場景需要,對這兩個值做相關的持久化儲存
// ./src/stores/index.ts

import { defineStore } from 'pinia'
import type { RouteRecordRaw } from 'vue-router'
import { createAuthRoutes, createNavMenus } from '@/router/extend'
import asyncRoutes from '@/router/async'

export const userStore = defineStore('USER_STORES', {
  state: () => ({
    authNavMenus: [] as any[]
  }),

  actions: {
    getAuthRoutes(syncRoutes: Readonly<RouteRecordRaw[]>) {
      /**
       * 1. 獲取快取使用者許可權路由
       * 2. 獲取快取使用者許可權選單
       * 3. 如果存在快取,則給導航選單賦值並返回有許可權的路由
       */
      const authAsyncRoutes = sessionStorage.get('UserRoutes')
      const authNavMenus = sessionStorage.get('UserMenus')
      if (authAsyncRoutes && authNavMenus) {
        this.authNavMenus = authNavMenus
        return authAsyncRoutes
      }

      /**
       * 4. 如果不存在快取,則獲取當前使用者的許可權配置(我的業務場景是二進位制許可權配置,因此是一個最大許可權值)
       * 5. 根據許可權配置,生成有許可權的路由
       * 6. 根據有許可權的路由,生成導航選單,並賦值
       * 7. 將有許可權的路由和導航選單進行快取
       * 9. 返回有許可權的路由
       */
      const authCode = sessionStorage.get('UserAuthCode')
      if (authCode) {
        const authAsyncRoutes = createAuthRoutes(asyncRoutes, authCode)
        const allRoutes = syncRoutes.concat(authAsyncRoutes)
        this.authNavMenus = createNavMenus(allRoutes)

        sessionStorage.set('UserRoutes', authAsyncRoutes)
        sessionStorage.set('UserMenus', this.authNavMenus)
        return authAsyncRoutes
      }
    }
  }
})

main.ts

  1. 引入路由和 stores,並use,ok!感受報錯和重新整理白屏的洗禮!
// ./src/main.ts

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'

const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)

app.mount('#app')

是不是發現沒有沒有看出什麼問題,其實這其中有兩個問題

  1. 控制檯報錯問題,具體是因為 next() 這個發生了一些修改,看了官方文件其實還是一知半解,經過不斷實驗,不斷探索,終於我發現了問題:
// ./src/router/guard.ts 中以下程式碼錯誤,這段程式碼等同於執行了一個 next()

router.push({ ...to, replace: true })

// ,,因此這應該修改為:
  if (hasInitAuth) {
    authRoutes.forEach((item: any) => router.addRoute(item))
    hasInitAuth = false
    router.push({ ...to, replace: true })
+  } else {
+    next()
+  }
- next()
  1. 解決報錯問題,發現重新整理空白,經檢視報錯提示及查看了快取的有許可權的路由發現,快取中是不存在路由元件關聯的,因此這裡我們需要手動將檢視元件匯入並關聯,需要在新增如下程式碼:
// ./src/router/extend.ts 新增如下程式碼:

/**
 * 動態新增路由當快取時只會儲存其路由清單樹,不會儲存其關聯的檢視元件
 * 故而當重新重新整理或進入頁面時,需要重新將檢視元件與路由清單樹關聯
 * 否則會導致頁面空白,無法正常顯示
 */
+  export function authRouteTreePlug(
+    authRoutesTree: Readonly<RouteRecordRaw[]>,
+    parentPath: string = ''
+  ) {
+    // 查閱資料得出,在這裡不支援 () => import() 的寫法,需要使用 import.meta.glob匯入元件,而後在使用,這裡不要使用 @ 哦,否則會找不到,具體原因不明
+    const modules = import.meta.glob('../views/**/*.vue')
+
+    return authRoutesTree.map((item) => {
+      const itemPath = item.path.slice(0, 1) === '/' ? item.path : `/${item.path}`
+      const hasChild = item.children && item.children.length > 0
+
+      // 由於這裡是管理後臺,因此一級路由需要使用layout佈局元件,故增加判斷,各位看官可以根據需求修改
+      const compPath = item.redirect && hasChild ? '/layout/index' : parentPath + itemPath
+      item.component = modules[`../views${compPath}.vue`]
+
+      if (hasChild) {
+        item.children = authRouteTreePlug(item.children as Readonly<RouteRecordRaw[]>, itemPath)
+      }
+      return item
+    })
+  }
// ./src/stores/index.ts 修改如下這段程式碼:

/**
 * 1. 獲取快取使用者許可權路由
 * 2. 獲取快取使用者許可權選單
 * 3. 如果存在快取,則給導航選單賦值並返回有許可權的路由
 */
+ const authAsyncRoutesTree = sessionStorage.get('UserRoutes')
  const authNavMenus = sessionStorage.get('UserMenus')
+ if (authAsyncRoutesTree && authNavMenus) {
+   const authAsyncRoutes = authRouteTreePlug(authRoutesTree)
    this.authNavMenus = authNavMenus
    return authAsyncRoutes
  }

這下終於解決了,完事!