Skip to content
第 126 / 250 章前端⏱ 10 分钟阅读

第 126 章:Vue Router 路由管理

学习目标

  • 掌握 Vue Router 4 的安装与基本配置
  • 理解动态路由、嵌套路由、命名路由
  • 学会路由守卫、懒加载、过渡动画
  • 能在项目中用 TypeScript 集成路由

一、为什么需要 Vue Router

SPA(单页应用)必须在前端实现路由切换,不能依赖浏览器跳转。Vue Router 是 Vue 官方路由管理器。

二、安装与基本配置

2.1 安装

bash
# npm
npm install vue-router@4

# pnpm
pnpm add vue-router@4

2.2 创建路由表

typescript
// src/router/index.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/about',
    name: 'about',
    component: () => import('@/views/About.vue')
  },
  {
    // 404
    path: '/:pathMatch(.*)*',
    name: 'not-found',
    component: () => import('@/views/NotFound.vue')
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

export default router;

2.3 main.ts 注入

typescript
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

const app = createApp(App);
app.use(router);
app.mount('#app');

2.4 路由出口

vue
<!-- App.vue -->
<template>
  <router-view />
</template>

三、路由模式

3.1 HTML5 History

typescript
// 推荐:URL 不含 #,需要服务器配合
createWebHistory();
createWebHistory(import.meta.env.BASE_URL);
nginx
# Nginx 配置示例
location / {
  try_files $uri $uri/ /index.html;
}

3.2 Hash 模式

typescript
// URL 含 #,无需服务器配置
createWebHashHistory();
text
https://example.com/#/about

3.3 Memory 模式

typescript
// SSR 或测试环境
createMemoryHistory();

四、动态路由

4.1 路径参数

typescript
{
  path: '/users/:id',
  component: () => import('@/views/UserDetail.vue')
}
vue
<!-- 跳转 -->
<router-link :to="`/users/${userId}`">用户详情</router-link>

<!-- 组件中获取 -->
<script setup>
import { useRoute } from 'vue-router';
const route = useRoute();
console.log(route.params.id);  // 字符串
</script>

4.2 多参数

typescript
// 路由配置
{ path: '/search/:category/:keyword', component: Search }

// 跳转
router.push('/search/books/vue');

// 组件
const { category, keyword } = route.params;

4.3 可选参数

typescript
// ?表示可选
{ path: '/posts/:id?', component: PostList }

// 匹配 /posts 和 /posts/123

4.4 通配符

typescript
{
  // 任意路径
  path: '/:pathMatch(.*)*',
  component: NotFound
}

// 路由参数
route.params.pathMatch;  // 'foo/bar'

五、嵌套路由

5.1 配置

typescript
{
  path: '/admin',
  component: () => import('@/layouts/Admin.vue'),
  children: [
    {
      path: '',
      component: () => import('@/views/admin/Dashboard.vue')
    },
    {
      path: 'users',
      component: () => import('@/views/admin/Users.vue')
    },
    {
      path: 'settings',
      component: () => import('@/views/admin/Settings.vue')
    }
  ]
}

5.2 父布局

vue
<!-- Admin.vue -->
<template>
  <div class="admin">
    <aside>
      <router-link to="/admin">仪表盘</router-link>
      <router-link to="/admin/users">用户</router-link>
      <router-link to="/admin/settings">设置</router-link>
    </aside>
    <main>
      <!-- 子路由出口 -->
      <router-view />
    </main>
  </div>
</template>

六、命名路由与编程式导航

6.1 命名路由

typescript
{
  path: '/user/:id',
  name: 'user',
  component: () => import('@/views/User.vue')
}

6.2 命名跳转

vue
<router-link :to="{ name: 'user', params: { id: 123 } }">用户</router-link>
typescript
router.push({ name: 'user', params: { id: 123 } });
router.push({ path: '/user/123' });

6.3 编程式导航

typescript
import { useRouter } from 'vue-router';

const router = useRouter();

// 跳转
router.push('/about');
router.push({ name: 'about' });

// 替换(不留历史)
router.replace('/login');

// 后退/前进
router.back();
router.forward();
router.go(-2);  // 后退两步

6.4 命名视图

typescript
{
  path: '/',
  components: {
    default: Home,
    sidebar: Sidebar,
    header: Header
  }
}
vue
<router-view />
<router-view name="sidebar" />
<router-view name="header" />

七、路由传参

7.1 Query 参数

typescript
// 跳转
router.push({ path: '/search', query: { q: 'vue', page: 2 } });

// 读取
const { q, page } = route.query;

7.2 Params

typescript
// 跳转
router.push({ name: 'user', params: { id: 123 } });

// 读取
const { id } = route.params;

7.3 状态(state)

typescript
router.push({
  path: '/result',
  state: { from: 'home' }
});

// 读取
console.log(history.state.from);

八、路由守卫

8.1 全局前置守卫

typescript
router.beforeEach((to, from) => {
  // 必须返回 false / 路由地址 / true
  if (to.meta.requiresAuth && !isLoggedIn()) {
    return { name: 'login', query: { redirect: to.fullPath } };
  }
  return true;
});

8.2 全局后置守卫

typescript
router.afterEach((to, from) => {
  // 常用于埋点、滚动
  document.title = (to.meta.title as string) || 'Default';
  window.scrollTo(0, 0);
});

8.3 路由独享守卫

typescript
{
  path: '/admin',
  component: Admin,
  beforeEnter: (to, from) => {
    if (!isAdmin()) return false;
  }
}

8.4 组件内守卫

vue
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router';

onBeforeRouteUpdate((to, from) => {
  // 当前路由变化但组件复用
  console.log('路由更新');
});

onBeforeRouteLeave((to, from) => {
  // 离开前
  if (hasUnsavedChanges) {
    return confirm('确定离开?');
  }
});
</script>

8.5 组合式封装

typescript
// composables/useAuthGuard.ts
export function useAuthGuard() {
  const router = useRouter();

  function requireAuth() {
    if (!isLoggedIn()) {
      router.push({ name: 'login', query: { redirect: router.currentRoute.value.fullPath } });
      return false;
    }
    return true;
  }

  return { requireAuth };
}

九、懒加载

9.1 动态导入

typescript
{
  path: '/dashboard',
  component: () => import('@/views/Dashboard.vue')
}

9.2 分组打包

typescript
// 同一组的路由打包到一个 chunk
const Admin = () => import(/* webpackChunkName: "admin" */ '@/views/Admin.vue');
const Users = () => import(/* webpackChunkName: "admin" */ '@/views/Users.vue');

9.3 Vite 配置

typescript
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia']
        }
      }
    }
  }
});

十、过渡动画

10.1 路由过渡

vue
<template>
  <router-view v-slot="{ Component }">
    <transition name="fade" mode="out-in">
      <component :is="Component" />
    </transition>
  </router-view>
</template>

<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

10.2 滚动行为

typescript
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 };
  }
});

十一、TypeScript 增强

11.1 路由元信息类型

typescript
// types/router.d.ts
import 'vue-router';

declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean;
    title?: string;
    roles?: string[];
  }
}

11.2 路由参数类型

typescript
// typed-router.ts
import 'vue-router';

declare module 'vue-router' {
  interface RouteParamValue {}
  interface RouteParams {
    id?: string;
    slug?: string;
  }
}

11.3 路由对象

typescript
const route = useRoute();
// route.params.id 自动推断为 string | undefined

十二、错误处理

12.1 解析错误

typescript
router.onError((error) => {
  if (error.message.includes('Failed to fetch')) {
    console.log('路由 chunk 加载失败');
  }
});

12.2 重试机制

typescript
component: () => import('@/views/Dashboard.vue').catch(() => import('@/views/Error.vue'))

十三、本章小结

概念关键
路由模式web(hash 含 #)/ history(HTML5)
动态路由:id 路径参数
嵌套路由children + 二级 router-view
编程式导航router.push / replace / go
路由守卫beforeEach / beforeEnter / onBeforeRouteLeave
懒加载动态 import, 按需打包
路由元meta 字段,扩展类型

动手练习

  1. 用户路由:实现 /users/:id 详情页,展示用户信息
  2. 嵌套路由:实现后台管理布局,包含侧边栏和子路由
  3. 路由守卫:实现登录拦截,未登录跳转登录页
  4. 懒加载:将所有页面改为动态 import
  5. 滚动行为:实现路由切换到顶部与 hash 锚点

推荐阅读


下一章第 127 章:Pinia 状态管理

本站基于 VitePress 构建 · 由 Codebook 团队维护