Code前端首页关于Code前端联系我们

Vue3 useRouter怎么用?新手必看的基础操作、高级技巧与避坑指南

terry 17小时前 阅读数 287 #Vue

刚接触Vue3 Composition API的同学,肯定会发现之前在Vue2里随处敲的this.$routerthis.$route突然失效了——这可不是框架出bug,而是Vue3为了逻辑复用、更清晰的代码结构,把路由操作彻底重构进了组合式API模块里,核心就是useRouteruseRoute两个钩子,接下来就从大家最常问的问题入手,一步步拆解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),这时候可以自己在路由配置的parseQuerystringifyQuery里自定义。

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

动态删除路由

删除路由有三种方式:

  1. 通过路由的name删除:router.removeRoute('Admin')
  2. 通过addRoute返回的函数删除(适合临时添加的路由):
    const removeTempRoute = router.addRoute({
    path: '/temp',
    name: 'Temp',
    component: () => import('@/views/Temp.vue')
    })
    // 不用的时候调用这个函数
    removeTempRoute()
  3. 检查路由是否存在: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里的加一个动态key,让Vue3认为每次路由变化都是不同的组件,从而重新挂载:

<!-- 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的特性,还是有很多需要注意的地方。

  1. 区分useRouter(控制台,主动操作)和useRoute(屏幕,被动查看);
  2. 必须在setup或自定义钩子的顶层调用;
  3. 传参优先用name+params(动态、稳定)或name/path+query(持久化);
  4. 组件外使用路由要提前封装好实例;
  5. 动态添加路由后要重新触发匹配;
  6. 提前避开那8个常见的坑。

其实useRouter还有很多其他的用法,比如自定义路由过渡、路由懒加载的优化、路由元信息的高级用法等等,不过对于新手来说,先把上面的基础操作、进阶技巧和避坑指南掌握好,就足够应对大部分项目的需求了,如果在实际开发中遇到了其他问题,可以随时去查Vue Router 4.x的官方文档,官方文档写得非常详细,而且有中文版本,新手也能看懂。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门