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

Vue3 Router传参到底有几种?每种都有什么坑?

terry 49分钟前 阅读数 13 #Vue

现在做Vue项目的前端同学,几乎都会用到路由传参吧?不管是从列表页跳转到详情页带ID,还是从筛选页跳转到列表页带条件,传参都是日常操作,但Vue3发布后,Router4也跟着更新了,有些传参方式和Vue2+Router3有了变化,甚至还偷偷藏了点小坑,稍不注意就会掉进去,今天就来跟大家聊聊Vue3 Router传参的所有合法方式,还有每种方式的最佳使用场景、避坑指南,以及最近在项目里看到的一些小技巧。

Vue3 Router传参的3种核心方式,你真的都用对了吗?

很多同学可能只知道query和params传参,但其实Router4还有一种路径动态参数的“变种”?不对,严格来说路径动态参数是独立的一种底层方式,query和params是基于它或者URL机制的上层用法,接下来我会把这3种核心方式拆得明明白白。

路径动态参数传参(最推荐用于标识性资源跳转)

路径动态参数应该是大家用得最早也最多的一种?比如从电商列表跳转到商品详情页,URL一般会是/product/12345,这里的12345就是商品ID的动态参数,这种方式的好处是URL清晰、语义化强、刷新页面参数不丢失、搜索引擎友好,因为搜索引擎爬虫更喜欢这种层级分明的路径,而不是后面跟着一堆问号的query。

怎么配置路由?

在路由配置文件里,路径动态参数需要用冒号开头,后面接参数名,比如刚才的商品详情页,可以这么写:

// router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import ProductList from '@/views/ProductList.vue'
import ProductDetail from '@/views/ProductDetail.vue'
const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    redirect: '/product-list'
  },
  {
    path: '/product-list',
    name: 'ProductList',
    component: ProductList
  },
  {
    path: '/product/:id', // 这里的:id就是动态参数
    name: 'ProductDetail',
    component: ProductDetail
  }
]
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})
export default router

如果参数是可选的怎么办?比如有时候详情页不传ID就是新建商品页面,传了就是编辑页面,这时候可以在参数名后面加个问号:path: '/product/:id?'

怎么发起跳转?

发起跳转有两种方式,一种是声明式导航(用<router-link>组件),另一种是编程式导航(用useRouter组合式API)。 声明式导航

<!-- ProductList.vue -->
<template>
  <div class="product-list">
    <router-link
      v-for="product in products"
      :key="product.id"
      :to="`/product/${product.id}`" // 直接拼接路径字符串
    >
      {{ product.name }}
    </router-link>
  </div>
</template>

或者用更规范的对象写法,避免手动拼接出错(比如参数有特殊字符的时候):

<router-link
  :to="{ path: `/product/${product.id}` }"
>
  {{ product.name }}
</router-link>

编程式导航

<!-- ProductList.vue -->
<template>
  <div class="product-list">
    <div
      v-for="product in products"
      :key="product.id"
      @click="goToDetail(product.id)"
      class="product-item"
    >
      {{ product.name }}
    </div>
  </div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
const goToDetail = (id: string | number) => {
  // 同样有字符串和对象两种写法,推荐对象写法
  // 字符串写法
  // router.push(`/product/${id}`)
  // 对象写法
  router.push({ path: `/product/${id}` })
}
</script>

怎么获取参数?

在目标页面(ProductDetail.vue)里,用useRoute组合式API获取:

<!-- ProductDetail.vue -->
<script setup lang="ts">
import { useRoute } from 'vue-router'
const route = useRoute()
const productId = route.params.id // 注意这里是params,不是pathParams或者其他的
</script>

这种方式的坑有哪些?

第一个坑:用编程式导航的对象写法时,不能同时用path和params属性,比如刚才的goToDetail函数,如果写成:

router.push({
  path: '/product', // 这里不能只写/product,要写带:id的路径,或者干脆不用path用name
  params: { id: product.id }
})

这种写法在开发环境下可能没问题,因为Router4会自动把params拼到path后面,但在生产环境打包后,params属性会被直接忽略!这是新手最容易踩的第一个大坑!正确的做法要么是用刚才的path拼接,要么是用name和params配合:

router.push({
  name: 'ProductDetail', // 必须和路由配置里的name完全一致,包括大小写
  params: { id: product.id }
})

第二个坑:动态参数不能是对象或者数组,只能是字符串、数字、布尔值(布尔值会被转换成字符串'true'或'false'),如果一定要传复杂类型,建议转成JSON字符串后编码URI,或者用后面的state传参。 第三个坑:可选参数后面如果跟着其他动态参数,要注意参数的顺序和分隔符的处理,尽量避免这种复杂的路由结构,不然路径匹配和参数获取都会出问题。

Query传参(最推荐用于非标识性、可序列化的筛选/搜索条件)

Query传参的URL一般是/product-list?keyword=手机&minPrice=1000&maxPrice=5000,这里的keyword、minPrice、maxPrice就是query参数,这种方式的好处是刷新页面参数不丢失、可以分享给别人(别人打开也是同样的筛选结果)、可以支持复杂的查询组合(比如同时加多个筛选条件),而且不需要提前在路由配置里声明,灵活度很高。

怎么配置路由?

Query传参不需要提前配置路由,直接在发起跳转的时候带参数就行。

怎么发起跳转?

同样有声明式和编程式两种写法,推荐用对象写法。 声明式导航

<!-- ProductFilter.vue -->
<template>
  <div class="product-filter">
    <input v-model="keyword" type="text" placeholder="搜索商品" />
    <input v-model.number="minPrice" type="number" placeholder="最低价格" />
    <input v-model.number="maxPrice" type="number" placeholder="最高价格" />
    <router-link
      :to="{
        name: 'ProductList',
        query: {
          keyword,
          minPrice,
          maxPrice
        }
      }"
    >
      搜索
    </router-link>
  </div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const keyword = ref('')
const minPrice = ref<number | ''>('')
const maxPrice = ref<number | ''>('')
</script>

注意:不管是用name还是path配合query都可以,不会像params那样忽略。 编程式导航

// ProductFilter.vue的script setup部分,其他同上
const router = useRouter()
const handleSearch = () => {
  router.push({
    path: '/product-list',
    query: {
      keyword: keyword.value || undefined, // 可以传undefined,这样URL里不会显示这个参数
      minPrice: minPrice.value || undefined,
      maxPrice: maxPrice.value || undefined
    }
  })
}

这里有个小技巧:如果某个筛选条件是空的,可以传undefined或者null,Router4会自动把这个参数从URL里去掉,让URL更简洁。

怎么获取参数?

还是用useRoute组合式API,不过这次是route.query

<!-- ProductList.vue -->
<script setup lang="ts">
import { useRoute, ref, onMounted, watch } from 'vue'
const route = useRoute()
const products = ref([])
const keyword = ref('')
const minPrice = ref<number | ''>('')
const maxPrice = ref<number | ''>('')
// 页面初始化的时候获取一次参数
onMounted(() => {
  updateParams()
  fetchProducts()
})
// 监听路由变化,当query参数改变的时候重新获取数据
watch(
  () => route.query,
  () => {
    updateParams()
    fetchProducts()
  },
  { deep: true } // query是对象,需要开启深度监听
)
const updateParams = () => {
  keyword.value = route.query.keyword as string || ''
  minPrice.value = route.query.minPrice ? Number(route.query.minPrice) : ''
  maxPrice.value = route.query.maxPrice ? Number(route.query.maxPrice) : ''
}
const fetchProducts = () => {
  // 这里写请求接口的逻辑,把keyword、minPrice、maxPrice传给后端
  console.log('搜索条件:', { keyword, minPrice, maxPrice })
}
</script>

这里要注意:从route.query里获取到的所有参数都是字符串类型的,包括数字和布尔值,所以如果需要用数字或者布尔值,必须自己转换。

这种方式的坑有哪些?

第一个坑:如果传的是对象或者数组,虽然可以转成JSON字符串后直接传,但有时候会出现特殊字符的问题,比如对象里有中文、空格、&、?这些字符,可能会导致URL解析错误,解决办法是用encodeURIComponent()编码,获取的时候用decodeURIComponent()解码,再转成JSON:

// 发起跳转时编码
const conditions = {
  categories: ['手机', '平板'],
  sort: { field: 'price', order: 'asc' }
}
router.push({
  path: '/product-list',
  query: {
    conditions: encodeURIComponent(JSON.stringify(conditions))
  }
})
// 获取参数时解码
const conditionsStr = decodeURIComponent(route.query.conditions as string)
const conditions = JSON.parse(conditionsStr)

第二个坑:不要传太复杂或者太长的对象/数组,因为不同浏览器对URL的长度有限制(一般是2000-8000个字符),超过的话会被截断,导致参数获取不完整,如果条件真的很多,建议用localStorage或者sessionStorage临时存储,或者用后面的state传参。 第三个坑:watch路由的时候,如果只监听route的变化,而不是route.query或者route.params,可能会触发不必要的重新请求(比如只是hash变了,或者路由元信息变了,但参数没变),所以最好是只监听需要变化的部分,或者在watch的回调里加个判断,对比新旧参数是否一样。

State传参(最近几年火起来的临时、复杂、敏感数据传参)

State传参是HTML5 History API自带的功能,不是Vue3 Router特有的,但Vue3 Router4把它封装得很好用了,这种方式的URL和普通的跳转一样,不会显示任何参数,非常适合传临时的、复杂的、不希望用户看到或者修改的敏感数据(比如表单的临时草稿、用户的临时登录状态、上一页的滚动位置等等),刷新页面后state参数会丢失,这是它的特点也是它的缺点,大家要根据场景选择。

怎么配置路由?

和Query传参一样,不需要提前配置路由

怎么发起跳转?

只有编程式导航有state传参,声明式导航<router-link>虽然也有state属性,但用法不如编程式灵活。

<!-- ProductEdit.vue -->
<template>
  <div class="product-edit">
    <input v-model="productForm.name" type="text" placeholder="商品名称" />
    <textarea v-model="productForm.desc" placeholder="商品描述"></textarea>
    <button @click="saveDraftAndGoBack">保存草稿并返回</button>
  </div>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const productForm = reactive({
  id: '12345',
  name: '',
  desc: ''
})
const saveDraftAndGoBack = () => {
  router.push({
    name: 'ProductDetail',
    state: {
      draft: productForm // 可以直接传对象或数组,不需要转JSON
    }
  })
}
</script>

注意:state传参可以配合name、path、query、params一起使用,不会有冲突。

怎么获取参数?

获取state参数有两种方式,一种是用useRoute组合式API,另一种是直接用history.state(原生HTML5的方式,但推荐用前者):

<!-- ProductDetail.vue -->
<script setup lang="ts">
import { useRoute, onMounted } from 'vue'
const route = useRoute()
onMounted(() => {
  // 第一种方式:用route.state
  const draft = route.state?.draft
  if (draft) {
    console.log('收到草稿:', draft)
    // 这里可以把草稿存到localStorage里,防止刷新页面丢失
    localStorage.setItem('productDraft', JSON.stringify(draft))
  }
  // 第二种方式:用history.state
  // const draft = history.state?.draft
})
</script>

这里有个小技巧:如果想要state参数在刷新页面后也能保留,可以在获取到的时候存到localStorage或者sessionStorage里,页面刷新后再从里面取出来,用完记得删掉。

这种方式的坑有哪些?

第一个坑:刷新页面后state参数会丢失,这个刚才已经说过了,大家要注意场景。 第二个坑:State传参不支持跨域跳转,如果从当前域名跳转到另一个域名,state参数会被忽略。 第三个坑:在使用router.replace()替换路由的时候,state参数也会替换掉原来的state,如果原来的state有重要信息,要记得先保存下来。

3种传参方式的最佳使用场景对比,一张表帮你搞定

为了让大家更直观地选择合适的传参方式,我做了一张对比表:

传参方式 URL是否显示参数 刷新页面是否丢失 是否需要配置路由 是否支持复杂类型 最佳使用场景
路径动态参数 是,语义化强 标识性资源跳转(商品详情、用户详情)
Query传参 是,在问号后面 是(需编码) 非标识性、可序列化的筛选/搜索条件
State传参 是(直接传) 临时、复杂、敏感数据(表单草稿、滚动位置)

除了核心方式,还有两个冷知识/小技巧,用过的都是老司机

冷知识1:用Router4的useRoute可以获取当前路由的所有信息,包括之前的路由

很多同学不知道,useRoute返回的对象里还有一个matched属性,里面是当前路由匹配到的所有路由记录(包括父路由),还有一个redirectedFrom属性,里面是当前路由的来源路由(也就是上一个路由,如果是直接输入URL打开的,这个属性是undefined),这个redirectedFrom属性有时候非常有用,比如在登录页面,用户登录成功后,可以跳转到之前的页面。

冷知识2:用Router4的router.go()可以传递额外的state参数

没错,不仅router.push()router.replace()可以传state,router.go()也可以!不过这个功能可能用得比较少,比如在某些复杂的多步骤表单场景里,用户回到上一步的时候,可能需要传递一些状态信息。

Vue3 Router传参的正确打开姿势

  1. 优先选择路径动态参数传参,用于标识性资源跳转,语义化强,SEO友好。
  2. 其次选择Query传参,用于非标识性、可序列化的筛选/搜索条件,灵活度高,可以分享。
  3. 最后选择State传参,用于临时、复杂、敏感数据,URL不显示,刷新丢失。
  4. 用编程式导航的对象写法时,千万不要同时用path和params属性。
  5. 从route.query里获取的参数都是字符串类型的,记得自己转换。
  6. 如果传复杂类型的Query参数,记得用encodeURIComponent()编码。
  7. 如果想要State参数刷新后也能保留,可以存到localStorage里。

好了,今天关于Vue3 Router传参的内容就聊到这里了,希望对大家有所帮助,如果大家还有其他关于Vue3 Router的问题,欢迎在评论区留言,我们一起讨论。

版权声明

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

热门