路径的艺术:Vue Router 完全指南

第 1 节 概述

到目前为止,我们一直在单页面上做文章。

但真实的应用有多个页面:首页、列表页、详情页、个人中心……

用户点击导航栏,URL 变化,页面内容切换——这就是路由要做的事。

Vue 3 的官方路由库是 Vue Router。它的核心思路很简单:URL 映射到组件

1
2
3
/home  → Home.vue
/news  → News.vue
/about → About.vue

第 2 节 安装与配置

如果使用 npm create vue@latest 创建项目时勾选了 Router,它会自动配置好。

手动安装:

1
npm install vue-router

然后创建路由配置文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/pages/Home.vue'
import News from '@/pages/News.vue'

const routes = [
  { path: '/home', component: Home },
  { path: '/news', component: News },
]

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

export default router

main.ts 中注册:

1
2
3
4
5
6
7
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

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

第 3 节 页面跳转

Vue Router 提供了两个全局组件:

  • RouterLink:替代 <a> 标签,点击时不刷新页面
  • RouterView:当前路由对应的组件渲染在这个位置
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!-- App.vue -->
<template>
  <nav>
    <router-link to="/home">首页</router-link>
    <router-link to="/news">新闻</router-link>
    <router-link to="/about">关于</router-link>
  </nav>

  <main>
    <router-view />
  </main>
</template>

点击 router-link 时,Vue Router 会拦截点击,更新 URL,并把对应的组件渲染到 router-view 中。

说明

router-linkrouter-view 在模板中也可以写成 kebab-case:<router-link><router-view>

3.2 路由配置

/src/pages/ 下创建三个页面组件:

1
2
3
4
5
<!-- Home.vue -->
<template>
  <h1>首页</h1>
  <p>欢迎来到首页</p>
</template>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/pages/Home.vue'
import News from '@/pages/News.vue'
import About from '@/pages/About.vue'

const routes = [
  {
    path: '/',
    redirect: '/home',
  },
  {
    path: '/home',
    component: Home,
  },
  {
    path: '/news',
    component: News,
  },
  {
    path: '/about',
    component: About,
  },
]

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

export default router

redirect 表示访问 / 时自动跳转到 /home

第 4 节 命名路由

路由可以添加 name 属性,用名称代替路径:

1
2
3
4
5
6
7
const routes = [
  {
    path: '/home',
    name: 'home',
    component: Home,
  },
]

模板中使用:

1
<router-link :to="{ name: 'home' }">首页</router-link>

用名称的好处:如果路径变了,只需改一处(路由配置),不需要改所有模板。

第 5 节 动态路由匹配

5.1 路由参数

新闻详情页的 URL 可能是 /news/1/news/2,其中 12 是动态变化的。

:id 来匹配动态段:

1
2
3
4
5
6
7
const routes = [
  {
    path: '/news/:id',
    name: 'news-detail',
    component: NewsDetail,
  },
]

在组件中获取参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!-- NewsDetail.vue -->
<script setup lang="ts">
import { useRoute } from 'vue-router'

const route = useRoute()
const id = route.params.id
</script>

<template>
  <p>新闻 ID{{ id }}</p>
</template>

跳转时传入参数:

1
2
3
<router-link :to="{ name: 'news-detail', params: { id: 1 } }">
  新闻1
</router-link>

5.2 多个参数

可以有多个动态段:

1
{ path: '/user/:userId/post/:postId', component: UserPost }

5.3 参数变化时组件复用

同一个路由组件,参数变化时组件会被复用(不会销毁重建)。

如果需要在参数变化时重新获取数据,可以使用 watch 监听 route.params

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { useRoute } from 'vue-router'
import { watch } from 'vue'

const route = useRoute()

watch(
  () => route.params.id,
  (newId) => {
    // 根据新的 ID 重新获取数据
    fetchArticle(newId)
  }
)

第 6 节 嵌套路由

新闻页面下,左侧是列表,右侧是详情区域。这时需要嵌套路由。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import News from '@/pages/News.vue'
import NewsDetail from '@/pages/NewsDetail.vue'

const routes = [
  {
    path: '/news',
    component: News,
    children: [
      {
        path: ':id',          // 实际路径:/news/1
        name: 'news-detail',
        component: NewsDetail,
      },
    ],
  },
]

News.vue 中需要一个 router-view 来渲染子路由:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!-- News.vue -->
<template>
  <div class="flex">
    <div class="sidebar">
      <router-link
        v-for="item in newsList"
        :key="item.id"
        :to="{ name: 'news-detail', params: { id: item.id } }"
      >
        {{ item.title }}
      </router-link>
    </div>

    <div class="content">
      <router-view />
    </div>
  </div>
</template>

子路由的内容会渲染在父组件的 router-view 位置。

第 7 节 路由传参

7.1 params 传参

参数作为 URL 路径的一部分:

1
2
/news/1    → params: { id: 1 }
/news/1/张三  → params: { id: 1, author: '张三' }
1
2
3
<router-link :to="{ name: 'news-detail', params: { id: 1 } }">
  新闻1
</router-link>

7.2 query 传参

参数作为 URL 查询字符串:

1
/news?id=1&page=2    → query: { id: '1', page: '2' }
1
2
3
<router-link :to="{ name: 'news', query: { id: 1 } }">
  新闻1
</router-link>

7.3 两种方式的对比

对比paramsquery
URL 格式/news/1/news?id=1
参数位置路径部分查询字符串
适用场景必填参数、资源标识可选参数、过滤条件
刷新是否丢失不丢失不丢失

第 8 节 编程式导航

除了用 router-link,还可以在 JavaScript 中控制跳转。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { useRouter } from 'vue-router'
const router = useRouter()

// 跳转到指定路径
router.push('/home')

// 按名称跳转
router.push({ name: 'news-detail', params: { id: 1 } })

// 替换当前历史记录(不能回退)
router.replace('/home')

// 前进/后退
router.go(1)     // 前进一页
router.go(-1)    // 后退一页

pushreplace 的区别:

  • push:添加一条新历史记录,可以点"返回"
  • replace:替换当前历史记录,不能点"返回"回到上一个页面

第 9 节 导航守卫

9.1 全局前置守卫

导航触发时,可以用 beforeEach 做拦截:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// src/router/index.ts
import { createRouter } from 'vue-router'

const router = createRouter({ ... })

router.beforeEach((to, from) => {
  // 检查目标路由是否需要登录
  if (to.meta.requiresAuth && !isLoggedIn()) {
    // 未登录,重定向到登录页
    return { name: 'login' }
  }
})

9.2 路由元信息

可以用 meta 给路由附加信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const routes = [
  {
    path: '/admin',
    component: Admin,
    meta: {
      requiresAuth: true,
      title: '管理后台',
    },
  },
]

在守卫中读取:

1
2
3
router.beforeEach((to) => {
  document.title = to.meta.title as string || '默认标题'
})

9.3 完整的守卫流程

1
2
3
4
5
导航触发
  → beforeEach(全局)
    → beforeEnter(路由独享)
      → 组件内守卫
        → afterEach(全局,导航确认后)

afterEach 不能中断导航,适合做日志记录、页面统计:

1
2
3
router.afterEach((to) => {
  console.log(`导航到:${to.path}`)
})

第 10 节 404 页面

:pathMatch(.*)* 捕获所有未匹配的路径:

1
2
3
4
5
6
7
8
const routes = [
  // ...其他路由
  {
    path: '/:pathMatch(.*)*',
    name: 'not-found',
    component: NotFound,
  },
]

这个路由要放在最后,这样就先匹配已有路由,都不匹配才走 404。


路由解决了"页面切换"的问题。现在应用有多个页面了。

但还有一个问题:多个页面需要共享同一份数据

比如用户登录后,用户信息要在首页、个人中心、设置页等多处使用。每个页面都去调接口太浪费,存在哪里统一管理?

答案是 Pinia,Vue 3 的官方状态管理库。