实战案例:实现一个带权限控制的多页应用
Vue Router 4 的动态路由、嵌套路由和守卫功能为构建带权限控制的多页应用提供了强大支持。本节通过一个实战案例,设计并实现一个简单的多页应用,包含用户认证、权限校验和页面导航,展示如何在 Vue 3 项目中整合这些特性,打造一个实用的权限管理系统。
需求分析
我们需要实现以下功能:
- 页面结构:
- 公开页面:首页、登录页。
- 受限页面:仪表盘(包含嵌套子页面:概览、设置)。
- 权限控制:
- 未登录用户只能访问公开页面。
- 已登录用户可访问仪表盘及其子页面。
- 管理员用户可访问额外管理页面。
- 状态管理:
- 使用 Pinia 管理用户认证状态。
- 导航体验:
- 动态导航菜单。
- 路由懒加载优化性能。
项目结构
src/
├── stores/
│ └── auth.js # 认证状态管理
├── router/
│ └── index.js # 路由配置
├── views/
│ ├── Home.vue # 首页
│ ├── Login.vue # 登录页
│ ├── Dashboard.vue # 仪表盘
│ ├── Overview.vue # 概览子页面
│ ├── Settings.vue # 设置子页面
│ └── Admin.vue # 管理页面
├── components/
│ └── Nav.vue # 导航组件
├── App.vue
└── main.js
实现步骤
1. 状态管理
使用 Pinia 管理用户认证:
// src/stores/auth.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useAuthStore = defineStore('auth', () => {
const user = ref(null); // { name: string, role: 'user' | 'admin' }
const login = (credentials) => {
// 模拟登录
const { username, password } = credentials;
if (username && password === '123') {
user.value = { name: username, role: username === 'admin' ? 'admin' : 'user' };
localStorage.setItem('user', JSON.stringify(user.value));
}
};
const logout = () => {
user.value = null;
localStorage.removeItem('user');
};
// 初始化用户状态
const storedUser = localStorage.getItem('user');
if (storedUser) user.value = JSON.parse(storedUser);
return { user, login, logout };
});
2. 路由配置
定义路由并添加权限守卫:
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from '@/stores/auth';
const routes = [
{ path: '/', name: 'Home', component: () => import('@/views/Home.vue') },
{ path: '/login', name: 'Login', component: () => import('@/views/Login.vue') },
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { requiresAuth: true },
children: [
{ path: '', name: 'Overview', component: () => import('@/views/Overview.vue') },
{ path: 'settings', name: 'Settings', component: () => import('@/views/Settings.vue') }
]
},
{
path: '/admin',
name: 'Admin',
component: () => import('@/views/Admin.vue'),
meta: { requiresAuth: true, requiresAdmin: true }
},
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('@/views/NotFound.vue') }
];
const router = createRouter({
history: createWebHistory(),
routes
});
router.beforeEach((to, from, next) => {
const authStore = useAuthStore();
const isAuthenticated = !!authStore.user;
const isAdmin = authStore.user?.role === 'admin';
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login');
} else if (to.meta.requiresAdmin && !isAdmin) {
next('/dashboard');
} else {
next();
}
});
export default router;
3. 导航组件
动态显示导航项:
<!-- src/components/Nav.vue -->
<template>
<nav>
<router-link to="/">首页</router-link>
<router-link v-if="!isAuthenticated" to="/login">登录</router-link>
<template v-else>
<router-link to="/dashboard">仪表盘</router-link>
<router-link to="/dashboard/settings">设置</router-link>
<router-link v-if="isAdmin" to="/admin">管理</router-link>
<button @click="logout">退出</button>
</template>
</nav>
</template>
<script>
import { useAuthStore } from '@/stores/auth';
import { useRouter } from 'vue-router';
import { computed } from 'vue';
export default {
setup() {
const authStore = useAuthStore();
const router = useRouter();
const isAuthenticated = computed(() => !!authStore.user);
const isAdmin = computed(() => authStore.user?.role === 'admin');
const logout = () => {
authStore.logout();
router.push('/login');
};
return { isAuthenticated, isAdmin, logout };
}
};
</script>
<style scoped>
nav { margin: 10px; }
nav a, nav button { margin-right: 10px; }
</style>
4. 页面组件
App.vue
<template>
<div>
<Nav />
<Suspense>
<template #default>
<router-view />
</template>
<template #fallback>
<p>加载中...</p>
</template>
</Suspense>
</div>
</template>
<script>
import Nav from './components/Nav.vue';
export default {
components: { Nav }
};
</script>
Home.vue
<template>
<h1>欢迎来到首页</h1>
</template>
Login.vue
<template>
<div>
<h1>登录</h1>
<input v-model="username" placeholder="用户名" />
<input v-model="password" type="password" placeholder="密码" />
<button @click="login">登录</button>
</div>
</template>
<script>
import { useAuthStore } from '@/stores/auth';
import { useRouter } from 'vue-router';
import { ref } from 'vue';
export default {
setup() {
const authStore = useAuthStore();
const router = useRouter();
const username = ref('');
const password = ref('');
const login = () => {
authStore.login({ username: username.value, password: password.value });
if (authStore.user) router.push('/dashboard');
};
return { username, password, login };
}
};
</script>
Dashboard.vue
<template>
<div>
<h1>仪表盘</h1>
<router-view />
</div>
</template>
Overview.vue
<template>
<p>概览页面 - 欢迎,{{ user.name }}</p>
</template>
<script>
import { useAuthStore } from '@/stores/auth';
export default {
setup() {
const authStore = useAuthStore();
return { user: authStore.user };
}
};
</script>
Settings.vue
<template>
<p>设置页面</p>
</template>
Admin.vue
<template>
<h1>管理面板 - 仅管理员可见</h1>
</template>
NotFound.vue
<template>
<h1>404 - 页面未找到</h1>
</template>
功能亮点
- 权限控制:
- 全局守卫结合
meta字段实现分级权限。
- 全局守卫结合
- 嵌套路由:
- 仪表盘内子页面通过嵌套路由管理。
- 懒加载:
- 所有页面按需加载,提升性能。
- 动态导航:
- 根据用户状态显示菜单项。
- 状态管理:
- Pinia 统一管理认证状态。
测试与验证
- 未登录:
- 访问
/dashboard跳转到/login。 - 导航只显示“首页”和“登录”。
- 访问
- 普通用户登录:
- 输入
alice和123,跳转到/dashboard。 - 可访问
/dashboard/settings,但访问/admin跳转到/dashboard。
- 输入
- 管理员登录:
- 输入
admin和123,可访问所有页面。
- 输入
扩展与优化
1. 添加加载状态
<!-- src/App.vue -->
<template>
<div>
<Nav />
<Suspense>
<template #default>
<router-view />
</template>
<template #fallback>
<div class="loading">加载中...</div>
</template>
</Suspense>
</div>
</template>
<style scoped>
.loading { font-size: 18px; text-align: center; padding: 20px; }
</style>
2. 异步数据预加载
在守卫中加载用户数据:
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore();
if (to.meta.requiresAuth && !authStore.user) {
await authStore.fetchUser(); // 假设异步方法
if (!authStore.user) next('/login');
else next();
} else {
next();
}
});
3. 类型支持
为 Store 添加 TypeScript:
// src/stores/auth.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';
interface User {
name: string;
role: 'user' | 'admin';
}
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null);
const login = (credentials: { username: string; password: string }) => {
// ...
};
return { user, login, logout };
});
注意事项
- 路由重复:
- 使用
router.push().catch(() => {})处理重复导航错误。
- 使用
- 持久化:
- 确保
localStorage与实际认证同步。
- 确保
- 性能:
- 懒加载减少初始加载,守卫逻辑保持简洁。
总结
通过本案例,你实现了一个带权限控制的多页应用,结合 Vue Router 4 的嵌套路由、懒加载和守卫功能,以及 Pinia 的状态管理,完成了从认证到导航的完整流程。这一实践为你提供了构建复杂应用的模板。本章结束,下一章将探索 Vue 3 的新特性,继续提升你的开发能力!
