1. useRoute和useRouter到底是什么,为什么非用不可
1.1 两个API的定位与职责边界
先说结论:useRoute和useRouter是Vue3组合式API体系中处理路由的两个核心函数。useRoute负责“读”,它返回当前激活的路由信息对象,包含path、query、params、meta、fullPath、name等字段;useRouter负责“写”,它返回一个Router实例,提供push、replace、go、back、forward等导航方法。
我见过很多刚转Vue3的同学把这两个东西搞混,上来就const route = useRouter(),然后route.push()一下发现能用,转头又想route.query拿参数,结果拿不到,一脸懵。其实这俩的职责边界非常清晰:useRoute是当前路由的“快照描述”,useRouter是整个路由系统的“控制器”。
为什么需要单独引入这两个函数?因为在Vue3的setup环境中,根本没有this可以用。Vue2时代大家习惯的this.$route和this.$router在组合式API里直接失效,如果你在setup里写this.$route.path,控制台会直接给你报错。useRoute和useRouter就是官方给出的替代方案,它们以显式调用的方式,把路由信息暴露给setup环境。
一个特别容易忽略的细节:useRoute返回的对象本身是响应式的,但设计上它应该被当作只读来使用。官方文档明确说,不要对route对象直接赋值或者修改其属性,因为路由状态的唯一来源是router本身,你手动改route对象不会触发任何导航,只会造成状态不一致。
1.2 与选项式API的路由对象对比
为了让你更直观地理解,这里直接对照一下Vue2/Vue3选项式写法和Vue3组合式写法的差异:
| 需求 | Vue2 / Vue3选项式 | Vue3组合式 |
|---|---|---|
| 获取当前路径 | this.$route.path | route.path |
| 获取查询参数 | this.$route.query | route.query |
| 获取动态路由参数 | this.$route.params | route.params |
| 获取路由元信息 | this.$route.meta | route.meta |
| 编程式导航跳转 | this.$router.push() | router.push() |
| 替换当前路由 | this.$router.replace() | router.replace() |
| 前进/后退 | this.$router.go() | router.go() |
看到没,除了获取位置变了,方法名基本是一一对应的。所以如果你以前写过Vue2的路由操作,切换过来的成本很低——核心就是记住:this.$route变成useRoute()的返回值,this.$router变成useRouter()的返回值。
但这里有个必须强调的点:useRoute和useRouter函数必须在组件setup的执行阶段同步调用。你如果在setTimeout的回调里调用,或者在一个普通的工具函数里调用(非setup上下文),大概率会拿到一个undefined或者直接报错。原因也很简单,这两个函数内部依赖inject机制从Vue的依赖注入系统中获取router实例,而注入关系只在组件实例活跃的同步时期是有效的。
1.3 响应式原理与更新机制
再深挖一层,useRoute为什么是响应式的?Vue Router 4内部对当前路由做了响应式处理,具体来说,它把当前路由的path、query、params、name、meta、fullPath、hash等字段放到了一个响应式对象里。当你调用useRoute时,实际上拿到的是这个响应式对象的浅层代理(Proxy)。
正因为返回的是响应式对象,在模板里直接用是没问题的:
<template> <div>当前路径:{{ route.path }}</div> <div>当前查询参数:{{ route.query }}</div> </template> <script setup> import { useRoute } from 'vue-router' const route = useRoute() </script>路由切换时,route对象会自动更新,模板也会自动重新渲染。这背后的机制涉及Vue Router内部对路由变化的监听和响应式系统的联动,但作为使用者,你不需要关心那么深——你只需要知道,不要对route对象做解构赋值(比如const { path, query } = route),因为解构出来的值是普通值,不具备响应性,路由变化时它们不会更新。这个问题后面常见坑的部分我会详细展开。
2. 从零到上手:useRoute和useRouter的正确使用姿势
2.1 安装与路由实例化前提
在正式使用这两个API之前,你首先得有一个Vue Router实例。这一步看似基础,但很多新手直接在项目中npm install vue-router之后就上手了,版本混乱反而导致useRoute导出找不到。这里建议直接安装Vue Router 4.x版本,它才是适配Vue3的正式版本。
npm install vue-router@4然后创建一个router实例:
// src/router/index.js import { createRouter, createWebHistory } from 'vue-router' import Home from '../views/Home.vue' const routes = [ { path: '/', name: 'Home', component: Home }, { path: '/user/:id', name: 'UserDetail', component: () => import('../views/UserDetail.vue'), meta: { title: '用户详情' } } ] const router = createRouter({ history: createWebHistory(), routes }) export default router在入口文件里注册:
// src/main.js import { createApp } from 'vue' import App from './App.vue' import router from './router' const app = createApp(App) app.use(router) app.mount('#app')注册完成后,useRoute和useRouter才真正可用。这里有个冷门知识点:如果你在路由实例注册之前就在组件里调用useRouter,会得到undefined,因为依赖注入还没建立。
2.2 在setup中获取路由对象
拿到路由对象的核心代码非常简单:
<script setup> import { useRoute, useRouter } from 'vue-router' const route = useRoute() const router = useRouter() </script>就这三行,你就能在组件里自由获取路由信息和执行导航操作了。注意vue-router这个包名,这个导入路径是固定的,没有开箱即用的全局变量可选。
我见过有些项目为了图省事,将route和router挂到provide上全局注入,然后到处inject,其实完全没必要。useRoute和useRouter本身就是基于provide/inject机制的封装,官方已经把最方便的姿势给你了,直接用就行。
2.3 获取路由信息:query、params、meta、fullPath
这是useRoute最核心的使用场景。我在实际项目里最常用的字段有四个:query、params、meta、fullPath。
query用于获取URL中?后面的查询参数:
<script setup> import { useRoute } from 'vue-router' const route = useRoute() // 假设URL是 /list?page=2&keyword=vue console.log(route.query.page) // "2" console.log(route.query.keyword) // "vue" </script>params用于获取动态路由参数:
<script setup> import { useRoute } from 'vue-router' const route = useRoute() // 假设URL是 /user/123,路由定义是 /user/:id console.log(route.params.id) // "123" </script>meta用于获取路由元信息,这个在实际项目中非常重要,比如面包屑、标题、权限标识都可以放在meta里:
<script setup> import { useRoute } from 'vue-router' const route = useRoute() // 假设路由定义中 meta: { title: '用户详情', requiresAuth: true } console.log(route.meta.title) // "用户详情" console.log(route.meta.requiresAuth) // true </script>fullPath是完整路径,包括query部分:
// 假设URL是 /user/123?tab=info console.log(route.fullPath) // "/user/123?tab=info" console.log(route.path) // "/user/123"这里有个细节:route.path不含query,route.fullPath含query。如果你需要全局唯一的路由标识(比如做埋点上报、动态标题),fullPath通常更可靠,因为不同query组合算不同页面状态。
还有一个比较常用的字段是route.name,返回当前路由配置的name,在判断页面身份、控制显示逻辑时有奇效。
2.4 路由变化监听:watch的几种玩法
useRoute虽好,但有个问题:如果你在组件里只在初始化时读取一次route数据,后续路由变化(特别是参数变化)不会自动触发你的业务逻辑。比如详情页从/user/1切到/user/2,组件可能被复用,setup不会重新执行。
这时就需要手动监听路由变化,最常见的方式是使用watch:
<script setup> import { useRoute } from 'vue-router' import { watch } from 'vue' const route = useRoute() // 监听整个route对象 watch( () => route.path, (newPath, oldPath) => { console.log('路径变化:', oldPath, '->', newPath) // 在这里重新请求数据、重置状态等 } ) // 也可以监听具体的query参数 watch( () => route.query.page, (newPage, oldPage) => { if (newPage && newPage !== oldPage) { fetchList({ page: newPage }) } } ) </script>还有一种场景是需要获取路由变化前后的完整route对象,这时可以直接写watch(() => route.fullPath, ...),因为fullPath的变化几乎等价于路由的整体变化。
需要特别注意,这里watch的getter函数写法一定要正确。watch(route.path, ...)这种写法在Vue3里其实也是合法的,因为route.path本身是响应式对象的属性,可以直接被追踪。但更推荐的写法是watch(() => route.path, ...),语义更明确,也避免了一些边界问题的潜在风险。
2.5 组件内路由守卫的配合使用
Vue Router 4提供了几个组合式API形式的组件内守卫,它们和useRoute、useRouter是一套体系里的,经常一起配合使用。
先看onBeforeRouteLeave,它在离开当前路由前触发,适合做表单未保存确认:
<script setup> import { onBeforeRouteLeave } from 'vue-router' onBeforeRouteLeave((to, from, next) => { if (hasUnsavedChanges.value) { const ok = window.confirm('你有未保存的修改,确定离开吗?') if (!ok) { return false // 取消导航 } } return true // 允许离开 }) </script>再看onBeforeRouteUpdate,它在当前路由参数变化但组件被复用时触发,这和watch监听其实有重叠,但守卫的语义更贴近“路由生命周期”:
<script setup> import { onBeforeRouteUpdate } from 'vue-router' onBeforeRouteUpdate((to, from) => { // 处理动态参数变化时的逻辑 loadUserData(to.params.id) }) </script>如果你想要在守卫里做跳转,就需要配合useRouter:
<script setup> import { useRouter, onBeforeRouteLeave } from 'vue-router' const router = useRouter() onBeforeRouteLeave((to, from) => { if (someCondition) { router.push('/other-page') return false } }) </script>组件内守卫和全局守卫(router.beforeEach)的定位不同:全局守卫管理“全局规则”,组件内守卫处理“局部业务”,在开发后台管理系统时,这两者经常要同时使用。useRouter在守卫之外的场景也有大用处,下面专门讲。
3. useRouter实战:跳转操作、参数传递与进厂配合
3.1 编程式导航的完整API清单
编程式导航的意思是,在代码里通过调用router实例的方法来触发路由跳转,而不是用户点击<router-link>。useRouter返回的router实例提供了以下核心方法:
push:跳转到新路由,会往历史记录里增加一条记录(用户能通过返回按钮回退):
router.push('/list') router.push({ path: '/list' }) router.push({ name: 'List', query: { page: 1 } })replace:跳转到新路由,但替换当前历史记录(用户按返回按钮会跳到上一个页面,而不是当前页面):
router.replace('/login') router.replace({ path: '/login' })go:正数前进,负数后退:
router.go(1) // 前进一步,等价于router.forward() router.go(-1) // 后退一步,等价于router.back() router.go(-3) // 后退三步back:后退一步,等价于router.go(-1)。
forward:前进一步,等价于router.go(1)。
实际开发里,push和replace用的最多,go偶尔用在返回上,back和forward比较少见。
3.2 三种传参方式对比:query、params、state
跳转时最核心的需求是传参。useRouter配合传参,有三种姿势,但各有坑,必须分清。
第一种,query传参。这种方式参数会挂在URL的?后面:
// 跳转页面写入 router.push({ path: '/list', query: { page: 2, keyword: 'vue' } }) // 目标页面读取 const route = useRoute() console.log(route.query.page) // "2" console.log(route.query.keyword) // "vue"query参数的特点是完全URL化,刷新页面、复制链接、分享给他人参数都不会丢,可以收藏。缺点是参数不能在URL里暴露得过于敏感(比如密码、token之类别放这里)。
第二种,params传参。这种方式参数写在路由路径里,需要在路由定义里通过:占位符来声明:
// 路由定义 { path: '/user/:id', name: 'UserDetail', component: () => import('../views/UserDetail.vue') } // 跳转 router.push({ name: 'UserDetail', params: { id: 123 } }) // 目标页面读取 const route = useRoute() console.log(route.params.id) // "123"params传参有个极易踩的坑:如果跳转时用path而不是name,params会被忽略:
// 错误示例:params不会生效 router.push({ path: '/user', params: { id: 123 } }) // 正确示例:用name跳转 router.push({ name: 'UserDetail', params: { id: 123 } })这个问题在Vue Router过往版本中就存在,很多从Vue2转过来的开发者都栽过。实际开发中,如果路由里已经摆了:id占位符,强烈建议优先用name跳转。
第三种,state传参。这种方式的参数不会出现在URL里,而是存到浏览器history的状态中:
// 跳转 router.push({ path: '/detail', state: { from: 'home' } }) // 目标页面读取 const route = useRoute() console.log(route.state) // { from: 'home' }state传参的优点是参数对用户不可见,比较安全,能传稍微大点的数据。缺点是刷新页面后state可能丢失(不同浏览器行为略有不一),且不利于分享链接。
三种方式各有用处,我的经验是:需要可分享、可收藏的页面状态用query;路由本身定义好的动态路径用params;纯业务传递、不想暴露给用户的数据用state或pinia。
3.3 路由重复跳转报错的处理思路
这是useRouter使用中高频出现的问题:重复点击同一个路由链接,或者跳到当前所在路由,控制台会报一个TypeError:Avoided redundant navigation to current location。
举个例子,你在列表页,连续点击两次跳转相同地址的按钮:
const goList = () => { router.push('/list') } // 第一次点击正常跳转,第二次点击控制台报错这个错误本质上不是致命错误,不影响功能,但控制台的红字很烦人,而且如果写测试用例,这类异常会导致测试失败。
处理方案有好几种,最简单的做法是在调用时catch掉:
router.push('/list').catch(() => {})更优雅的做法是在全局路由层面上做统一处理:
// router/index.js const originalPush = Router.prototype.push Router.prototype.push = function push(location) { return originalPush.call(this, location).catch(err => err) }Vue Router 4里默认会返回一个Promise,所以用catch兜底是正规姿势。另外啰嗦一句,别因为这个错误就去拦截全局跳转逻辑,这样容易引发其他边界问题。
3.4 在全局守卫里使用useRouter的替代方案
有一种情况我见很多人困惑:在router文件里定义的全局前置守卫,想在里面跳转,能不能用useRouter?答案是不能直接使用,因为守卫的注册是在router实例上,而useRouter依赖组件上下文。
在全局守卫里如果你需要做页面跳转,直接用router实例本身就行,因为守卫注册时你已经在useRoute/useRouter的依赖之外了:
// router/index.js import router from './router' router.beforeEach((to, from, next) => { if (to.meta.requiresAuth && !isLogin()) { // 跳转登录页 next({ path: '/login', query: { redirect: to.fullPath } }) } else { next() } })这里next({ path: '/login' })就是在守卫里执行重定向的写法,不需要useRouter。很多人被“必须用useRouter才能跳转”的思维限制住了,其实在路由配置文件层面,router实例就是无所不能的。
4. 高频踩坑与排查实录:useRoute和useRouter的常见问题
4.1 在setup之外调用导致的undefined大坑
这个坑我遇见的频率极高。常见场景是你在一个普通工具函数里调用useRouter:
// utils/nav.js import { useRouter } from 'vue-router' export function goHome() { const router = useRouter() // 报错:useRouter() is called without a component context router.push('/home') }这个错误信息本质上是在提示:你不在组件上下文中,useRouter拿不到注入的实例。
解决方案有几个。最推荐的是,不把导航逻辑抽出去,直接在组件里调用useRouter;如果非要在工具函数里导航,可以把router实例作为参数传进来:
// utils/nav.js export function goHome(router) { router.push('/home') } // 组件里 const router = useRouter() goHome(router)还有一个思路,在工具函数里直接引入之前创建好的router实例:
// utils/nav.js import router from '../router' export function goHome() { router.push('/home') }这个方案可行,但前提是router实例已经被创建并导出,同时在组件中不要重复创建router实例,保持单例。
4.2 解构route导致响应式失效
route对象本身是响应式的,但如果你对它进行解构,拿出来的就是普通值,响应式会静默丢失。看这个例子:
<script setup> import { useRoute } from 'vue-router' import { watch } from 'vue' const route = useRoute() // 危险操作:解构出来的path不是响应式的 const { path, query } = route watch(path, (newVal) => { console.log('path变化啦', newVal) // 永远不会触发 }) </script>这种情况下watch的getter被传了一个普通值进去,Vue根本没有可追踪的响应式依赖。
正确的做法是,要么直接在整个route对象上使用watch的getter函数:
watch(() => route.path, (newVal) => { ... })要么用toRefs或者toRef来保留响应性:
import { toRefs } from 'vue' const { path, query } = toRefs(route) // 此时path.value才是当前路径,且是响应式的 watch(path, (newVal) => { ... })实际项目中我推荐写法:如果你只是想在模板里展示route字段,直接用route.path;如果你需要在script里监听变化,用watch(() => route.path, ...);只有在你真的需要把某个字段单独传递出去、且要保持响应式时才用toRefs。
4.3 页面刷新后params参数丢失的终极解
这个问题堪称经典。在Vue2时代,动态路由/user/1刷新后params还在,因为params天生就在URL里。但在Vue Router 4里有一种情况,params会莫名其妙丢失——就是4.2里说的,用path跳转时传params:
// 假设当前在 /user/1 router.push({ path: '/user/2', params: { source: 'list' } })刷新页面后route.params.source变成了undefined。因为params的设计是绑定到路由路径的,不在URL里体现的动态参数,刷新时自然就丢了。
更严重的一种场景,组件复用刷新:从/user/1切到/user/2,组件被复用了,但setup不重新执行,你在setup里读取route.params.id的代码只在初始化时跑了一次,页面显示的还是旧数据。
这两种问题的组合解决思路是:
- 必要的、需要持久化的参数放到query里,因为它天然持久。
- 业务传参不想暴露时用pinia/状态管理库。
- 监听路由变化,在参数变化时重新获取数据:
<script setup> import { useRoute } from 'vue-router' import { watch } from 'vue' const route = useRoute() watch( () => route.params.id, async (newId) => { if (newId) { await fetchUser(newId) } }, { immediate: true } // 初始化时立即执行一次 ) </script>这样即使组件被复用,参数改变也能正确响应。
4.4 在定时器或事件回调中使用旧路由对象的问题
useRoute返回的route对象虽然是响应式的,但如果你在某个异步回调里捕获了旧值,回调执行时拿到的还是旧的路由信息。看这个例子:
const route = useRoute() setTimeout(() => { console.log(route.path) // 如果路由已经切换,这可能是最新的,也可能不是 }, 3000)这里之所以说“可能不是”,是因为route是响应式对象,读取property时是实时从Proxy上取值的,所以如果你访问的是route.path,理论上能拿到最新的。直接访问route对象本身也是这样,它就是活的。
但反过来,如果你在回调里访问的是解构出来的普通变量:
const route = useRoute() const { path: oldPath } = route setTimeout(() => { console.log(oldPath) // 永远不会变,是旧值 }, 3000)这又是解构导致的问题。所以在异步场景里,要么用实时读取,要么仔细跟踪依赖关系。
4.5 useRoute与静态路由配置的联动陷阱
有个场景容易忽略:路由配置里的redirect字段。当你配置了重定向,比如/重定向到/home,在/组件里调用useRoute时,route对象指向的是重定向后的路由,不是用户一开始访问的路由。这会导致你在代码里读route.query时发现某些参数对不上。
实际项目中的表现是:用户访问/?from=mobile,你的路由配置把/重定向到了/home,此时在/home页面读route.query就找不到from参数了。
如果需要保留初始URL上的query和重定向组合,可以这样配置:
const routes = [ { path: '/', redirect: to => { return { path: '/home', query: to.query } // 手动合并query } } ]这种“路由跳转时参数丢失”的问题,排查起来很隐蔽,最好在项目初期就把路由重定向的参数保留策略定好。
5. 真实场景里的组合技:useRoute和useRouter的进阶玩法
5.1 后台管理系统:动态面包屑和页面标题
后台管理系统的核心套路之一,就是根据路由信息生成面包屑导航。我参与过的几个后台项目基本都这么干:路由的meta里配置每一项的title,面包屑组件读取route.matched,逐层渲染。
<template> <el-breadcrumb separator="/"> <el-breadcrumb-item v-for="item in breadcrumbs" :key="item.path" :to="{ path: item.path }" > {{ item.meta.title }} </el-breadcrumb-item> </el-breadcrumb> </template> <script setup> import { computed } from 'vue' import { useRoute } from 'vue-router' const route = useRoute() const breadcrumbs = computed(() => { // route.matched是当前路由匹配到的所有嵌套路由记录的数组 return route.matched.filter(item => item.meta && item.meta.title) }) </script>页面标题同理,在后置守卫里统一设置:
// router/index.js router.afterEach((to) => { document.title = to.meta.title ? `${to.meta.title} - 管理系统` : '管理系统' })这里的核心思想就是:把页面元信息交给路由配置管理,组件只用读取展示,配合useRoute读meta,实现了清晰的职责分离。
5.2 商城场景:列表页跳详情页的传参规范
商城类项目里,商品列表页到详情页的跳转是最高频路由操作。商品id绝对不能乱放,它既关系到页面初始化数据加载,也关系到分享、收藏、复购入口能否定位到同一个商品。我的通用写法是:
// 列表页跳详情页 const goDetail = (goodsId) => { router.push({ path: `/goods/${goodsId}` }) }这样跳过去的URL是/goods/123,可分享、可收藏、可刷新,完全不依赖组件内部状态。详情页拿到id后先展示缓存数据(如果有),再发请求拿最新数据:
<script setup> import { useRoute } from 'vue-router' const route = useRoute() const goodsId = route.params.id // 根据goodsId加载商品信息 fetchGoodsDetail(goodsId) </script>这里需要特别提醒:不要用query传商品id,即URL变成/goods?goodsId=123的形态。虽然在功能上完全可用,但URL语义化差,不利于SEO、埋点统计和后续维护。路由设计上保住“路径即资源”的规则,项目越做越久越能体会到好处。
5.3 配合pinia做跨页面状态缓存
前面说了,params和query都有各自的局限性,跨页面传递较大对象(比如用户勾选的一批表格数据)时,最优雅的方案是用状态管理库。pinia和Vue Router的组合很常见。
比如在A页面保存过滤条件,跳转到B页面并读取:
// stores/filter.js import { defineStore } from 'pinia' export const useFilterStore = defineStore('filter', { state: () => ({ condition: null }), actions: { setCondition(condition) { this.condition = condition } } })// A页面 <script setup> import { useRouter } from 'vue-router' import { useFilterStore } from '../stores/filter' const router = useRouter() const filterStore = useFilterStore() const goResultPage = () => { filterStore.setCondition({ keyword: 'vue', status: 1 }) router.push('/result') } </script>// B页面 <script setup> import { useFilterStore } from '../stores/filter' const filterStore = useFilterStore() console.log(filterStore.condition) // { keyword: 'vue', status: 1 } </script>这种方案的优点是数据量可以很大、刷新页面仍能保留(如果配合pinia持久化插件),缺点是数据不在URL里,不利于分享。所以如果是可分享的场景就要用query,如果只是业务流程里的状态就用pinia,两者分工明确。
5.4 与keep-alive组件配合时的缓存生命周期管理
Vue3中keep-alive和多级路由融合后,组件缓存生命周期会直接影响useRoute的使用。比如后台管理系统中的系统设置页面,用户从A环境切到B环境,同一组件被keep-alive缓存住,setup不会重新执行,但你期望它根据路由参数重新初始化数据。
这时可以配合onActivated组合式API来处理:
<script setup> import { useRoute } from 'vue-router' import { onActivated } from 'vue' const route = useRoute() onActivated(() => { // 从缓存激活时重新读取路由参数 refreshData(route.query) }) </script>这种情况下route.query依然是响应式的,在onActivated里读取能拿到最新的。但要注意,如果你在setup里已经把route.query赋值给了某个响应式变量,激活后这个变量不会自动更新,需要在onActivated里重新赋值。
keep-alive加上useRoute的组合,是后台管理系统里特别容易出坑的地方。排查思路也很明确:先确认组件是被缓存了还是重新渲染了,再确认代码逻辑是在setup阶段执行还是需要在onActivated中执行。
5.5 用useRoute实现页面埋点上报
埋点系统的核心是,在页面切换时上报用户从哪来到哪去,useRoute的fullPath和route.matched正好能拼出这些信息。
// router/index.js router.afterEach((to, from) => { // 上报页面访问 trackPageView({ from_path: from.fullPath, to_path: to.fullPath, from_title: from.meta.title, to_title: to.meta.title }) })如果你需要在组件内部上报特定的交互,也可以结合useRoute拿当前的定位信息:
<script setup> import { useRoute } from 'vue-router' const route = useRoute() const reportClick = (eventName) => { trackEvent({ event: eventName, page_path: route.fullPath, page_title: route.meta.title }) } </script>这种方案的优点是埋点代码和路由解耦,页面上线后不需要逐个组件去找埋点位置,在路由层统一收口即可。
写在最后的几点实际经验
根据我这些年在多个Vue3项目里的实操感受,用useRoute和useRouter写代码时,有几个习惯是真的能帮你少踩坑的。第一,从String Router 4开始,官方设计非常强调“显式调用”和“显式依赖”,所以尽量在setup顶部把route和router一次性拿好,整个组件统一使用这两个变量,不要各种绕路。第二,路由信息本质上是一个“外部资源”,它的变化是不以组件内部状态为转移的,所以涉及路由参数驱动的逻辑,务必使用watch或onBeforeRouteUpdate来同步,而不是在setup里一次性读取。第三,传参之前先想清楚:这个参数刷新后还要不要存在?要不要允许用户分享链接?如果都要,就用query;如果是纯粹的业务流转,就放pinia。最后,我再分享一个我自己常用的调试技巧——在开发环境的根组件里加一个小面板,实时展示route对象的结构变化,比如path、query、params、meta、fullPath这些字段一变化就高亮,排查路由问题效率非常高。这些经验不是什么高深技术,但日积月累下来,确实能让Vue3项目的路由相关代码变得非常省心。