Vue Router 完全指南

Vue Router 4 是 Vue 3 的官方路由库,与 Vue 3 的 Composition API 深度集成。 createWebHistory 可传入 base 参数,例如 createWebHistory('/app/'),适合应用部署在子路径下。 子路由的 path 不需要以 / 开头,父组件模板中必须包含 <RouterView>。 通过 TypeScript 模块扩充声明,为 meta 添加类型约束: router.push() 参数与 <RouterLink to=""> 完全一致,均接受 RouteLocationRaw: 当路由从

分享

官方文档:https://router.vuejs.org/zh/
适用版本:Vue Router 4.x(2026-05-07 核实)

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


安装与基础配置

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 是否区分大小写
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 是否严格匹配尾部斜杠

路由定义

静态路由与动态路由

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>

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

命名路由

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

重定向与别名

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 添加类型约束:

// 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']
    }
  }
]

导航

参数 类型 默认值 说明
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 属性值
<!-- 字符串路径 -->
<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

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 获取参数

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 操作路由

import { useRouter } from 'vue-router'

const router = useRouter()

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

响应路由参数变化

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

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 正在离开的当前路由

守卫返回值含义:

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

全局守卫

// 全局前置守卫:在导航确认前执行,可以拒绝或重定向
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) ?? '默认标题'
})

路由独享守卫

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

组件内守卫

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:

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> 实现动画:

<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>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

滚动行为

scrollBehavior 接收 tofromsavedPosition 三个参数,返回滚动目标:

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' }
  }
})

动态添加路由

// 添加顶级路由
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)
}

最佳实践

统一路由模块组织

// 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 }
  ]
})

权限路由控制

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 会被忽略:

// 正确:命名路由 + 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 配置

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

Vite 开发服务器(自动处理,无需额外配置)。

导航守卫中使用 Pinia store

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

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

动态路由参数类型

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

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

嵌套路由的空路径子路由

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


最佳实践

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

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

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

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

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.pushroute.params 还是旧值

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

原因: router.push 是异步操作,返回 Promise;同步代码里紧接着读 route.params 时导航还未完成。

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

// 正确: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,由前端路由接管。

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

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

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

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

解决: 在守卫中先判断目标路由是否是白名单(如 /login/public),是则直接放行。

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

参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog