> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Vue Router 完全指南
- URL: https://blog.vercanti.com/vue-router-wan-quan-zhi-nan/
- Published: 2026-08-28T14:35:27.000Z
- Updated: 2026-08-28T14:58:49.000Z
- Description: Vue Router 4 是 Vue 3 的官方路由库，与 Vue 3 的 Composition API 深度集成。 createWebHistory 可传入 base 参数，例如 createWebHistory('/app/')，适合应用部署在子路径下。 子路由的 path 不需要以 / 开头，父组件模板中必须包含 <RouterView>。 通过 TypeScript 模块扩充声明，为 meta 添加类型约束： router.push() 参数与 <RouterLink to=""> 完全一致，均接受 RouteLocationRaw： 当路由从
- Author: yellowdog
- Tags: 前端开发, Vue生态

> 官方文档：<https://router.vuejs.org/zh/>  
> 适用版本：Vue Router 4.x（2026-05-07 核实）

Vue Router 4 是 Vue 3 的官方路由库，与 Vue 3 的 Composition API 深度集成。

---

## 安装与基础配置

```bash
npm install vue-router@4

```

### createRouter 参数

| 参数                   | 类型                   | 默认值                        | 说明                |
| -------------------- | -------------------- | -------------------------- | ----------------- |
| history              | RouterHistory        | 必填                         | 路由历史模式，决定 URL 格式  |
| routes               | RouteRecordRaw\[\]   | 必填                         | 路由记录数组            |
| scrollBehavior       | RouterScrollBehavior | undefined                  | 控制页面切换时的滚动行为      |
| linkActiveClass      | string               | 'router-link-active'       | 激活链接的 CSS class   |
| linkExactActiveClass | string               | 'router-link-exact-active' | 精确激活链接的 CSS class |
| strict               | boolean              | false                      | 是否严格匹配尾部斜杠        |
| end                  | boolean              | true                       | 是否匹配到路径末尾         |
| sensitive            | boolean              | false                      | 是否区分大小写           |

```typescript
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [],
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    }
    return { top: 0 }
  }
})

```

### 三种 history 模式对比

| 模式            | 函数                     | URL 格式    | 服务器配置         | 适用场景       |
| ------------- | ---------------------- | --------- | ------------- | ---------- |
| HTML5 History | createWebHistory()     | /user/1   | 需要配置 fallback | 生产环境推荐     |
| Hash          | createWebHashHistory() | /#/user/1 | 无需配置          | 静态文件部署     |
| Memory        | createMemoryHistory()  | 无 URL 变化  | 无需配置          | SSR / 测试环境 |

`createWebHistory` 可传入 base 参数，例如 `createWebHistory('/app/')`，适合应用部署在子路径下。

### RouteRecordRaw 字段

| 字段          | 类型                                     | 必填       | 说明            |                    |
| ----------- | -------------------------------------- | -------- | ------------- | ------------------ |
| path        | string                                 | 是        | 路由路径，支持动态段和正则 |                    |
| component   | Component \| () => Promise<Component>  | 否        | 单视图组件         |                    |
| components  | Record<string, Component>              | 否        | 命名视图组件        |                    |
| name        | string \| symbol                       | 否        | 路由名称          |                    |
| children    | RouteRecordRaw\[\]                     | 否        | 嵌套子路由         |                    |
| redirect    | RouteLocationRaw \| Function           | 否        | 重定向目标         |                    |
| alias       | string \| string\[\]                   | 否        | 别名            |                    |
| meta        | RouteMeta                              | 否        | 自定义元信息        |                    |
| beforeEnter | NavigationGuard \| NavigationGuard\[\] | 否        | 路由独享守卫        |                    |
| props       | boolean \| Record<string, any>         | Function | 否             | 将路由参数作为组件 props 传入 |
| sensitive   | boolean                                | 否        | 是否区分大小写       |                    |
| strict      | boolean                                | 否        | 是否严格匹配尾部斜杠    |                    |

---

## 路由定义

### 静态路由与动态路由

```typescript
const routes: RouteRecordRaw[] = [
  // 静态路由
  { path: '/', component: HomeView },

  // 动态路由：:id 为必填参数
  { path: '/user/:id', component: UserView },

  // 可选参数：:id? 表示可有可无
  { path: '/post/:id?', component: PostView },

  // 多个动态段
  { path: '/user/:userId/post/:postId', component: UserPostView },

  // 匹配任意路径（404 页面放在最后）
  { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFoundView }
]

```

### 嵌套路由

子路由的 `path` 不需要以 `/` 开头，父组件模板中必须包含 `<RouterView>`。

```typescript
const routes: RouteRecordRaw[] = [
  {
    path: '/dashboard',
    component: DashboardLayout,
    children: [
      // 访问 /dashboard 时渲染 DashboardHome
      { path: '', component: DashboardHome },
      // 访问 /dashboard/profile 时渲染 UserProfile
      { path: 'profile', component: UserProfile },
      { path: 'settings', component: UserSettings }
    ]
  }
]

```

### 命名路由

```typescript
const routes: RouteRecordRaw[] = [
  {
    path: '/user/:id',
    name: 'user-detail',
    component: UserDetail
  }
]

```

### 重定向与别名

```typescript
const routes: RouteRecordRaw[] = [
  // 字符串重定向
  { path: '/home', redirect: '/' },

  // 命名路由重定向
  { path: '/user/:id', redirect: { name: 'user-detail' } },

  // 函数重定向（可根据 to 动态决定目标）
  {
    path: '/search',
    redirect: (to) => {
      return { path: '/results', query: { q: to.query.keyword } }
    }
  },

  // 别名：访问 /alias 和访问 /original 效果相同
  { path: '/original', alias: '/alias', component: SomeView },

  // 多个别名
  { path: '/users', alias: ['/people', '/members'], component: UsersView }
]

```

### 路由元信息与 TypeScript 类型扩展

通过 TypeScript 模块扩充声明，为 `meta` 添加类型约束：

```typescript
// src/types/router.d.ts 或 src/router/index.ts 顶部
import 'vue-router'

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

// 路由定义中使用
const routes: RouteRecordRaw[] = [
  {
    path: '/admin',
    component: AdminView,
    meta: {
      requiresAuth: true,
      title: '管理后台',
      roles: ['admin']
    }
  }
]

```

---

## 导航

### RouterLink 参数

| 参数                 | 类型               | 默认值                        | 说明                         |
| ------------------ | ---------------- | -------------------------- | -------------------------- |
| to                 | RouteLocationRaw | 必填                         | 目标路由，与 router.push() 参数相同  |
| replace            | boolean          | false                      | 使用 replace 而非 push，不产生历史记录 |
| active-class       | string           | 'router-link-active'       | 链接部分匹配时的 class             |
| exact-active-class | string           | 'router-link-exact-active' | 链接精确匹配时的 class             |
| custom             | boolean          | false                      | 不渲染 <a> 标签，由插槽完全控制渲染       |
| aria-current-value | string           | 'page'                     | 激活链接的 aria-current 属性值     |

```html
<!-- 字符串路径 -->
<RouterLink to="/about">关于</RouterLink>

<!-- 对象形式 -->
<RouterLink :to="{ name: 'user-detail', params: { id: 42 } }">用户详情</RouterLink>

<!-- 带 query -->
<RouterLink :to="{ path: '/search', query: { keyword: 'vue' } }">搜索</RouterLink>

<!-- 替换历史记录 -->
<RouterLink to="/login" replace>登录</RouterLink>

<!-- 自定义渲染（custom + v-slot） -->
<RouterLink to="/about" custom v-slot="{ isActive, navigate }">
  <button :class="{ active: isActive }" @click="navigate">关于</button>
</RouterLink>

```

### 编程式导航

`router.push()` 参数与 `<RouterLink to="">` 完全一致，均接受 `RouteLocationRaw`：

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

const router = useRouter()

// 字符串路径
router.push('/user/1')

// 对象形式（path + query）
router.push({ path: '/search', query: { keyword: 'vue' } })

// 命名路由 + params（推荐）
router.push({ name: 'user-detail', params: { id: 42 } })

// 带 hash
router.push({ path: '/about', hash: '#team' })

// replace：不产生新的历史记录
router.replace({ name: 'home' })

// go：在历史栈中前进/后退
router.go(-1)   // 后退一步，等同于 router.back()
router.go(1)    // 前进一步，等同于 router.forward()
router.go(-3)   // 后退三步

```

---

## 路由参数

### useRoute 获取参数

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

const route = useRoute()

// 动态路由参数（/user/:id → route.params.id）
const userId = route.params.id          // string | string[]

// URL 查询参数（?keyword=vue → route.query.keyword）
const keyword = route.query.keyword    // string | string[] | null

// hash（#section1 → route.hash）
const hash = route.hash                 // string，含 # 符号

// 当前路由完整路径
const fullPath = route.fullPath

// 路由名称
const routeName = route.name

// meta
const requiresAuth = route.meta.requiresAuth

```

### useRouter 操作路由

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

const router = useRouter()

// 获取当前路由（与 useRoute() 相同，但是非响应式快照）
const currentRoute = router.currentRoute.value

```

### 响应路由参数变化

当路由从 `/user/1` 跳转到 `/user/2` 时，因为复用同一组件，组件不会重新创建，需要手动监听参数变化：

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

const route = useRoute()

// 监听单个参数
watch(
  () => route.params.id,
  (newId, oldId) => {
    // 执行数据加载等操作
    loadUserData(newId as string)
  },
  { immediate: true }
)

// 监听整个 params 对象
watch(
  () => route.params,
  (newParams) => {
    console.log(newParams)
  }
)

```

---

## 导航守卫

### 守卫执行顺序

1. `router.beforeEach`（全局前置）
2. `beforeEnter`（路由独享）
3. `onBeforeRouteUpdate`（组件内，路由参数变化时）
4. `router.beforeResolve`（全局解析，异步组件加载完成后）
5. `router.afterEach`（全局后置，无法阻止导航）

### 守卫参数

| 参数   | 类型                      | 说明        |
| ---- | ----------------------- | --------- |
| to   | RouteLocationNormalized | 即将进入的目标路由 |
| from | RouteLocationNormalized | 正在离开的当前路由 |

守卫返回值含义：

| 返回值              | 效果                       |
| ---------------- | ------------------------ |
| true 或 undefined | 允许导航，继续执行                |
| false            | 取消导航，URL 恢复原状            |
| RouteLocationRaw | 重定向到指定路由                 |
| Error            | 取消导航并触发 router.onError() |

### 全局守卫

```typescript
// 全局前置守卫：在导航确认前执行，可以拒绝或重定向
router.beforeEach((to, from) => {
  const authStore = useAuthStore()

  if (to.meta.requiresAuth && !authStore.isLoggedIn) {
    return { name: 'login', query: { redirect: to.fullPath } }
  }
})

// 全局解析守卫：在所有组件内守卫和异步路由组件被解析后执行
router.beforeResolve(async (to) => {
  if (to.meta.requiresCamera) {
    try {
      await askForCameraPermission()
    } catch {
      return false
    }
  }
})

// 全局后置守卫：不能修改导航，常用于修改页面标题、上报分析数据
router.afterEach((to, from) => {
  document.title = (to.meta.title as string) ?? '默认标题'
})

```

### 路由独享守卫

```typescript
const routes: RouteRecordRaw[] = [
  {
    path: '/admin',
    component: AdminView,
    beforeEnter: (to, from) => {
      // 仅在进入该路由时触发，路由参数变化不触发
      if (!isAdmin()) {
        return { name: 'forbidden' }
      }
    }
  }
]

```

### 组件内守卫

```typescript
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

// 离开当前路由前触发（确认是否保存未提交的表单数据）
onBeforeRouteLeave((to, from) => {
  const confirmed = window.confirm('有未保存的内容，确定离开吗？')
  if (!confirmed) return false
})

// 当前路由参数变化时触发（组件复用场景）
onBeforeRouteUpdate(async (to, from) => {
  if (to.params.id !== from.params.id) {
    await loadUserData(to.params.id as string)
  }
})

```

---

## 高级特性

### 路由懒加载

将路由组件定义为动态 import，Vite/Webpack 会自动将其分割为独立的 chunk：

```typescript
const routes: RouteRecordRaw[] = [
  {
    path: '/user',
    // 箭头函数返回 import() 即可
    component: () => import('./views/UserView.vue')
  },
  {
    path: '/admin',
    // 使用 webpackChunkName 或 Vite rollup 注释分组 chunk
    component: () => import(/* webpackChunkName: "admin" */ './views/AdminView.vue')
  }
]

```

### 路由过渡动画

通过 `<RouterView>` 的默认插槽获取当前组件，配合 `<Transition>` 实现动画：

```html
<RouterView v-slot="{ Component, route }">
  <Transition
    :name="route.meta.transition as string ?? 'fade'"
    mode="out-in"
  >
    <!-- key 使不同路由强制触发过渡，即使组件相同 -->
    <component :is="Component" :key="route.path" />
  </Transition>
</RouterView>

```

```css
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

```

### 滚动行为

`scrollBehavior` 接收 `to`、`from`、`savedPosition` 三个参数，返回滚动目标：

```typescript
const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    // 浏览器前进/后退：恢复到上次的滚动位置
    if (savedPosition) {
      return savedPosition
    }

    // 带 hash 的路由：滚动到对应锚点
    if (to.hash) {
      return {
        el: to.hash,
        behavior: 'smooth'
      }
    }

    // 其他情况：回到顶部
    return { top: 0, behavior: 'smooth' }
  }
})

```

### 动态添加路由

```typescript
// 添加顶级路由
router.addRoute({
  path: '/new-feature',
  name: 'new-feature',
  component: () => import('./views/NewFeature.vue')
})

// 添加子路由（指定父路由 name）
router.addRoute('dashboard', {
  path: 'analytics',
  component: AnalyticsView
})

// 检查路由是否存在
if (!router.hasRoute('new-feature')) {
  router.addRoute({ ... })
}

// 删除路由
router.removeRoute('new-feature')

// 添加路由后需要手动触发重定向，以匹配可能因为路由缺失而降级的当前路由
if (router.currentRoute.value.name === 'not-found') {
  router.replace(router.currentRoute.value.fullPath)
}

```

---

## 最佳实践

### 统一路由模块组织

```typescript
// src/router/routes/user.ts
export const userRoutes: RouteRecordRaw[] = [
  {
    path: '/user',
    component: UserLayout,
    meta: { requiresAuth: true },
    children: [
      { path: '', name: 'user-home', component: UserHome },
      { path: ':id', name: 'user-detail', component: UserDetail }
    ]
  }
]

// src/router/index.ts
import { userRoutes } from './routes/user'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: HomeView },
    ...userRoutes,
    { path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound }
  ]
})

```

### 权限路由控制

```typescript
router.beforeEach((to, from) => {
  const auth = useAuthStore()

  // 检查是否需要认证
  if (!to.meta.requiresAuth) return true

  if (!auth.token) {
    return { name: 'login', query: { redirect: to.fullPath } }
  }

  // 检查角色权限
  const required = to.meta.roles
  if (required && !required.some((r) => auth.roles.includes(r))) {
    return { name: 'forbidden' }
  }
})

```

---

## 踩坑与注意事项

### params 只能用于命名路由

`params` 只有在使用命名路由导航时才有效，通过 `path` 导航时 `params` 会被忽略：

```typescript
// 正确：命名路由 + params
router.push({ name: 'user-detail', params: { id: 42 } })

// 错误：path + params 无效，params 会被忽略
router.push({ path: '/user', params: { id: 42 } })  // id 不会生效

// 替代方案：将参数直接写入 path 或使用 query
router.push({ path: `/user/${id}` })
router.push({ path: '/user', query: { id: 42 } })

```

### History 模式需要服务器配置

`createWebHistory` 模式下，刷新页面会向服务器请求该 URL。若服务器没有对应路由，会返回 404。必须配置服务器将所有请求回退到 `index.html`：

**Nginx 配置**：

```nginx
location / {
  try_files $uri $uri/ /index.html;
}

```

**Vite 开发服务器**（自动处理，无需额外配置）。

### 导航守卫中使用 Pinia store

在 `router.beforeEach` 中调用 `useAuthStore()` 必须在 `app.use(pinia)` 之后执行，否则会报错。确保 `app.use(pinia)` 在 `app.use(router)` 之前：

```typescript
const app = createApp(App)
app.use(createPinia())   // Pinia 必须先于 Router 注册
app.use(router)
app.mount('#app')

```

### 动态路由参数类型

`route.params.id` 的类型是 `string | string[]`，不能直接当 number 使用，需要手动转换：

```typescript
const id = Number(route.params.id as string)

```

### 嵌套路由的空路径子路由

父路由 `path: '/dashboard'` 下若有 `path: ''` 的子路由，访问 `/dashboard` 时会渲染该子组件。注意父组件模板必须有 `<RouterView />`，否则子路由无法显示。

---

## 最佳实践

**使用命名路由（`name`）跳转，而非硬编码路径字符串**：路径变化时只需更新路由配置，所有跳转代码自动适配，不会出现部分页面路径未更新的遗漏。

```typescript
// 推荐
router.push({ name: 'UserProfile', params: { id: 123 } })

// 不推荐（路径变化时需要全局查找替换）
router.push('/users/123/profile')

```

**路由守卫中处理权限，不要在每个页面组件里判断**：将鉴权逻辑集中在全局守卫 `beforeEach` 或路由元信息 `meta` 中，组件内部无需关心权限，职责更清晰。

```typescript
router.beforeEach((to, from) => {
  if (to.meta.requiresAuth && !store.isLoggedIn) {
    return { name: 'Login', query: { redirect: to.fullPath } }
  }
})

```

**动态路由懒加载，拆分代码包**：使用 `() => import('./views/xxx.vue')` 的路由组件定义，Vite/Webpack 会将其打包为独立 chunk，首屏只加载必要代码。

**用 `<RouterLink>` 而非 `<a href>`**：`<RouterLink>` 自动处理 `history` vs `hash` 模式的路径差异，并阻止页面刷新（SPA 跳转），`<a>` 会触发全页面刷新。

---

## 常见陷阱

### 陷阱：`router.push` 后 `route.params` 还是旧值

**现象：** 在守卫或页面组件里调用 `router.push({ name: 'Detail', params: { id: 2 } })`，但 `useRoute().params.id` 读到的仍是上一个路由的值。

**原因：** `router.push` 是异步操作，返回 Promise；同步代码里紧接着读 `route.params` 时导航还未完成。

**解决：** `await router.push(...)` 等待导航完成，再读参数；或在组件 `onMounted` / `watch(() => route.params.id, ...)` 里响应式监听变化。

```typescript
// 正确：await 等待导航完成
await router.push({ name: 'Detail', params: { id: 2 } });
console.log(route.params.id); // 已更新

```

### 陷阱：history 模式部署后刷新页面 404

**现象：** 本地开发正常，部署到 Nginx/Apache 后，直接访问 `/user/123` 或刷新页面返回 404。

**原因：** history 模式的路由路径由前端 JS 处理，服务器没有对应的物理文件，直接请求时返回 404。

**解决：** 配置服务器将所有请求回退到 `index.html`，由前端路由接管。

```nginx
location / {
    try_files $uri $uri/ /index.html;
}

```

### 陷阱：全局守卫里忘记处理无限重定向

**现象：** 未登录用户被重定向到 `/login`，但 `/login` 本身也触发守卫，再次重定向，页面卡死或报 `NavigationDuplicated` 错误。

**原因：** `beforeEach` 守卫对所有路由生效，包括重定向目标 `/login`，如果不排除白名单路由，会进入无限重定向循环。

**解决：** 在守卫中先判断目标路由是否是白名单（如 `/login`、`/public`），是则直接放行。

```typescript
router.beforeEach((to) => {
    const whitelist = ['/login', '/register'];
    if (whitelist.includes(to.path)) return true; // 放行
    if (!isLoggedIn()) return '/login';
});

```

---

## 参见

- [Vue3入门](https://blog.vercanti.com/vue-3-ru-men-zhi-nan/)
- [Pinia完全指南](https://blog.vercanti.com/pinia-wan-quan-zhi-nan/)
- [TypeScript完全指南](https://blog.vercanti.com/typescript-wan-quan-zhi-nan/)