一、Vue Router 简介
Vue Router 是 Vue.js 的官方路由管理器,它与 Vue.js 核心深度集成,让构建单页应用变得更加简单。Vue Router 4 是为 Vue 3 设计的最新版本,提供了更强大的功能和更好的性能。
二、安装与基本配置
2.1 安装
# 使用 npm
npm install vue-router@4
# 使用 yarn
yarn add vue-router@4
2.2 基本配置
在 Vue 3 项目中,通常在 src/router/index.js 文件中配置路由:
import { createRouter, createWebHistory } from "vue-router";
const routes = [
{
path: "/",
name: "Home",
component: () => import("../views/Home.vue"),
},
{
path: "/about",
name: "About",
component: () => import("../views/About.vue"),
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
export default router;
然后在 main.js 中注册路由:
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
const app = createApp(App);
app.use(router);
app.mount("#app");
三、路由的基本用法
3.1 声明式导航
使用 <router-link> 组件进行导航:
<template>
<div>
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>
<router-view></router-view>
</div>
</template>
3.2 编程式导航
使用 router.push() 方法进行编程式导航:
// 字符串路径
router.push("/users/eduardo");
// 对象
router.push({ path: "/users/eduardo" });
// 命名路由
router.push({ name: "user", params: { username: "eduardo" } });
// 带查询参数
router.push({ path: "/register", query: { plan: "private" } });
四、动态路由
动态路由允许我们匹配具有相同结构但不同参数的路由:
const routes = [
// 动态段以冒号开头
{
path: "/user/:id",
component: User,
props: true, // 将路由参数作为 props 传递给组件
},
];
在组件中访问路由参数:
<template>
<div>User {{ id }}</div>
</template>
<script setup>
import { useRoute } from "vue-router";
const route = useRoute();
const id = route.params.id;
</script>
五、嵌套路由
嵌套路由允许我们在父路由中定义子路由:
const routes = [
{
path: "/user/:id",
component: User,
children: [
{
// 当 /user/:id/profile 匹配成功
// UserProfile 会被渲染在 User 的 <router-view> 中
path: "profile",
component: UserProfile,
},
{
// 当 /user/:id/posts 匹配成功
// UserPosts 会被渲染在 User 的 <router-view> 中
path: "posts",
component: UserPosts,
},
],
},
];
六、路由懒加载
路由懒加载可以减小初始打包体积,提高应用加载速度:
const routes = [
{
path: "/",
name: "Home",
component: () => import("../views/Home.vue"),
},
{
path: "/about",
name: "About",
component: () => import("../views/About.vue"),
},
];
6.1 按组懒加载
const routes = [
{
path: "/user",
component: () => import("../views/UserLayout.vue"),
children: [
{
path: "profile",
component: () => import("../views/user/Profile.vue"),
},
{
path: "settings",
component: () => import("../views/user/Settings.vue"),
},
],
},
];
6.2 路由懒加载分组
// 将相关的路由分组到同一个 chunk
const UserRoutes = () => import(/* webpackChunkName: "user" */ "../views/user/Index.vue");
const AdminRoutes = () => import(/* webpackChunkName: "admin" */ "../views/admin/Index.vue");
const routes = [
{
path: "/user",
component: UserRoutes,
children: [
{ path: "profile", component: () => import("../views/user/Profile.vue") },
{ path: "settings", component: () => import("../views/user/Settings.vue") },
],
},
{
path: "/admin",
component: AdminRoutes,
children: [
{ path: "dashboard", component: () => import("../views/admin/Dashboard.vue") },
{ path: "users", component: () => import("../views/admin/Users.vue") },
],
},
];
七、路由导航守卫
路由导航守卫用于控制路由的访问权限和执行导航相关的逻辑。
7.1 全局前置守卫
router.beforeEach((to, from, next) => {
// 检查用户是否登录
const isLoggedIn = localStorage.getItem("token");
if (to.meta.requiresAuth && !isLoggedIn) {
// 未登录,重定向到登录页
next("/login");
} else {
// 已登录,继续导航
next();
}
});
7.2 全局后置钩子
router.afterEach((to, from) => {
// 可以在这里添加页面标题设置、统计分析等
document.title = to.meta.title || "默认标题";
});
7.3 路由独享守卫
const routes = [
{
path: "/admin",
component: Admin,
beforeEnter: (to, from, next) => {
// 检查是否为管理员
const isAdmin = localStorage.getItem("role") === "admin";
if (!isAdmin) {
next("/");
} else {
next();
}
},
},
];
7.4 组件内守卫
<script setup>
import { onBeforeRouteEnter, onBeforeRouteUpdate, onBeforeRouteLeave } from "vue-router";
// 进入路由前
onBeforeRouteEnter((to, from, next) => {
// 这里不能访问组件实例,因为组件还未创建
next(vm => {
// 这里可以访问组件实例 vm
});
});
// 路由更新时(参数变化)
onBeforeRouteUpdate((to, from, next) => {
// 这里可以访问组件实例
console.log("路由参数已更新");
next();
});
// 离开路由前
onBeforeRouteLeave((to, from, next) => {
// 可以在这里询问用户是否确认离开
const confirmLeave = window.confirm("确定要离开吗?");
if (confirmLeave) {
next();
} else {
next(false);
}
});
</script>
八、路由元信息
路由元信息允许我们为路由添加额外的自定义数据:
const routes = [
{
path: "/admin",
component: Admin,
meta: {
requiresAuth: true,
title: "管理后台",
roles: ["admin"],
},
},
];
在导航守卫中使用元信息:
router.beforeEach((to, from, next) => {
// 设置页面标题
document.title = to.meta.title || "默认标题";
// 检查权限
if (to.meta.requiresAuth) {
const userRole = localStorage.getItem("role");
if (!userRole || !to.meta.roles.includes(userRole)) {
next("/");
} else {
next();
}
} else {
next();
}
});
九、路由权限管理
9.1 基于角色的权限控制
// 权限管理工具
const permissionUtils = {
// 检查用户是否有权限访问路由
hasPermission(userRoles, routeRoles) {
if (!routeRoles) return true;
return userRoles.some(role => routeRoles.includes(role));
},
// 过滤路由表
filterRoutes(routes, userRoles) {
return routes.filter(route => {
if (this.hasPermission(userRoles, route.meta?.roles)) {
if (route.children) {
route.children = this.filterRoutes(route.children, userRoles);
}
return true;
}
return false;
});
},
};
// 动态添加路由
const addRoutes = userRoles => {
const accessibleRoutes = permissionUtils.filterRoutes(routes, userRoles);
accessibleRoutes.forEach(route => {
router.addRoute(route);
});
};
9.2 完整的权限守卫实现
// router/guards.js
export function setupPermissionGuard(router) {
router.beforeEach(async (to, from, next) => {
// 检查是否需要权限
if (!to.meta?.requiresAuth) {
return next();
}
// 获取用户信息
const token = localStorage.getItem("token");
if (!token) {
return next({ name: "Login", query: { redirect: to.fullPath } });
}
// 检查角色权限
const userRoles = JSON.parse(localStorage.getItem("roles") || "[]");
if (to.meta?.roles && !permissionUtils.hasPermission(userRoles, to.meta.roles)) {
return next({ name: "Forbidden" });
}
next();
});
}
十、与组合式 API 的配合
在 Vue 3 的组合式 API 中使用路由:
<template>
<div>
<h1>用户详情</h1>
<p>用户 ID: {{ id }}</p>
<button @click="goBack">返回</button>
</div>
</template>
<script setup>
import { useRoute, useRouter } from "vue-router";
const route = useRoute();
const router = useRouter();
const id = route.params.id;
const goBack = () => {
router.back();
};
</script>
十一、路由状态管理
11.1 路由参数的响应式
在组合式 API 中,route.params 是响应式的,当路由参数变化时,组件会自动更新:
<template>
<div>
<h1>用户详情</h1>
<p>用户 ID: {{ id }}</p>
</div>
</template>
<script setup>
import { useRoute, watch, computed } from "vue";
const route = useRoute();
const id = computed(() => route.params.id);
// 监听路由参数变化
watch(
() => route.params.id,
(newId, oldId) => {
console.log(`ID 从 ${oldId} 变为 ${newId}`);
// 可以在这里执行数据重新加载等操作
}
);
</script>
十二、高级配置
12.1 滚动行为
配置路由切换时的滚动行为:
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
// 如果有保存的位置,恢复它
if (savedPosition) {
return savedPosition;
} else {
// 否则滚动到顶部
return { top: 0 };
}
},
});
12.2 滚动行为优化
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
// 如果有保存的位置,恢复它
if (savedPosition) {
return savedPosition;
}
// 如果是锚点导航
if (to.hash) {
return {
el: to.hash,
behavior: "smooth",
};
}
// 否则滚动到顶部
return { top: 0, behavior: "smooth" };
},
});
12.3 路由别名
为路由设置别名:
const routes = [
{
path: "/",
component: Home,
alias: ["/home", "/index"],
},
];
12.4 路由重定向
设置路由重定向:
const routes = [
{
path: "/",
redirect: "/home",
},
{
path: "/home",
component: Home,
},
];
十三、路由过渡动画
使用 Vue 的 <transition> 组件为路由添加过渡效果:
<template>
<div>
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>
<transition name="fade" mode="out-in">
<router-view></router-view>
</transition>
</div>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
十四、路由缓存
使用 keep-alive 实现路由组件缓存:
<template>
<div>
<router-link to="/home">首页</router-link>
<router-link to="/profile">个人资料</router-link>
<keep-alive :include="cachedViews">
<router-view></router-view>
</keep-alive>
</div>
</template>
<script setup>
import { ref, watch } from "vue";
import { useRoute } from "vue-router";
const route = useRoute();
const cachedViews = ref(["Home", "Profile"]);
// 监听路由变化,动态管理缓存
watch(
() => route.name,
newName => {
if (newName && !cachedViews.value.includes(newName)) {
cachedViews.value.push(newName);
}
}
);
</script>
十五、路由预加载
路由预加载可以提升用户体验,提前加载可能会访问的路由:
// 预加载指定路由
router.preloadRoute("/about");
// 预加载所有路由
const loadRoutes = async () => {
const routeComponents = routes.map(route => route.component);
for (const component of routeComponents) {
if (typeof component === "function") {
await component();
}
}
};
十六、路由错误处理
16.1 404 路由配置
const routes = [
// 其他路由...
{
path: "/:pathMatch(.*)*",
name: "NotFound",
component: () => import("../views/NotFound.vue"),
meta: {
title: "页面不存在",
},
},
];
16.2 全局错误处理
// 路由导航错误处理
router.onError(error => {
console.error("路由错误:", error);
// 可以在这里添加错误监控
if (error.name === "ChunkLoadError") {
// 处理懒加载失败
window.location.reload();
}
});
// 导航取消处理
router.isReady().catch(error => {
if (error.name === "NavigationDuplicated") {
// 忽略重复导航错误
return;
}
console.error("路由就绪失败:", error);
});
16.3 自定义错误组件
<template>
<div class="error-page">
<h1>404</h1>
<p>抱歉,您访问的页面不存在</p>
<router-link to="/">返回首页</router-link>
</div>
</template>
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
const goHome = () => {
router.push("/");
};
</script>
<style scoped>
.error-page {
text-align: center;
padding: 100px 20px;
}
.error-page h1 {
font-size: 120px;
color: #409eff;
margin: 0;
}
.error-page p {
font-size: 18px;
color: #666;
margin: 20px 0;
}
</style>
十七、路由性能优化
17.1 避免过度嵌套
// 不推荐:过度嵌套
const routes = [
{
path: "/a",
component: LayoutA,
children: [
{
path: "b",
component: LayoutB,
children: [
{
path: "c",
component: LayoutC,
children: [{ path: "d", component: PageD }],
},
],
},
],
},
];
// 推荐:扁平化路由
const routes = [
{
path: "/a/b/c/d",
component: PageD,
},
];
17.2 合理使用懒加载
// 推荐:按需加载相关路由
const UserRoutes = () => import(/* webpackChunkName: "user" */ "../views/user/Index.vue");
// 不推荐:所有路由都使用懒加载(影响初始加载性能的关键路由)
const HomeRoutes = () => import("../views/Home.vue");
十八、SSR 中的路由处理
Vue Router 在服务端渲染中的使用:
// server.js
import { createSSRApp } from "vue";
import { createMemoryHistory, createRouter } from "vue-router";
import App from "./App.vue";
import routes from "./router";
export function createApp() {
const history = createMemoryHistory();
const router = createRouter({
history,
routes,
});
const app = createSSRApp(App);
app.use(router);
return { app, router };
}
// 在服务端处理路由
export async function render(url) {
const { app, router } = createApp();
await router.push(url);
await router.isReady();
const context = {};
const html = await renderToString(app, context);
return { html, context };
}
18.1 客户端激活
// client.js
import { createApp } from "vue";
import { createWebHistory, createRouter } from "vue-router";
import App from "./App.vue";
import routes from "./router";
const router = createRouter({
history: createWebHistory(),
routes,
});
const app = createApp(App);
app.use(router);
router.isReady().then(() => {
app.mount("#app");
});
十九、与状态管理库的集成
19.1 与 Pinia 集成
// store/router.js
import { defineStore } from "pinia";
export const useRouterStore = defineStore("router", {
state: () => ({
currentRoute: null,
breadcrumbs: [],
history: [],
}),
actions: {
updateCurrentRoute(route) {
this.currentRoute = route;
this.updateBreadcrumbs(route);
this.addToHistory(route);
},
updateBreadcrumbs(route) {
const breadcrumbs = [];
let matched = route.matched;
matched.forEach((record, index) => {
breadcrumbs.push({
name: record.meta.title || record.name,
path: matched
.slice(0, index + 1)
.map(r => r.path)
.join("/"),
});
});
this.breadcrumbs = breadcrumbs;
},
addToHistory(route) {
if (this.history.length > 10) {
this.history.shift();
}
this.history.push({
path: route.fullPath,
name: route.name,
timestamp: Date.now(),
});
},
},
});
// 在路由守卫中使用
router.beforeEach((to, from, next) => {
const routerStore = useRouterStore();
routerStore.updateCurrentRoute(to);
next();
});
19.2 在组件中使用
<template>
<div class="breadcrumb">
<span v-for="(item, index) in breadcrumbs" :key="index">
<router-link v-if="index < breadcrumbs.length - 1" :to="item.path">
{{ item.name }}
</router-link>
<span v-else>{{ item.name }}</span>
<span v-if="index < breadcrumbs.length - 1">/</span>
</span>
</div>
</template>
<script setup>
import { useRouterStore } from "@/stores/router";
const routerStore = useRouterStore();
const breadcrumbs = computed(() => routerStore.breadcrumbs);
</script>
二十、常见问题与解决方案
20.1 路由参数变化时组件不更新
问题:当路由参数变化时(如从 /user/1 到 /user/2),组件不会重新渲染。
解决方案:
- 使用
watch监听路由参数变化 - 使用
beforeRouteUpdate守卫 - 在组件中使用
key属性
20.2 路由导航守卫中的无限循环
问题:在导航守卫中错误的 next() 调用导致无限循环。
解决方案:
- 确保在所有分支中都调用
next() - 避免在守卫中再次导航到相同的路由
20.3 懒加载路由导致的白屏
问题:首次加载懒加载路由时出现短暂白屏。
解决方案:
- 添加加载状态或骨架屏
- 预加载关键路由
- 优化路由组件的大小
二十一、最佳实践
- 使用命名路由:提高代码可读性和可维护性
- 合理使用路由元信息:集中管理路由相关的配置和权限
- 实现路由懒加载:减小初始打包体积,提高加载速度
- 使用路由守卫:控制路由访问权限,执行导航相关逻辑
- 保持路由配置清晰:合理组织路由结构,使用嵌套路由
- 使用 props 传递路由参数:使组件更加解耦和可复用
- 添加路由过渡动画:提升用户体验
- 处理 404 页面:为不存在的路由提供友好的错误页面
二十二、路由测试
22.1 使用 Vitest 测试路由
// tests/router.test.js
import { describe, it, expect } from "vitest";
import { createRouter, createMemoryHistory } from "vue-router";
import { mount } from "@vue/test-utils";
import Home from "@/views/Home.vue";
describe("路由测试", () => {
it("路由导航到首页", async () => {
const router = createRouter({
history: createMemoryHistory(),
routes: [{ path: "/", name: "Home", component: Home }],
});
const wrapper = mount(
{
template: "<router-view></router-view>",
},
{
global: {
plugins: [router],
},
}
);
await router.push("/");
expect(router.currentRoute.value.name).toBe("Home");
});
it("动态路由参数", async () => {
const router = createRouter({
history: createMemoryHistory(),
routes: [{ path: "/user/:id", name: "User", component: Home }],
});
await router.push({ name: "User", params: { id: "123" } });
expect(router.currentRoute.value.params.id).toBe("123");
});
});
22.2 测试路由守卫
// tests/guards.test.js
import { describe, it, expect, vi } from "vitest";
import { createRouter, createMemoryHistory } from "vue-router";
import { setupPermissionGuard } from "@/router/guards";
describe("路由守卫测试", () => {
it("未登录用户重定向到登录页", async () => {
const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: "/", name: "Home", component: {} },
{ path: "/login", name: "Login", component: {} },
{ path: "/admin", name: "Admin", component: {}, meta: { requiresAuth: true } },
],
});
setupPermissionGuard(router);
await router.push("/admin");
expect(router.currentRoute.value.name).toBe("Login");
});
});
二十三、国际化路由
23.1 多语言路由配置
// router/index.js
import { createRouter, createWebHistory } from "vue-router";
import { createI18n } from "vue-i18n";
import messages from "@/locales";
const i18n = createI18n({
legacy: false,
locale: "zh",
fallbackLocale: "en",
messages,
});
const routes = [
{
path: "/:lang",
component: () => import("@/layouts/LanguageLayout.vue"),
children: [
{ path: "", name: "Home", component: () => import("@/views/Home.vue") },
{ path: "about", name: "About", component: () => import("@/views/About.vue") },
],
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
// 路由守卫:设置语言
router.beforeEach((to, from, next) => {
const lang = to.params.lang || "zh";
if (["zh", "en"].includes(lang)) {
i18n.global.locale.value = lang;
}
next();
});
export { router, i18n };
23.2 语言切换组件
<template>
<div class="language-switcher">
<button
v-for="lang in languages"
:key="lang.value"
@click="changeLanguage(lang.value)"
:class="{ active: currentLang === lang.value }"
>
{{ lang.label }}
</button>
</div>
</template>
<script setup>
import { ref, computed } from "vue";
import { useRouter, useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
const router = useRouter();
const route = useRoute();
const { locale } = useI18n();
const languages = [
{ value: "zh", label: "中文" },
{ value: "en", label: "English" },
];
const currentLang = computed(() => locale.value);
const changeLanguage = lang => {
const currentPath = route.fullPath;
const newPath = currentPath.replace(/^\/[a-z]{2}/, `/${lang}`);
router.push(newPath);
};
</script>
<style scoped>
.language-switcher button {
padding: 8px 16px;
margin: 0 4px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
}
.language-switcher button.active {
background: #409eff;
color: white;
border-color: #409eff;
}
</style>
二十四、移动端路由处理
24.1 底部导航栏
<template>
<div class="mobile-layout">
<div class="content">
<router-view></router-view>
</div>
<div class="bottom-nav">
<router-link
v-for="item in navItems"
:key="item.path"
:to="item.path"
class="nav-item"
active-class="active"
>
<span class="icon">{{ item.icon }}</span>
<span class="text">{{ item.label }}</span>
</router-link>
</div>
</div>
</template>
<script setup>
const navItems = [
{ path: "/", label: "首页", icon: "🏠" },
{ path: "/discover", label: "发现", icon: "🔍" },
{ path: "/profile", label: "我的", icon: "👤" },
];
</script>
<style scoped>
.mobile-layout {
display: flex;
flex-direction: column;
height: 100vh;
}
.content {
flex: 1;
overflow-y: auto;
}
.bottom-nav {
display: flex;
justify-content: space-around;
padding: 10px 0;
background: white;
border-top: 1px solid #eee;
}
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
text-decoration: none;
color: #999;
font-size: 12px;
}
.nav-item.active {
color: #409eff;
}
.nav-item .icon {
font-size: 24px;
margin-bottom: 4px;
}
</style>
24.2 手势导航
<template>
<div class="gesture-container" @touchstart="handleTouchStart" @touchend="handleTouchEnd">
<router-view></router-view>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
const touchStartX = ref(0);
const touchEndX = ref(0);
const handleTouchStart = e => {
touchStartX.value = e.touches[0].clientX;
};
const handleTouchEnd = e => {
touchEndX.value = e.changedTouches[0].clientX;
handleSwipe();
};
const handleSwipe = () => {
const diff = touchStartX.value - touchEndX.value;
// 向左滑动,返回上一页
if (diff > 50) {
router.back();
}
// 向右滑动,前进
else if (diff < -50) {
router.forward();
}
};
</script>
<style scoped>
.gesture-container {
height: 100%;
overflow: hidden;
}
</style>
24.3 移动端转场动画
<template>
<div class="app-container">
<router-view v-slot="{ Component, route }">
<transition :name="transitionName">
<component :is="Component" :key="route.path" />
</transition>
</router-view>
</div>
</template>
<script setup>
import { ref, watch } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
const transitionName = ref("slide-left");
watch(
() => router.currentRoute.value,
(to, from) => {
if (to.meta.index > from.meta.index) {
transitionName.value = "slide-left";
} else {
transitionName.value = "slide-right";
}
}
);
</script>
<style>
.slide-left-enter-active,
.slide-right-enter-active {
transition: all 0.3s ease;
}
.slide-left-enter-from {
transform: translateX(100%);
}
.slide-right-enter-from {
transform: translateX(-100%);
}
.slide-left-leave-to {
transform: translateX(-100%);
}
.slide-right-leave-to {
transform: translateX(100%);
}
</style>
二十五、完整的路由系统
25.1 项目结构
src/
├── router/
│ ├── index.js # 路由配置
│ ├── guards.js # 路由守卫
│ └── modules/ # 路由模块
│ ├── user.js
│ ├── admin.js
│ └── common.js
├── views/
│ ├── Home.vue
│ ├── Login.vue
│ ├── NotFound.vue
│ └── user/
│ ├── Profile.vue
│ └── Settings.vue
└── layouts/
├── DefaultLayout.vue
└── AuthLayout.vue
25.2 路由模块化配置
// router/modules/user.js
export default {
path: '/user',
component: () => import('@/layouts/DefaultLayout.vue'),
meta: { requiresAuth: true },
children: [
{
path: 'profile',
name: 'UserProfile',
component: () => import('@/views/user/Profile.vue'),
meta: { title: '个人资料', index: 1 }
},
{
path: 'settings',
name: 'UserSettings',
component: () => import('@/views/user/Settings.vue'),
meta: { title: '设置', index: 2 }
}
]
}
// router/modules/admin.js
export default {
path: '/admin',
component: () => import('@/layouts/DefaultLayout.vue'),
meta: { requiresAuth: true, roles: ['admin'] },
children: [
{
path: 'dashboard',
name: 'AdminDashboard',
component: () => import('@/views/admin/Dashboard.vue'),
meta: { title: '管理后台', index: 1 }
},
{
path: 'users',
name: 'AdminUsers',
component: () => import('@/views/admin/Users.vue'),
meta: { title: '用户管理', index: 2 }
}
]
}
// router/modules/common.js
export default [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: { title: '首页', index: 0 }
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { title: '登录' }
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue'),
meta: { title: '页面不存在' }
}
]
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import userRoutes from './modules/user'
import adminRoutes from './modules/admin'
import commonRoutes from './modules/common'
import { setupPermissionGuard } from './guards'
const routes = [
...commonRoutes,
userRoutes,
adminRoutes
]
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
}
return { top: 0 }
}
})
setupPermissionGuard(router)
export default router
25.3 完整的权限守卫
// router/guards.js
import { setupPermissionGuard, setupProgressGuard, setupErrorGuard } from "./guards";
export function setupRouterGuards(router) {
setupPermissionGuard(router);
setupProgressGuard(router);
setupErrorGuard(router);
}
// 权限守卫
export function setupPermissionGuard(router) {
router.beforeEach(async (to, from, next) => {
// 设置页面标题
document.title = to.meta.title || "Vue App";
// 检查是否需要权限
if (!to.meta?.requiresAuth) {
return next();
}
// 获取用户信息
const token = localStorage.getItem("token");
if (!token) {
return next({
name: "Login",
query: { redirect: to.fullPath },
});
}
// 检查角色权限
const userRoles = JSON.parse(localStorage.getItem("roles") || "[]");
if (to.meta?.roles && !hasPermission(userRoles, to.meta.roles)) {
return next({ name: "Forbidden" });
}
next();
});
}
// 进度条守卫
export function setupProgressGuard(router) {
router.beforeEach((to, from, next) => {
NProgress.start();
next();
});
router.afterEach(() => {
NProgress.done();
});
}
// 错误守卫
export function setupErrorGuard(router) {
router.onError(error => {
console.error("路由错误:", error);
if (error.name === "ChunkLoadError") {
window.location.reload();
}
});
}
function hasPermission(userRoles, requiredRoles) {
if (!requiredRoles) return true;
return userRoles.some(role => requiredRoles.includes(role));
}
二十六、总结
Vue Router 4 是 Vue 3 生态系统中不可或缺的一部分,它提供了强大而灵活的路由管理功能。通过合理使用 Vue Router,我们可以构建出结构清晰、用户体验良好的单页应用。
本文介绍了 Vue Router 4 的核心功能,包括基本配置、动态路由、嵌套路由、导航守卫、路由元信息、路由懒加载等。希望这些内容能帮助你更好地理解和使用 Vue Router,构建出更加优秀的 Vue 应用。