Vue Router详解


一、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 懒加载路由导致的白屏

问题:首次加载懒加载路由时出现短暂白屏。

解决方案

  • 添加加载状态或骨架屏
  • 预加载关键路由
  • 优化路由组件的大小

二十一、最佳实践

  1. 使用命名路由:提高代码可读性和可维护性
  2. 合理使用路由元信息:集中管理路由相关的配置和权限
  3. 实现路由懒加载:减小初始打包体积,提高加载速度
  4. 使用路由守卫:控制路由访问权限,执行导航相关逻辑
  5. 保持路由配置清晰:合理组织路由结构,使用嵌套路由
  6. 使用 props 传递路由参数:使组件更加解耦和可复用
  7. 添加路由过渡动画:提升用户体验
  8. 处理 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 应用。

参考资料


文章作者: 弈心
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 弈心 !
评论
 上一篇
JavaScript十大设计模式详解 JavaScript十大设计模式详解
深入讲解JavaScript十大设计模式,包括工厂模式、单体模式、模块模式、代理模式、职责链模式、命令模式、模板方法模式、策略模式、发布订阅模式、中介者模式
下一篇 
js中Object.create()用法 js中Object.create()用法
深入讲解JavaScript中Object.create()方法的原理、应用场景、与其他创建对象方法的比较
2026-05-03
  目录