在現代的單頁應用程序(SPA)中,路由管理是一個核心功能。Vue Router 不僅提供了基本的路由功能,還支持多級嵌套路由和強大的導航守衛系統。今天,我們將深入探討如何利用 Vue Router 實現複雜的多級嵌套路由,並結合先前學習的概念,如 Pinia、Zod、Vee-Validate 等,打造一個功能豐富、類型安全的路由系統。我們還將實現一個記憶當前 tab 的左側導航欄,並探討如何使用 Vue Router 的過渡效果來增強用戶體驗。
首先,我們需要設置一個基本的路由結構,包括多級嵌套路由:
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { usePermissionStore } from '@/stores/usePermissionStore'
import { storeToRefs } from 'pinia'
export enum RoutesStatus {
Home = 'Home',
UserProfile = 'UserProfile',
UserSettings = 'UserSettings',
AdminDashboard = 'AdminDashboard',
UserSettings = 'userAccountSettings',
AdminUsers = 'AdminUsers',
NotFound = 'NotFound',
TokenFail = 'TokenFail',
}
const routes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('@/layouts/MainLayout.vue'),
children: [
{
path: '',
name: RoutesStatus.Home,
component: () => import('../pages/Home.vue')
},
{
path: 'user',
component: () => import('../pages/user/UserLayout.vue'),
children: [
{
path: 'profile',
name: RoutesStatus.UserProfile,
component: () => import('../pages/user/UserProfile.vue')
},
{
path: 'settings',
name: RoutesStatus.UserSettings,
component: () => import('../pages/user/UserSettings.vue')
}
]
},
{
path: 'admin',
component: () => import('../pages/admin/AdminLayout.vue'),
children: [
{
path: 'dashboard',
name: RoutesStatus.AdminDashboard,
component: () => import('../pages/admin/AdminDashboard.vue')
},
{
path: 'users',
name: RoutesStatus.AdminUsers,
component: () => import('../pages/admin/AdminUsers.vue')
}
]
}
],
{
path: '/:catchAll(.*)',
name: RoutesStatus.NotFound,
component: () => import('../pages/NotFound.vue')
},
{
path: '/tokenFail',
name: RoutesStatus.TokenFail,
component: () => import('../pages/TokenNotFound.vue')
},
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
接下來,我們將實現基於角色的路由權限控制,結合 Day 14 中提到的 RBAC 概念:
(檔案: src/router/index.ts
)
import { PermissionRole } from '@/schemas/auth'
// ... 前面的代碼保持不變
const roleRouteMap: Record<Exclude<PermissionRole, PermissionRole.None>, string[]> = {
[PermissionRole.Admin]: ['AdminDashboard', 'AdminUsers'],
[PermissionRole.Manager]: ['AdminDashboard'],
[PermissionRole.User]: ['UserProfile', 'UserSettings']
}
router.beforeEach(async (to, from, next) => {
const permissionStore = usePermissionStore()
const { userRole } = storeToRefs(permissionStore)
if (userRole.value === PermissionRole.None) {
await permissionStore.fetchUserPermissions()
}
if (to.name && userRole.value !== PermissionRole.None) {
const allowedRoutes = roleRouteMap[userRole.value]
if (!allowedRoutes.includes(to.name as string)) {
return { name: RouteStatus.Home }
}
}
return true;
})
我們可以進一步擴展權限控制,實現基於屬性的訪問控制 (ABAC):
(檔案:src/stores/usePermissionStore.ts
)
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { PermissionRole, PolicySchema } from '@/schemas/auth'
import { usePermissionApi } from '@/composables/usePermissionApi'
export const usePermissionStore = defineStore('permission', () => {
const userRole = ref<PermissionRole>(PermissionRole.None)
const userAttributes = ref<string[]>([])
const policies = ref<PolicySchema[]>([])
const { getFakeUserPermissions, getFakePolicies } = usePermissionApi()
const fetchUserPermissions = async () => {
const result = await getFakeUserPermissions()
userRole.value = result.role
userAttributes.value = result.attributes
}
const fetchPolicies = async () => {
policies.value = await getFakePolicies()
}
const evaluateAccess = (resource: string, action: string) => {
return policies.value.some(policy =>
policy.resource === resource &&
policy.action.includes(action) &&
policy.attributes.every(attr => userAttributes.value.includes(attr))
)
}
return {
userRole,
userAttributes,
policies,
fetchUserPermissions,
fetchPolicies,
evaluateAccess
}
})
然後在路由守衛中使用:
(檔案: src/router/index.ts
)
// ... 前面的代碼保持不變
export interface PartialAuthAttribute {
action: string;
resource: string;
}
const isPartialAuthAttribute = (input: unknown): input is PartialAuthAttribute => {
if (typeof input !=='object' || input === null) return false;
if (!('action' in input) || !('resource' in input)) return false;
if (typeof input['action'] !== 'string') return false;
if (typeof input['resource'] !== 'string') return false;
return true;
};
router.beforeEach(async to => {
const permissionStore = usePermissionStore()
if (permissionStore.userRole === PermissionRole.None) {
await permissionStore.fetchUserPermissions()
await permissionStore.fetchPolicies()
}
if (to.meta.requiredAccess) {
if (!isPartialAuthAttribute(to.meta.requiredAccess)) {
return { name: RoutesStatus.Home };
}
const { resource, action } = to.meta.requiredAccess;
if (!permissionStore.evaluateAccess(resource, action)) {
return { name: RoutesStatus.Home };
}
}
return true;
})
現在,讓我們實現一個使用 UnoCSS 的左側導航欄組件,並在 Pinia store 中記憶當前的 tab:
(檔案: src/components/SideNav.vue
)
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { storeToRefs } from 'pinia'
import { useNavStore } from '@/stores/useNavStore'
import { usePermissionStore } from '@/stores/usePermissionStore'
import { RoutesStatus } from '../router'
const route = useRoute()
const navStore = useNavStore()
const permissionStore = usePermissionStore()
const { currentTab } = storeToRefs(navStore)
const { userRole } = storeToRefs(permissionStore)
const navItems = computed(() => {
const items = [
{ name: 'home', label: 'Home', routeName: RoutesStatus.Home },
{ name: 'profile', label: 'Profile', routeName: RoutesStatus.UserProfile },
{ name: 'settings', label: 'Settings', routeName: RoutesStatus.UserSettings },
]
if (userRole.value === 'admin') {
items.push(
{ name: 'admin-dashboard', label: 'Admin Dashboard', routeName: RoutesStatus.AdminDashboard },
{ name: 'admin-users', label: 'Manage Users', routeName: RoutesStatus.AdminUsers }
)
}
return items
})
const setCurrentTab = (tabName: string) => {
navStore.setCurrentTab(tabName)
}
</script>
<template>
<nav w-64 bg-gray-800 h-screen flex flex-col>
<div p-4>
<h1 text-white text-xl font-bold>My App</h1>
</div>
<ul class="flex-1">
<li v-for="item in navItems" :key="item.name">
<router-link
:to="{ name: item.routeName }"
block px-4 py-2 text-gray-300 bg="hover:gray-700"
:class="{ 'bg-gray-700': currentTab === item.name }"
@click="setCurrentTab(item.name)"
>
{{ item.label }}
</router-link>
</li>
</ul>
</nav>
</template>
(檔案: src/stores/useNavStore.ts
)
import { defineStore } from 'pinia'
import { shallowRef } from 'vue'
export const useNavStore = defineStore('nav', () => {
const currentTab = shallowRef(RoutesStatus.Home)
const setCurrentTab = (tabName: RoutesStatus) => {
currentTab.value = tabName
}
return {
currentTab,
setCurrentTab
}
})
最後,我們可以為路由切換添加過渡效果:
(檔案: src/App.vue
)
<script setup lang="ts">
import SideNav from '@/components/SideNav.vue'
</script>
<template>
<div class="flex">
<SideNav />
<main class="flex-1">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</main>
</div>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
最後,讓我們使用 VueUse 的 createFetch
來處理 API 請求
(檔案:src/composables/useApi.ts
)
import { createFetch } from '@vueuse/core'
import { useAuthStore } from '@/stores/useAuthStore'
export const useApi = () => {
const authStore = useAuthStore()
const apiFetch = createFetch({
baseUrl: import.meta.env.VITE_API_BASE_URL,
options: {
async beforeFetch({ options }) {
const token = authStore.token
if (token) {
options.headers = {
...options.headers,
Authorization: `Bearer ${token}`,
}
}
return { options }
},
},
fetchOptions: {
mode: 'cors',
},
})
return {
apiFetch,
}
}
然後,我們可以在 usePermissionApi
中使用這個 apiFetch
:
(檔案: src/composables/usePermissionApi.ts
)
import { useApi } from './useApi'
import { PermissionRoleSchema, PolicySchema, permissionRoleSchema, policySchema } from '@/schemas/auth'
export const usePermissionApi = () => {
const { apiFetch } = useApi()
const getFakeUserPermissions = async (): Promise<PermissionRoleSchema> => {
const { data, error } = await apiFetch('/api/user-permissions').json()
if (error.value) throw error.value
const result = permissionRoleSchema.parse(data.value)
return result
}
const getFakePolicies = async (): Promise<PolicySchema[]> => {
const { data, error } = await apiFetch('/api/policies').json()
if (error.value) throw error.value
const result = policySchema.array().parse(data.value)
return result
}
return {
getFakeUserPermissions,
getFakePolicies,
}
}
在這篇文章中,我們深入探討了如何使用 Vue Router 實現多級嵌套路由和複雜的導航守衛。我們結合了 Pinia、Zod、VeeValidate 等工具,實現了基於角色(RBAC)和屬性(ABAC)的訪問控制。我們還創建了一個記憶當前 tab 的左側導航欄組件,並為路由切換添加了平滑的過渡效果。
通過這種方法,我們不僅實現了一個功能豐富的路由系統,還確保了代碼的可維護性和可擴展性。使用 TypeScript 和 Zod 進行類型檢查和驗證,我們提高了應用程序的穩定性和可靠性。
最後,我們使用了 VueUse 的 createFetch
函數來處理 API 請求。這種方法與 Vue 3 的組合式 API 完美配合,使得我們的代碼更加簡潔和靈活。
通過這些實踐,我們不僅提高了應用程序的安全性和用戶體驗,還為未來的擴展和維護奠定了堅實的基礎。在實際的項目中,您可能需要根據具體需求進行進一步的調整和優化,但這個框架為您提供了一個強大的