六维教程

Vue3 路由进阶

基础路由能跑通两个页面了,真实项目还需要给路由起名字、URL 带参数(/goods/123)、页面嵌子页面、登录才能进的页面等,这篇把路由的进阶能力一网打尽。

命名路由

跳转按路径写,路径一旦改动,所有跳转的地方都要跟着改,可以给路由记录起个名字,跳转按名字走,路径改了不用动别处:

{ path: '/about', name: 'about', component: About }

路由记录里的 name 字段就是名字,之后 router.push({ name: 'about' }) 就能按名字跳。

要带参数就再补一个 params,写成 router.push({ name: 'goods', params: { id } }),这点后面的动态路由会反复用到。

动态路由

商品详情都长一个样,只是 id 不同,为每个 id 写一条路由不现实,可以把 id 写成占位符,让一条路由匹配所有 id,URL 里不同的值存成参数,这就是动态路由

{ path: '/goods/:id', component: GoodsDetail }

path 里以 : 开头的段就是占位符,能匹配一段任意值,/goods/123/goods/abc 都会命中这一条,匹配到的那段值会存进参数对象。

读取参数

组件需要知道占位符的实际值。Vue Router 把匹配到的参数统一收进 route 对象的 params 属性,组件从它身上取,不用自己解析 URL:

<script setup>
import { useRoute } from 'vue-router'

const route = useRoute()
// 访问 /goods/123 时 route.params 是 { id: '123' }
console.log(route.params.id)
</script>

route.params 是只读属性,注意值永远是字符串,URL 里的一切都是字符串,需要数值时用 Number(route.params.id) 转换。

传递参数

参数不光要读,跳转时也要带过去,router.push 带参数有两种写法:

const routes = [
  { path: '/goods/:id', name: 'goods', component: GoodsDetail }
]
// 按路径写,参数自己拼进字符串,简单直接
router.push({ path: `/goods/${id}` })
// 按名字写,参数放进 params 交给路由去填,推荐
router.push({ name: 'goods', params: { id } })

按路径写,id 直接拼进 path;按名字写,参数放进 params 由路由去填。

注意,按路径写时 params 会被忽略{ path: '/goods/123', params: {...} } 里的 params 不生效,想用 params 就得走 name 写法,这是新手最容易踩的坑。

侦听参数变化

同一个动态路由组件下,参数变了组件会被复用(不会重建),取数据的代码只在挂载时跑过一次,参数变了它不会自己再跑。

需要一个”参数一变就重新执行”的机制,这正是 watch 的本职(Vue3 计算与侦听 讲过它),盯着 route.params.id,变了就重新请求:

<script setup>
import { useRoute } from 'vue-router'
import { watch } from 'vue'

const route = useRoute()

watch(() => route.params.id, (id) => {
  // 参数一变,重新请求商品详情
  fetchGoods(id)
})
</script>

嵌套路由

后台管理系统最典型,外层是”侧边栏 + 头部”框架,页面结构长这样:

┌─────────────────────────────────┐
│        顶部导航栏                │
├────────────┼────────────────────┤
│   侧边栏    │                    │
│  菜单列表   │     中间内容区      │
│            │   <router-view>    │
└────────────┴────────────────────┘

顶栏和侧边栏是布局,永远不动,中间那块是内容,点菜单只换这一块。

落到路由表上,/admin 这一条是布局,children 里挂的是内容。

访问 /admin/users 时,路由先匹配父记录 /admin,把 AdminLayout 整页画出来,再匹配子记录 users,把 AdminUsers 填进它中间的 <router-view>,外层套内层,这就是嵌套路由

const routes = [
  {
    path: '/admin',
    component: AdminLayout,        // 布局组件,负责顶栏和侧边栏
    children: [
      { path: '', component: Dashboard },        // 访问 /admin
      { path: 'users', component: AdminUsers },  // 访问 /admin/users
      { path: 'settings', component: AdminSettings }
    ]
  }
]

AdminLayout 画外壳,在中间内容区放一个 <router-view>,子路由的组件就渲染进这里,不占全局出口:

<!-- AdminLayout.vue -->
<template>
  <div class="admin">
    <header>顶部导航栏</header>
    <div class="body">
      <aside>侧边栏菜单</aside>
      <main>
        <!-- 子路由的组件渲染到这里,只有这块会换 -->
        <router-view />
      </main>
    </div>
  </div>
</template>

于是三个地址共用同一个外壳,只有内容区在换:

地址 外壳(不动) 内容区(换)
/admin AdminLayout Dashboard
/admin/users AdminLayout AdminUsers
/admin/settings AdminLayout AdminSettings

点侧边栏”用户管理”跳到 /admin/users,顶栏和侧边栏全程不动,只有中间 <main> 区域替换成 AdminUsers。

想再套一层,就在子组件里再放一个 <router-view>,后台系统一层套一层就是这么来的。

children 是子路由数组,写法跟路由表完全一样,子路由的 path 有讲究,'users' 不带 /,是相对父路由拼接,父是 /admin,最终地址就是 /admin/users'/users'/,是绝对路径,直接从根开始,地址变成 /users,和 /admin 没关系了,所以约定是子路由常写相对路径,把父路由的 / 开头留给父记录管。

重定向与 404

用户输入的地址不会都对应到页面,一种是旧地址已经换到新页面,要把旧地址指过去;另一种是地址根本没定义,得有个兜底页面接住,前者用 redirect,后者用 /:pathMatch(.*)*

重定向

网站改版后旧地址作废,访问旧地址的用户要能落到新页面,比如想让访问 / 的用户直接看到 /home,路由记录加一个 redirect 字段,值是目标地址:

const routes = [
  { path: '/', redirect: '/home' },          // 访问 / 直接跳到 /home
  { path: '/home', component: Home }
]

访问 / 时,路由发现这条记录带 redirect,就自动改道去 /home,地址栏也跟着变,redirect 的值可以写字符串 '/home',也可以写对象 { name: 'home' }

404

另一种情况是地址真没定义,比如手输错了一个 URL,所有路由都匹配不上,页面会白屏,解决办法是在路由表最后放一条兜底路由,把没被匹配的路径全部接住,渲染 404 页面:

const routes = [
  { path: '/', redirect: '/home' },
  { path: '/home', component: Home },
  { path: '/:pathMatch(.*)*', component: NotFound }  // 兜底 404
]

path 里的 :pathMatch 是参数名,负责捕获剩余未匹配的路径,(.*)* 表示匹配任意多字符,/abc/a/b/c 这种没定义的地址都会落到这条,被捕获的原始路径存在 route.params.pathMatch 里,需要时可以取出来。

懒加载

假设网站有首页、商品、订单等 10 个页面,全部用 import 直接引用,打包后 10 个页面的代码全塞进同一个 JS 文件,用户打开首页,得先下载完这个整个大文件才能看到内容,哪怕他根本不会点订单页,这部分代码也白下载了,页面越多浪费越大。

懒加载的思路是组件和访问时机绑定,访问到哪个页面,才去加载哪个页面的代码,component 不直接传组件,改传一个箭头函数,函数里用 import() 动态导入:

// 同步加载:所有页面代码挤进一个文件,打开首页就要全下载
import GoodsDetail from '../views/GoodsDetail.vue'
const routes = [
  { path: '/goods/:id', component: GoodsDetail }
]

// 懒加载:GoodsDetail.vue 单独一个文件,访问 /goods/:id 才请求
const routes = [
  { path: '/goods/:id', component: () => import('../views/GoodsDetail.vue') }
]

差别就在 component 的两种写法,直接写组件是同步引用,打包时和主文件混在一起;写成 () => import(...),Vite 会把 GoodsDetail.vue 单独拆成一个文件,浏览器第一次访问 /goods/:id 时才去请求它,首屏只下载当前页面需要的文件,页面越多差距越明显。

导航守卫

用户没登录就去点「订单」,不该直接放他进 /order,而是改道去登录页,登录完再回来,这种”进门前的检查”就是导航守卫

router.beforeEach 是全局入口,它挂的回调在每次导航确认前都会执行一次,拦不拦、改去哪都由回调的返回值说了算:

// src/router/index.js
router.beforeEach((to, from) => {
  const token = localStorage.getItem('token')

  // 要去的页面需要登录,但没登录 → 跳登录页并记录来源
  if (to.meta.requiresAuth && !token) {
    return { name: 'login', query: { redirect: to.fullPath } }
  }
  return true   // 放行
})

回调参数是两个路由对象,to 是要去的地方,from 是现在的地方,返回值决定导航结果,只有三种情况:

  • 返回 true 或不返回,放行
  • 返回一个路由对象,改道去那里(可以带 query
  • 返回 false,取消这次导航

守卫怎么知道”这页要登录”?靠路由上的 meta 字段,值是普通对象:

{ path: '/order', component: Order, meta: { requiresAuth: true } }

to.meta 就是 { requiresAuth: true }to.meta.requiresAuthtrue 就说明这页要登录。

除了 beforeEach,还有几种,触发时机不同,回调写法和返回值规则都一样,先混个脸熟,用到再查:

// afterEach:导航完成后再跑,没有返回值,适合改标题、埋点
router.afterEach((to) => {
  document.title = to.meta.title || '默认标题'
})

// beforeEnter:写在单条路由记录上,只进这一条时才触发
{ path: '/admin', component: Admin, beforeEnter: (to, from) => { /* 只拦这一页 */ } }

还有 beforeResolve,作用几乎等同 beforeEach,只是执行时机更靠后(等组件内守卫跑完),平常很少用到。

路由视图上的 KeepAlive

切换路由时组件默认会重新创建,表现是列表页滚到一半,切去详情页再切回来,列表重建,回到顶部、重新请求数据,滚动位置和临时状态全没了。

让路由切换也保留状态,把 <router-view> 包进 <KeepAlive>

<router-view>
  <template #default="{ Component }">
    <KeepAlive :include="['List', 'Detail']">
      <component :is="Component" />
    </KeepAlive>
  </template>
</router-view>

这段写法有点绕,拆开看:

<router-view> 的默认插槽会提供一个 Component,就是当前匹配到的组件,用 <component :is="Component"> 把它渲染出来(复习动态组件),中间套一个 <KeepAlive>,这个组件切走时不销毁,被缓存下来,再切回来直接恢复原样,滚动位置、表单都还在,include 按组件名指定缓存哪些,没列到的不缓存。

速查卡片

场景 写法
命名路由 { name: 'about' } + router.push({ name })
动态参数 path: '/goods/:id'route.params.id
参数变化再取数据 watch(() => route.params.id, cb)
嵌套路由 children: [{ path: 'users', component }]
重定向 / 404 redirect / /:pathMatch(.*)*
懒加载 component: () => import('./X.vue')
登录拦截 router.beforeEach((to) => to.meta.requiresAuth && !token ? 登录页 : true)
保留页面状态 <KeepAlive><router-view>

路由全通了。接下来是共享数据的治理,认识 Pinia 并用 Store 解决多个组件共享同一份数据的问题。

上一篇
Vue3 路由基础
下一篇
Vue3 状态管理入门