Vue3 useRouter怎么用?新手必看的基础操作、高级技巧与避坑指南
刚接触Vue3 Composition API的同学,肯定会发现之前在Vue2里随处敲的this.$router、this.$route突然失效了——这可不是框架出bug,而是Vue3为了逻辑复用、更清晰的代码结构,把路由操作彻底重构进了组合式API模块里,核心就是useRouter和useRoute两个钩子,接下来就从大家最常问的问题入手,一步步拆解useRouter的所有实用点,连容易踩的小坑都提前给你填好了。
先搞懂:useRouter和useRoute到底有什么区别?
别嫌这个问题基础,我刷问答平台的时候,至少有30%刚用Vue3的人会把这俩搞混,一会儿用useRouter取当前路由参数,一会儿又塞个路径字符串到useRoute的返回值里,简单打个比方:
useRouter就像你手里的「导航仪控制台」——负责主动发起操作,比如切换页面、前进后退、跳转前加个拦截跳转的钩子;useRoute则是「导航仪当前显示的屏幕」——只能看不能改,显示的是当前激活路由的所有静态、动态信息,比如路径、查询参数、路由元信息、匹配到的嵌套路由数组。
举个最直观的例子:在Vue2里你要从商品列表跳转到详情页,是写this.$router.push('/product/123'),同时获取详情页的商品ID是用this.$route.params.id;到了Vue3里,就拆成了分别导入这两个钩子:
// 先从vue-router里导入
import { useRouter, useRoute } from 'vue-router'
export default {
setup() {
// 拿到控制台和当前屏幕
const router = useRouter()
const route = useRoute()
// 点击跳转详情页的函数,用控制台操作
const goToDetail = (productId) => {
router.push(`/product/${productId}`)
}
// 获取当前商品ID,直接读屏幕数据
const currentProductId = computed(() => route.params.id)
return { goToDetail, currentProductId }
}
}
而且要注意,无论是useRouter还是useRoute,都必须在setup函数的顶层或者自定义组合式函数的顶层调用——不能放在if判断、for循环、异步函数(比如axios的回调)、watch回调这些嵌套作用域里,不然会报错“Cannot access 'inject' before initialization”或者拿不到正确的路由实例,为什么呢?因为Vue3的组合式API依赖“依赖注入的上下文”,这个上下文只有在组件/自定义钩子初始化的顶层才是有效的。
基础操作:useRouter的5种常用跳转方式
控制台操作嘛,最核心的就是「换页面」,虽然和Vue2的this.$router方法名基本一样,但细节还是有几个优化的地方,新手别用错了参数。
用字符串路径跳转(最简单但不灵活)
比如跳转到首页、关于页这种固定路径的页面,直接传字符串就行:
router.push('/') // 跳转到根路径首页
router.push('/about') // 跳转到关于页
不过如果你要传动态参数或者查询参数,字符串拼接就容易出错,特别是参数里有中文、特殊符号的时候,建议换成对象形式。
用对象形式跳转(推荐,传参更规范)
对象形式分两种,一种是传路径(path),另一种是传路由的命名(name)——传name的方式更稳定,比如以后你想把商品详情页的路径从/product/:id改成/goods/:productId,不用改所有跳转的地方,只改路由配置里的path就行。
带动态参数的对象跳转
传name的时候,动态参数放在params里;传path的时候,动态参数不能单独放params,必须拼接在path后面,而且单独放的话会被忽略:
// 推荐:用name+params,稳定且规范
router.push({
name: 'ProductDetail', // 路由配置里必须给这条路由加name属性
params: { id: 123 }
})
// 错误示范:用path+params,params会被Vue3完全忽略
// 跳完之后路径还是'/product',没有id
router.push({
path: '/product',
params: { id: 123 }
})
// 正确的path带动态参数方式:拼接字符串
router.push({
path: `/product/${123}`
})
这里还有个小细节:params传的参数是「临时路由内存参数」,刷新页面之后会消失!除非你在路由配置里把动态参数设置成「可选」或者「必填但有默认值」,或者用下面的查询参数。
带查询参数的对象跳转
查询参数就是URL里后面的部分,比如/search?keyword=手机&price=2000-5000,这种参数刷新页面不会消失,适合用来存搜索条件、分页页码这些需要持久化的临时数据。
查询参数既可以和name搭配,也可以和path搭配,放在query对象里就行:
// name+query
router.push({
name: 'ProductSearch',
query: { keyword: '手机', price: '2000-5000' }
})
// path+query
router.push({
path: '/search',
query: { keyword: '手机', price: '2000-5000' }
})
如果query里的参数是数组或者对象,Vue3会自动帮你序列化,但有的后端可能不认识默认的序列化格式(比如默认数组是keyword[]=1&keyword[]=2),这时候可以自己在路由配置的parseQuery和stringifyQuery里自定义。
用replace跳转(不保留当前页面的历史记录)
刚才的push跳转是「在浏览器历史记录栈里加一条新记录」,比如你从列表页→详情页→购物车,按浏览器的后退键会依次回到详情页、列表页,但有的场景不需要保留当前页,比如登录成功后跳转到首页,用户按后退键应该回到登录前的页面,而不是登录页本身——这时候就用replace:
// 用法和push完全一样,只是方法名变了
router.replace({
name: 'Home'
})
replace相当于「把当前历史记录栈顶的记录替换成新的」,其他没区别。
用go/forward/back控制历史记录前进后退
这三个方法就是直接操作浏览器的历史记录栈,和你点浏览器的前进后退按钮效果一样:
router.go(1) // 前进1页,等价于router.forward() router.go(-1) // 后退1页,等价于router.back() router.go(3) // 前进3页(如果历史记录够的话) router.go(0) // 刷新当前页面(不推荐,推荐用window.location.reload或者路由守卫的重定向?不对,路由刷新应该用别的方式?哦不对,go(0)确实是刷新,但会重新加载整个页面,Vue3的组件也会重新挂载,如果你想只重新加载当前路由的组件而不刷新整个页面,可以用「动态key」的方式,比如把App.vue里的<router-view>的key设成route.fullPath)
新开标签页跳转
Vue3的useRouter没有直接的openNewTab方法,但可以用router.resolve先解析出目标路由的完整信息,然后用window.open打开:
const openDetailInNewTab = (productId) => {
// 解析路由,得到href、route等信息
const { href } = router.resolve({
name: 'ProductDetail',
params: { id: productId }
})
// 新开标签页打开
window.open(href, '_blank')
}
这里的router.resolve方法很实用,除了新开标签页,还可以用来判断某个路径是否存在对应的路由,或者在组件外(比如工具函数里)生成路由链接。
进阶技巧:useRouter的隐藏用法
基础操作学会了,接下来讲几个能提升开发效率的进阶技巧,可能新手平时不太注意,但用了之后会觉得“原来还能这么玩”。
在组件外使用useRouter
刚才说过useRouter必须在setup或自定义钩子的顶层调用,但有时候我们需要在工具函数(比如axios的响应拦截器里,当后端返回401未授权时自动跳转到登录页)、Pinia/Vuex的store里使用路由——这时候怎么办呢?
其实很简单,Vue3的vue-router实例是通过provide/inject传递下去的,我们可以在main.js里把router实例挂载到window对象上(适合快速开发,但不推荐生产环境,容易污染全局变量),或者单独封装一个「获取router实例的工具函数」(推荐生产环境):
生产环境推荐的封装方式
// 新建一个文件 src/utils/routerInstance.js
import { createRouter, createWebHistory } from 'vue-router'
import routes from './routes' // 导入你的路由配置
// 先创建好router实例
const router = createRouter({
history: createWebHistory(),
routes
})
// 导出实例,供组件外的地方使用
export default router
然后在main.js里直接导入这个实例挂载:
import { createApp } from 'vue'
import App from './App.vue'
import router from './utils/routerInstance' // 导入封装好的实例
createApp(App).use(router).mount('#app')
之后在axios拦截器、Pinia store里,直接导入这个实例就行:
// src/utils/request.js(axios拦截器)
import axios from 'axios'
import router from './routerInstance'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL
})
// 响应拦截器
request.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
// 未授权,跳转到登录页
router.replace({
name: 'Login',
query: { redirect: router.currentRoute.value.fullPath } // 保存当前路径,登录成功后跳转回来
})
}
return Promise.reject(error)
}
)
export default request
这里的router.currentRoute就是useRoute返回的那个响应式对象的「非顶层调用兼容版本」,不过它是一个ref,所以要加.value才能访问里面的属性。
动态修改路由配置
Vue3的vue-router支持「动态添加路由」和「动态删除路由」,这个功能太实用了——比如做权限管理的时候,不同的用户登录后看到的菜单不一样,对应的路由也不一样,不用一开始就把所有路由都配置好(不然普通用户也能通过手动输入URL访问管理员页面,不安全),而是登录后拿到用户的权限列表,再动态把对应的路由加进去。
动态添加单个路由
用router.addRoute方法,可以加根路由,也可以加嵌套路由:
// 加根路由
router.addRoute({
path: '/admin',
name: 'Admin',
component: () => import('@/views/Admin.vue'),
meta: { requiresAuth: true, role: 'admin' }
})
// 加嵌套路由:第一个参数是父路由的name
router.addRoute('Admin', {
path: 'user-manage',
name: 'AdminUserManage',
component: () => import('@/views/Admin/UserManage.vue')
})
动态删除路由
删除路由有三种方式:
- 通过路由的name删除:
router.removeRoute('Admin') - 通过addRoute返回的函数删除(适合临时添加的路由):
const removeTempRoute = router.addRoute({ path: '/temp', name: 'Temp', component: () => import('@/views/Temp.vue') }) // 不用的时候调用这个函数 removeTempRoute() - 检查路由是否存在:
router.hasRoute('Admin')
权限管理的完整小例子
结合Pinia store,简单写一个权限管理的流程:
// src/store/user.js(Pinia store)
import { defineStore } from 'pinia'
import { loginAPI, getUserInfoAPI } from '@/api/user'
import router from '@/utils/routerInstance'
import asyncRoutes from '@/router/asyncRoutes' // 存放需要权限的动态路由
export const useUserStore = defineStore('user', {
state: () => ({
token: localStorage.getItem('token') || '',
userInfo: null,
routes: [] // 当前用户拥有的动态路由
}),
actions: {
// 登录
async login(loginForm) {
const res = await loginAPI(loginForm)
this.token = res.token
localStorage.setItem('token', res.token)
// 登录成功后获取用户信息和权限
await this.getUserInfo()
},
// 获取用户信息和权限
async getUserInfo() {
const res = await getUserInfoAPI()
this.userInfo = res.userInfo
// 根据用户的权限过滤动态路由
this.filterAsyncRoutes(res.roles)
// 把过滤后的路由动态添加到router里
this.addAsyncRoutes()
},
// 过滤动态路由
filterAsyncRoutes(roles) {
this.routes = asyncRoutes.filter(route => {
// 如果路由没有meta.role,说明所有人都能访问?或者只允许有权限的人?看业务需求
if (!route.meta?.role) return true
// 检查用户是否有对应的权限
return roles.includes(route.meta.role)
})
},
// 添加动态路由
addAsyncRoutes() {
this.routes.forEach(route => {
router.addRoute(route)
})
// 注意:动态添加路由后,要重新调用一次router.replace或者router.push,否则可能会出现404
// 这里可以跳转到登录前保存的redirect路径
const redirect = router.currentRoute.value.query.redirect || '/'
router.replace(redirect)
}
}
})
然后在路由守卫里判断用户是否登录、是否有访问当前路由的权限就行。
避坑指南:新手最容易踩的8个useRouter坑
上面讲了这么多用法,接下来讲几个新手踩坑率90%以上的问题,提前记好,能少走很多弯路。
坑1:在嵌套作用域里调用useRouter/useRoute
比如放在if判断、watch回调、异步函数里,前面已经说过了,这里再强调一遍——必须在setup或自定义钩子的顶层调用!如果实在需要在异步函数里访问路由,可以在顶层先拿到实例,然后在异步函数里用:
// 正确写法
setup() {
const router = useRouter()
const route = useRoute()
const fetchData = async () => {
const id = route.params.id // 这里没问题,route是在顶层拿到的
const res = await getProductAPI(id)
if (res.code === 404) {
router.push('/404') // 这里也没问题
}
}
onMounted(() => {
fetchData()
})
return {}
}
坑2:用path+params传动态参数
前面也讲过,path+params的话,params会被完全忽略,必须拼接在path后面,或者用name+params。
坑3:params参数刷新页面后消失
这个是params的特性,不是bug!因为params是存放在路由内存里的,刷新页面后内存就清空了,如果需要刷新页面后参数还在,要么用query参数,要么把动态参数设成路由配置里的必填项(比如path: '/product/:id')——哦不对,设成必填项的话,params参数是URL的一部分,刷新页面后当然还在!刚才说的临时内存参数是指「没有在路由配置里定义的params」,比如你传了一个路由配置里没有的extraParam,不管用name还是path,刷新页面后都会消失。
比如路由配置是{ path: '/product/:id', name: 'ProductDetail' }:
router.push({
name: 'ProductDetail',
params: { id: 123, extraParam: 'test' }
})
// 跳完之后的URL是'/product/123',extraParam不会出现在URL里
// 这时候route.params.id是123,route.params.extraParam是'test'
// 刷新页面后,route.params.id还是123(因为在URL里),但route.params.extraParam变成了undefined
坑4:useRoute的属性不是响应式的?
很多新手会写这样的代码:
setup() {
const route = useRoute()
const productId = route.params.id // 这里的productId是普通字符串,不是响应式的!
onMounted(() => {
console.log(productId) // 第一次能拿到,但如果路由切换到另一个productId,这里不会更新
})
return { productId }
}
因为route.params.id是一个普通的值,不是ref或reactive,所以要把它变成响应式的,必须用computed包裹:
const productId = computed(() => route.params.id)
这样当路由切换时,productId会自动更新,模板里也会重新渲染。
坑5:动态添加路由后出现404
刚才的权限管理例子里提到过,动态添加路由后,要重新调用一次router.replace或router.push,因为路由的匹配是在页面加载或者路由变化时完成的,动态添加的路由如果不重新触发一次匹配,浏览器地址栏里的路径可能还没被新路由识别,就会走到404页面。
404路由的配置也很重要——在Vue3里,404路由的path必须写成/:pathMatch(.*)*,而不是Vue2里的,因为Vue3用的是path-to-regexp@6.x版本,语法变了:
// 路由配置里的404路由,要放在最后!
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue')
}
如果放在前面,所有路由都会被它匹配到,其他路由就失效了。
坑6:router.push/replace/go没有返回值?
不对,Vue3的vue-router 4.x版本里,router.push/replace/go都返回一个Promise,用来处理跳转成功或失败的情况:
router.push('/product/123')
.then(() => {
console.log('跳转成功')
})
.catch((error) => {
console.log('跳转失败', error)
// 常见的错误原因:
// 1. 路由不存在
// 2. 导航被路由守卫拦截了(比如用户未登录,被前置守卫重定向到登录页)
// 3. 重复导航(比如当前已经在/product/123,又push了一次同样的路径)
})
如果是重复导航,Vue3会默认不报错,也不会执行then或catch——如果你想让重复导航也触发错误,可以在路由配置里设置duplicateNavigationHandling为'reject',或者用try/catch捕获,但一般不需要,因为重复导航没什么影响。
坑7:在路由守卫里调用useRouter/useRoute
路由守卫(比如beforeEach、beforeEnter、beforeRouteLeave)本身就有自己的参数,不需要调用useRouter/useRoute!
比如全局前置守卫beforeEach的参数是(to, from, next),to就是目标路由(等价于useRoute的未来值),from就是当前路由(等价于useRoute的当前值),next是用来控制导航的函数(不过Vue3推荐用return来控制导航,更简洁):
// 正确的全局前置守卫写法
router.beforeEach((to, from) => {
// 检查当前路由是否需要登录
if (to.meta.requiresAuth) {
const token = localStorage.getItem('token')
if (!token) {
// 未登录,重定向到登录页,不需要调用router.push,直接return目标路由对象
return {
name: 'Login',
query: { redirect: to.fullPath }
}
}
}
// 登录了或者不需要登录,直接放行,不需要return next()
})
组件内的路由守卫(beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave)也是一样,beforeRouteEnter不能调用setup里的东西(因为组件还没创建),但可以在next的回调里访问组件实例;beforeRouteUpdate和beforeRouteLeave可以调用setup里的东西,但它们本身也有to、from参数,不需要用useRoute。
坑8:忘记给加key
这个坑和useRouter本身关系不大,但经常和路由跳转一起出现——比如你有一个商品详情页组件,从商品ID为123的页面跳转到ID为456的页面,URL变了,但组件没有重新挂载,created、onMounted这些生命周期钩子不会重新执行,数据也不会更新。
这时候就需要给App.vue里的
<!-- App.vue -->
<template>
<div id="app">
<!-- 用route.fullPath作为key,因为fullPath包含了路径、查询参数、哈希值,每次路由变化都会变 -->
<router-view :key="route.fullPath" />
</div>
</template>
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
</script>
加了key之后组件会完全重新挂载,性能会稍微差一点,如果只是数据更新,不想重新挂载组件,可以用watch监听route.params或route.query的变化,然后在watch回调里重新获取数据:
setup() {
const route = useRoute()
const product = ref(null)
// 监听params的变化
watch(
() => route.params.id,
async (newId) => {
if (newId) {
const res = await getProductAPI(newId)
product.value = res.data
}
},
{ immediate: true } // immediate设为true,组件刚挂载时就执行一次
)
return { product }
}
Vue3的useRouter虽然和Vue2的$router用法类似,但因为组合式API的特性,还是有很多需要注意的地方。
- 区分useRouter(控制台,主动操作)和useRoute(屏幕,被动查看);
- 必须在setup或自定义钩子的顶层调用;
- 传参优先用name+params(动态、稳定)或name/path+query(持久化);
- 组件外使用路由要提前封装好实例;
- 动态添加路由后要重新触发匹配;
- 提前避开那8个常见的坑。
其实useRouter还有很多其他的用法,比如自定义路由过渡、路由懒加载的优化、路由元信息的高级用法等等,不过对于新手来说,先把上面的基础操作、进阶技巧和避坑指南掌握好,就足够应对大部分项目的需求了,如果在实际开发中遇到了其他问题,可以随时去查Vue Router 4.x的官方文档,官方文档写得非常详细,而且有中文版本,新手也能看懂。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


