# 路径的艺术：Vue Router 完全指南


## 概述

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

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

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

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

```
/home  → Home.vue
/news  → News.vue
/about → About.vue
```

## 安装与配置

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

手动安装：

```shell
npm install vue-router
```

然后创建路由配置文件：

```typescript
// 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` 中注册：

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

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

## 页面跳转

### RouterLink 和 RouterView

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

- **`RouterLink`**：替代 `<a>` 标签，点击时不刷新页面
- **`RouterView`**：当前路由对应的组件渲染在这个位置

```vue
<!-- 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` 中。

> [!info] 说明
> `router-link` 和 `router-view` 在模板中也可以写成 kebab-case：`<router-link>` 和 `<router-view>`。

### 路由配置

在 `/src/pages/` 下创建三个页面组件：

```vue
<!-- Home.vue -->
<template>
  <h1>首页</h1>
  <p>欢迎来到首页</p>
</template>
```

```typescript
// 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`。

## 命名路由

路由可以添加 `name` 属性，用名称代替路径：

```typescript
const routes = [
  {
    path: '/home',
    name: 'home',
    component: Home,
  },
]
```

模板中使用：

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

用名称的好处：如果路径变了，只需改一处（路由配置），不需要改所有模板。

## 动态路由匹配

### 路由参数

新闻详情页的 URL 可能是 `/news/1`、`/news/2`，其中 `1` 和 `2` 是动态变化的。

用 `:id` 来匹配动态段：

```typescript
const routes = [
  {
    path: '/news/:id',
    name: 'news-detail',
    component: NewsDetail,
  },
]
```

在组件中获取参数：

```vue
<!-- 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>
```

跳转时传入参数：

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

### 多个参数

可以有多个动态段：

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

### 参数变化时组件复用

同一个路由组件，参数变化时组件会被**复用**（不会销毁重建）。

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

```typescript
import { useRoute } from 'vue-router'
import { watch } from 'vue'

const route = useRoute()

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

## 嵌套路由

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

```typescript
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` 来渲染子路由：

```vue
<!-- 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` 位置。

## 路由传参

### params 传参

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

```
/news/1    → params: { id: 1 }
/news/1/张三  → params: { id: 1, author: '张三' }
```

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

### query 传参

参数作为 URL 查询字符串：

```
/news?id=1&page=2    → query: { id: '1', page: '2' }
```

```vue
<router-link :to="{ name: 'news', query: { id: 1 } }">
  新闻1
</router-link>
```

### 两种方式的对比

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

## 编程式导航

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

```typescript
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)    // 后退一页
```

`push` 和 `replace` 的区别：

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

## 导航守卫

### 全局前置守卫

导航触发时，可以用 `beforeEach` 做拦截：

```typescript
// src/router/index.ts
import { createRouter } from 'vue-router'

const router = createRouter({ ... })

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

### 路由元信息

可以用 `meta` 给路由附加信息：

```typescript
const routes = [
  {
    path: '/admin',
    component: Admin,
    meta: {
      requiresAuth: true,
      title: '管理后台',
    },
  },
]
```

在守卫中读取：

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

### 完整的守卫流程

```
导航触发
  → beforeEach（全局）
    → beforeEnter（路由独享）
      → 组件内守卫
        → afterEach（全局，导航确认后）
```

`afterEach` 不能中断导航，适合做日志记录、页面统计：

```typescript
router.afterEach((to) => {
  console.log(`导航到：${to.path}`)
})
```

## 404 页面

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

```typescript
const routes = [
  // ...其他路由
  {
    path: '/:pathMatch(.*)*',
    name: 'not-found',
    component: NotFound,
  },
]
```

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

---

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

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

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

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


---

> 作者: Aphros  
> URL: https://blog.papergate.top/posts/%E8%B7%AF%E5%BE%84%E7%9A%84%E8%89%BA%E6%9C%AFvue-router-%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97/  

