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

Vue3 Router Push到底怎么用?新手常踩的8个坑一次说透(附实战代码)

terry 14小时前 阅读数 236 #Vue

作为Vue全家桶的核心成员之一,Vue Router的路由跳转能力一直是单页应用(SPA)开发里的“刚需开关”,其中最常用的非push方法莫属——毕竟你总不能让用户每次都手动在地址栏敲路径吧?最近后台收到不少新学Vue3的小伙伴提问,要么是push跳不动,要么是参数传不过去,要么是控制台报一堆看不懂的错误,今天就把我两年多Vue3项目实战里关于push的经验全掏出来,从基础用法到进阶技巧,再到避坑指南,连权威文档里容易被忽略的细节都会提。

先搞懂:Vue3里的push和Vue2有啥不一样?

很多小伙伴是从Vue2转过来的,或者是看了旧教程学的,一上来直接用this.$router.push,结果发现控制台直接报错“Cannot read properties of undefined (reading 'push')”——是不是很懵?这就是新旧版本的第一个核心区别。

Vue2的组件实例里是直接挂载了$router和$route这两个全局属性的,用this就能轻松访问;但到了Vue3,为了响应Composition API的“按需引入”理念,官方默认不再把路由对象挂载到this上(除非你用Options API,不过现在Composition API是主流趋势),所以得用useRouter和useRoute这两个组合式函数来获取。

举个最简单的Options API和Composition API对比的例子吧: 如果是Options API写法(目前仍兼容,但官方更推荐Composition):

export default {
  methods: {
    goToDetail() {
      this.$router.push('/detail/123')
    }
  }
}

如果是Composition API写法(带setup语法糖,最常用):

<script setup>
// 必须先从vue-router里引入useRouter
import { useRouter } from 'vue-router'
// 执行useRouter拿到router实例
const router = useRouter()
// 定义跳转方法
const goToDetail = () => {
  router.push('/detail/123')
}
</script>

别小看这几行代码的变化,Composition API的写法更利于Tree Shaking(打包时自动剔除没用的代码),也让路由相关的逻辑更集中,比如可以单独封装一个hooks来处理所有的跳转、权限校验啥的。

基础用法别搞混:字符串、对象、命名路由,三种写法各有用途

push方法支持三种传参形式,每一种都有自己的适用场景,新手最容易犯的错误就是乱用这三种写法导致参数丢失或者跳转错误。

字符串形式:最省心,适合不带复杂参数的简单跳转

比如直接跳转到首页、列表页这种固定路径,用字符串是最快的:

// 纯路径字符串
router.push('/home')
// 带query参数的字符串(注意和路径拼接的区别,直接在字符串里写?后面的参数)
router.push('/list?page=1&size=10')

不过这里有个小问题:如果你的路径里有中文或者特殊字符,最好用encodeURIComponent转义一下,不然可能会出现乱码或者跳转失败的情况,

const searchKeyword = 'Vue3入门教程'
router.push(`/search?keyword=${encodeURIComponent(searchKeyword)}`)

不过话说回来,其实后面要讲的对象形式会自动处理转义的问题,所以带参数的话,更推荐用对象。

对象形式:最灵活,适合带path、query、params、hash等各种参数

对象形式可以拆分成两种:path+query的组合,以及name+params的组合(这两个组合不能混用!不能!这个是新手踩坑最多的地方,后面避坑指南会详细说)。 先讲path+query:

// 直接写path,query是一个对象
router.push({
  path: '/list',
  query: {
    page: 1,
    size: 10,
    category: 'frontend'
  }
})

这种组合的好处是,刷新页面后query参数不会丢失,会一直显示在地址栏里,比如地址栏会变成http://localhost:3000/list?page=1&size=10&category=frontend,适合用来做分页、搜索、筛选这种需要保留状态的场景。

再讲name+params:

// 先在路由配置里给路由加个name属性(必须加!不然会报错)
// 路由配置文件router/index.js里的写法:
// {
//   path: '/detail/:id',
//   name: 'Detail', // 这个name要和push里的name完全一致,区分大小写
//   component: () => import('@/views/Detail.vue')
// }
// 然后在组件里用:
router.push({
  name: 'Detail',
  params: {
    id: 123, 'Vue3的Composition API超好用'
  }
})

这种组合的好处是,params参数不会显示在地址栏的主路径后面,看起来更简洁(不过如果你在路由配置里的path里没有用:xxx这样的动态占位符,那刷新页面后params参数会全部丢失!这个也是超级重要的避坑点),比如上面的例子地址栏只会显示http://localhost:3000/detail/123,title参数完全看不到,适合用来传一些敏感信息或者临时数据。

对了,对象形式还可以加hash参数,比如跳转到某个页面的指定锚点:

router.push({
  path: '/home',
  hash: '#about-us'
})

命名路由其实就是对象形式的一种特殊情况,主要是为了方便

刚才讲name+params的时候已经提到命名路由了,其实就算不用params,用命名路由跳转纯路径也可以:

router.push({ name: 'Home' })

为什么要用命名路由呢?因为如果你哪天修改了路由的path,比如把/home改成/index,那你只需要修改路由配置文件里的path就行,所有用name跳转的地方都不用改,维护起来更方便,尤其是大型项目,路由可能有几十上百个,这时候命名路由的优势就非常明显了。

进阶技巧:权限拦截、重复跳转处理、编程式导航的返回

掌握了基础用法,再学点进阶技巧,你的项目体验会提升不少。

权限拦截:push前先判断用户有没有权限

很多项目都有登录、角色权限的需求,比如只有登录的用户才能跳转到个人中心,只有管理员才能跳转到后台管理页面,这时候可以在路由守卫里做拦截,也可以在push跳转前先判断。 路由守卫是全局拦截,适合统一处理;push前判断适合单个页面的特殊需求,比如个人中心里的某个子页面需要VIP权限。 举个push前判断的例子:

<script setup>
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'
const router = useRouter()
const userStore = useUserStore()
const goToVIPCourse = () => {
  if (!userStore.isLogin) {
    // 没登录,先跳转到登录页,并且用query保存当前要跳转的VIP课程页面路径,登录后可以直接跳回来
    router.push({
      path: '/login',
      query: {
        redirect: useRoute().fullPath // useRoute().fullPath可以拿到当前页面的完整路径
      }
    })
    return
  }
  if (!userStore.isVIP) {
    alert('您还不是VIP,请先开通VIP哦!')
    return
  }
  // 有权限,正常跳转
  router.push('/vip-course/456')
}
</script>

这里还要提一下useRoute的fullPath和path的区别:fullPath包含了query、hash这些所有参数,path只是纯路径,刚才保存redirect的时候一定要用fullPath,不然登录后只会跳转到个人中心的纯路径,之前的查询条件啥的都没了。

重复跳转处理:避免控制台报NavigationDuplicated错误

不知道你有没有遇到过这种情况:连续快速点击两次跳转按钮,控制台就会报一个红色的错误NavigationDuplicated: Avoided redundant navigation to current location——虽然不影响功能,但看着很烦,尤其是强迫症患者。 这个错误是Vue Router 3.1.0版本之后新增的,目的是防止重复路由跳转导致的性能问题或者状态混乱,怎么解决呢? 其实有好几种方法,这里推荐两种最常用的: 第一种是全局捕获这个错误,直接忽略它:

// 在路由配置文件router/index.js里,创建完router实例后加这几行
const originalPush = router.push
const originalReplace = router.replace
router.push = function push(location) {
  return originalPush.call(this, location).catch(err => err)
}
router.replace = function replace(location) {
  return originalReplace.call(this, location).catch(err => err)
}

第二种是在单个组件的push跳转里加catch:

router.push('/home').catch(err => {
  // 这里可以什么都不做,也可以根据err做一些处理,比如提示用户已经在当前页面了
  console.log('重复跳转了', err)
})

推荐第一种全局捕获的方法,因为不需要每个组件都写catch,太麻烦了。

编程式导航的返回:除了go(-1),还可以用back

说完了push,顺便提一下编程式导航的返回,因为它们经常一起用。 Vue3里的返回主要有三种方法:

  • router.back():返回上一页,相当于浏览器的后退按钮,只能返回一次
  • router.go(n):n是正数就前进n页,负数就后退n页,比如router.go(-2)就是后退两页,router.go(1)就是前进一页
  • router.forward():前进一页,相当于浏览器的前进按钮,只能前进一次 这些方法和push一样,Options API用this.$router.back(),Composition API用引入的router实例调用。

新手必踩的8个坑,我帮你踩过了

刚才在前面已经提到了一些坑,现在集中整理一下,一共8个,都是我自己或者身边的小伙伴踩过的,一定要记牢!

Composition API里忘记引入useRouter

这个是最最最常见的坑,没有之一!就像开头说的,直接用this.$router.push或者直接写router.push都会报错,必须先引入useRouter,然后执行它拿到实例。 避坑方法:每次要写push的时候,先检查一下有没有这两行代码:

import { useRouter } from 'vue-router'
const router = useRouter()

path+params混用

这个也是超级常见的!刚才讲对象形式的时候已经强调过了,path和params不能混用!如果混用了,params参数会被完全忽略,

// 错误写法!params会被忽略
router.push({
  path: '/detail/123',
  params: { 'Vue3超好用'
  }
})

避坑方法:要么用path+query,要么用name+params,不要搞混。

用name+params跳转,但路由配置里没有加name

用name+params跳转的前提是,路由配置里必须有对应的name属性,而且要完全一致,区分大小写!不然会报错“Route with name 'xxx' does not exist”。 避坑方法:每次给路由加name的时候,最好复制粘贴到push里,避免拼写错误或者大小写错误。

用name+params跳转,但路由配置里的path没有加动态占位符,导致刷新后params丢失

这个也是很多小伙伴容易忽略的!如果你的路由配置里的path是/detail,没有加:id这样的动态占位符,那用name+params跳转后,地址栏显示的是/detail,刷新页面后params参数会全部消失。 避坑方法:如果希望某个params参数刷新后不丢失,就要把它放到路由配置的path里作为动态占位符;如果是临时参数,刷新后不需要保留,那就没问题。

传递参数的时候用了字符串拼接而不是encodeURIComponent或者对象形式

刚才在基础用法里提到过,如果用字符串拼接带中文或者特殊字符的参数,可能会出现乱码或者跳转失败的情况。 避坑方法:带参数的话,尽量用对象形式,因为对象形式会自动处理转义的问题;如果一定要用字符串拼接,记得用encodeURIComponent转义一下参数值。

保存redirect的时候用了useRoute().path而不是fullPath

刚才在进阶技巧的权限拦截里提到过,fullPath包含了所有参数,path只是纯路径,如果保存redirect的时候用了path,登录后只会跳转到纯路径,之前的查询条件啥的都没了。 避坑方法:保存redirect的时候一定要用useRoute().fullPath。

重复跳转没有处理,导致控制台报NavigationDuplicated错误

这个刚才也讲过了,虽然不影响功能,但看着很烦。 避坑方法:推荐全局捕获这个错误,直接忽略它。

Options API和Composition API的useRouter/useRoute搞混

比如在Options API的setup里引入useRouter没问题,但在methods或者其他生命周期钩子(除了setup里的onMounted这些)里用this.$router.push或者useRouter/useRoute就会有问题。 避坑方法:Options API就用this.$router和this.$route;Composition API就用引入的useRouter和useRoute,不要搞混,如果是混用的情况(比如老项目升级),尽量统一用一种写法。

实战项目里的小案例:电商搜索页跳转到商品详情页

最后给大家举一个电商项目里的真实小案例:搜索页输入关键词,点击搜索,显示商品列表,然后点击某个商品跳转到详情页。 首先是路由配置文件router/index.js:

import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import Search from '@/views/Search.vue'
import Detail from '@/views/Detail.vue'
const routes = [
  {
    path: '/',
    redirect: '/home'
  },
  {
    path: '/home',
    name: 'Home',
    component: Home
  },
  {
    path: '/search',
    name: 'Search',
    component: Search
  },
  {
    path: '/detail/:id',
    name: 'Detail',
    component: Detail
  }
]
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})
// 全局捕获重复跳转错误
const originalPush = router.push
const originalReplace = router.replace
router.push = function push(location) {
  return originalPush.call(this, location).catch(err => err)
}
router.replace = function replace(location) {
  return originalReplace.call(this, location).catch(err => err)
}
export default router

然后是搜索页Search.vue:

<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const searchKeyword = ref('')
// 模拟商品列表数据
const productList = ref([
  { id: 1, title: 'Vue3实战项目教程', price: 99 },
  { id: 2, title: 'React18入门到精通', price: 129 },
  { id: 3, title: 'TypeScript进阶指南', price: 79 }
])
// 搜索方法
const handleSearch = () => {
  if (!searchKeyword.value.trim()) {
    alert('请输入搜索关键词!')
    return
  }
  // 用path+query跳转,刷新后关键词不会丢失
  router.push({
    path: '/search',
    query: {
      keyword: searchKeyword.value.trim()
    }
  })
  // 这里可以加一个请求接口获取商品列表的逻辑,根据searchKeyword.value过滤
}
// 跳转到详情页的方法
const goToDetail = (id) => {
  // 用name+params跳转,只显示id在地址栏,简洁
  router.push({
    name: 'Detail',
    params: {
      id
    }
  })
}
</script>
<template>
  <div class="search-page">
    <input v-model="searchKeyword" type="text" placeholder="请输入搜索关键词" />
    <button @click="handleSearch">搜索</button>
    <div class="product-list">
      <div v-for="product in productList" :key="product.id" class="product-item" @click="goToDetail(product.id)">
        <h3>{{ product.title }}</h3>
        <p>价格:¥{{ product.price }}</p>
      </div>
    </div>
  </div>
</template>
<style scoped>
.search-page {
  padding: 20px;
}
input {
  padding: 10px;
  width: 300px;
  margin-right: 10px;
}
button {
  padding: 10px 20px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
.product-list {
  margin-top: 20px;
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
}
.product-item {
  padding: 20px;
  border: 1px solid #eee;
  border-radius: 4px;
  cursor: pointer;
  width: 200px;
}
</style>

详情页Detail.vue:

<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const productDetail = ref({})
// 模拟获取商品详情的接口
const getProductDetail = async (id) => {
  // 这里可以替换成真实的axios请求
  const mockData = {
    1: { id: 1, title: 'Vue3实战项目教程', price: 99, desc: '这是一本关于Vue3实战项目的教程,包含了电商、后台管理、移动端等多个项目' },
    2: { id: 2, title: 'React18入门到精通', price: 129, desc: '这是一本关于React18的入门到精通的教程,适合零基础和有一定基础的开发者' },
    3: { id: 3, title: 'TypeScript进阶指南', price: 79, desc: '这是一本关于TypeScript的进阶指南,包含了泛型、装饰器、类型体操等高级内容' }
  }
  productDetail.value = mockData[id] || {}
}
// 页面加载时获取商品详情
onMounted(() => {
  // 从route.params里拿到id
  const id = route.params.id
  if (id) {
    getProductDetail(id)
  } else {
    // 没有id,跳转到首页
    router.push('/home')
  }
})
</script>
<template>
  <div class="detail-page" v-if="productDetail.id">
    <button @click="router.back()">返回上一页</button>
    <h1>{{ productDetail.title }}</h1>
    <p class="price">价格:¥{{ productDetail.price }}</p>
    <p class="desc">{{ productDetail.desc }}</p>
  </div>
  <div v-else>
    <p>商品不存在,正在跳转到首页...</p>
  </div>
</template>
<style scoped>
.detail-page {
  padding: 20px;
}
.price {
  font-size: 24px;
  color: red;
  font-weight: bold;
}
.desc {
  margin-top: 20px;
  line-height: 1.8;
}
</style>

这个案例虽然简单,但涵盖了push的三种基础用法、重复跳转处理、权限拦截的雏形(详情页判断有没有id)、useRouter和useRoute的正确使用、刷新后参数不丢失的处理等多个知识点,非常适合新手练习。

好了,关于Vue3 Router Push的内容就讲这么多了,从基础到进阶再到避坑,还有实战案例,应该能解决你大部分的问题,如果还有什么不清楚的地方,可以在评论区留言,我会尽量回复大家。

版权声明

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

热门