Vue.js路由参数

路由参数是 Vue Router 中非常重要的功能,它允许我们根据不同的参数动态匹配路由,实现灵活的路由配置。Vue Router 支持多种类型的参数,包括路径参数、查询参数和哈希参数。

路由参数概述

Vue Router 主要支持三种类型的参数:

参数类型 URL示例 访问方式 特点
路径参数 (Params) /user/123 route.params.id 路径的一部分,必需参数
查询参数 (Query) /search?q=vue&page=1 route.query.q 可选的键值对参数
哈希参数 (Hash) /about#section-2 route.hash 页面内锚点定位
参数选择建议
  • 使用路径参数:当参数是资源标识符时(如用户ID、文章ID)
  • 使用查询参数:当参数是可选的或用于筛选、分页时
  • 使用哈希参数:当需要在页面内定位到特定位置时

路径参数 (Path Params)

路径参数是 URL 路径的一部分,通常用于标识特定资源。

基本语法

# 基本动态路由
'/user/:id' // 匹配 /user/123, /user/abc

# 多个参数
'/user/:userId/post/:postId'

# 可选参数
'/archive/:year?/:month?/:day?'

# 正则约束
'/user/:id(\\d+)' // 只匹配数字
'/user/:username([a-z-]+)' // 只匹配小写字母和连字符

# 重复参数
'/tags/:tags+' // 一个或多个,如 /tags/vue/router
'/sections/:sections*' // 零个或多个

配置动态路由

示例动态路由配置

// router/index.js
const routes = [
  // 基本动态路由
  {
    path: '/user/:id',
    name: 'user',
    component: () => import('@/views/User.vue')
  },

  // 多个参数
  {
    path: '/category/:categoryId/product/:productId',
    name: 'product',
    component: () => import('@/views/Product.vue')
  },

  // 可选参数
  {
    path: '/blog/:year?/:month?/:day?',
    name: 'blog',
    component: () => import('@/views/Blog.vue'),
    // 当访问 /blog 时,year/month/day 都是 undefined
  },

  // 带正则约束的参数
  {
    path: '/user/:id(\\d+)',  // 只匹配数字ID
    name: 'userById',
    component: () => import('@/views/UserById.vue')
  },
  {
    path: '/file/:filename(.+)?',  // 匹配任意文件名,包括扩展名
    name: 'file',
    component: () => import('@/views/File.vue')
  },

  // 重复参数
  {
    path: '/tags/:tags+',
    name: 'tags',
    component: () => import('@/views/Tags.vue')
    // 匹配 /tags/vue, /tags/vue/router, /tags/vue/router/javascript
  },

  // 混合使用
  {
    path: '/search/:type/:query+',
    name: 'search',
    component: () => import('@/views/Search.vue')
    // 匹配 /search/articles/vue, /search/users/john/doe
  }
]
                        

访问路径参数

示例在组件中访问路径参数

<!-- User.vue -->
<template>
  <div>
    <h2>用户详情</h2>

    <!-- Options API -->
    <div v-if="userId">
      <p>用户ID: {{ userId }}</p>
      <p>来自 $route: {{ $route.params.id }}</p>
    </div>

    <!-- Composition API -->
    <div v-if="composUserId">
      <p>Composition API 用户ID: {{ composUserId }}</p>
    </div>

    <!-- 多个参数示例 -->
    <div v-if="$route.params.categoryId">
      <p>分类ID: {{ $route.params.categoryId }}</p>
      <p>产品ID: {{ $route.params.productId }}</p>
    </div>

    <!-- 重复参数示例 -->
    <div v-if="$route.params.tags">
      <h3>标签:</h3>
      <ul>
        <li v-for="tag in tagsArray" :key="tag">
          {{ tag }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
// Options API
export default {
  name: 'User',
  computed: {
    userId() {
      // 访问路径参数
      return this.$route.params.id
    },
    tagsArray() {
      // 处理重复参数(tags+ 会将多个标签合并为字符串)
      const tags = this.$route.params.tags
      return tags ? tags.split('/') : []
    }
  },
  created() {
    // 在生命周期中访问参数
    console.log('用户ID:', this.$route.params.id)
    console.log('完整参数对象:', this.$route.params)

    // 根据参数加载数据
    this.loadUserData(this.$route.params.id)
  },
  methods: {
    loadUserData(userId) {
      // 根据用户ID加载数据
      console.log('加载用户数据:', userId)
    }
  },
  // 监听参数变化(当同一组件复用时)
  watch: {
    '$route.params.id'(newId, oldId) {
      if (newId !== oldId) {
        console.log('用户ID变化:', oldId, '→', newId)
        this.loadUserData(newId)
      }
    }
  }
}
</script>

<!-- Composition API -->
<script setup>
import { computed, watch } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()

// 访问路径参数
const composUserId = computed(() => route.params.id)

// 监听参数变化
watch(
  () => route.params.id,
  (newId, oldId) => {
    if (newId !== oldId) {
      console.log('用户ID变化 (Composition):', oldId, '→', newId)
      loadUserData(newId)
    }
  }
)

// 立即执行一次
watch(
  () => route.params.id,
  (newId) => {
    if (newId) {
      loadUserData(newId)
    }
  },
  { immediate: true }
)

function loadUserData(userId) {
  // 加载用户数据
  console.log('加载用户数据 (Composition):', userId)
}
</script>
                        

查询参数 (Query Parameters)

查询参数是 URL 中 ? 后面的键值对,用于传递可选参数。

URL 结构

https://example.com/search?q=vue&page=1&sort=relevance&limit=20
↑ 查询参数开始
q=vue // 第一个参数
&page=1 // 第二个参数(以 & 分隔)
&sort=relevance&limit=20 // 更多参数

传递查询参数

示例传递查询参数的各种方式

// 1. 在模板中使用 router-link
<router-link :to="{
  path: '/search',
  query: { q: 'vue', page: 1, sort: 'relevance' }
}">
  搜索 Vue
</router-link>

// 2. 编程式导航
import { useRouter } from 'vue-router'
const router = useRouter()

// 使用 path + query
router.push({
  path: '/search',
  query: {
    q: 'vue',
    page: 1,
    sort: 'relevance',
    filter: ['tag1', 'tag2'] // 数组参数会自动编码
  }
})

// 使用 name + query
router.push({
  name: 'search',
  query: {
    q: 'vue',
    page: 1
  }
})

// 3. 字符串形式(不推荐,容易出错)
router.push('/search?q=vue&page=1&sort=relevance')

// 4. 替换当前路由(不添加历史记录)
router.replace({
  path: '/search',
  query: { q: 'vue' }
})

// 5. 更新当前路由的查询参数(保持其他参数不变)
function updateQueryParam(key, value) {
  router.push({
    query: {
      ...router.currentRoute.value.query, // 保留现有参数
      [key]: value // 更新或添加新参数
    }
  })
}

// 6. 删除查询参数
function removeQueryParam(key) {
  const query = { ...router.currentRoute.value.query }
  delete query[key]
  router.push({ query })
}

// 7. 带查询参数的命名路由
<router-link :to="{
  name: 'user',
  params: { id: 123 },
  query: { tab: 'profile', view: 'compact' }
}">
  用户资料
</router-link>
                        

访问查询参数

示例在组件中访问查询参数

<!-- Search.vue -->
<template>
  <div>
    <h2>搜索结果</h2>

    <!-- 显示查询参数 -->
    <div class="mb-3">
      <p>搜索关键词: <strong>{{ searchQuery }}</strong></p>
      <p>当前页码: <strong>{{ currentPage }}</strong></p>
      <p>排序方式: <strong>{{ sortBy }}</strong></p>

      <!-- 显示所有查询参数 -->
      <div v-if="Object.keys(allQueryParams).length">
        <h4>所有查询参数:</h4>
        <ul>
          <li v-for="(value, key) in allQueryParams" :key="key">
            {{ key }}: {{ value }}
          </li>
        </ul>
      </div>
    </div>

    <!-- 分页控件 -->
    <div class="pagination">
      <button
        @click="goToPage(currentPage - 1)"
        :disabled="currentPage <= 1"
      >
        上一页
      </button>

      <span>第 {{ currentPage }} 页</span>

      <button @click="goToPage(currentPage + 1)">
        下一页
      </button>
    </div>
  </div>
</template>

<script>
// Options API
export default {
  name: 'Search',
  computed: {
    // 访问查询参数
    searchQuery() {
      return this.$route.query.q || ''
    },
    currentPage() {
      const page = parseInt(this.$route.query.page) || 1
      return Math.max(1, page) // 确保页码不小于1
    },
    sortBy() {
      return this.$route.query.sort || 'relevance'
    },
    allQueryParams() {
      return this.$route.query
    }
  },
  created() {
    // 初始加载数据
    this.performSearch()
  },
  watch: {
    // 监听查询参数变化
    '$route.query': {
      handler(newQuery, oldQuery) {
        console.log('查询参数变化:', oldQuery, '→', newQuery)
        this.performSearch()
      },
      deep: true // 深度监听,因为 query 是对象
    }
  },
  methods: {
    performSearch() {
      const { q, page, sort } = this.$route.query
      console.log('执行搜索:', { q, page, sort })
      // 调用 API 搜索...
    },
    goToPage(page) {
      // 更新查询参数,触发重新搜索
      this.$router.push({
        query: {
          ...this.$route.query,
          page: Math.max(1, page) // 确保页码有效
        }
      })
    },
    updateSort(sortType) {
      this.$router.push({
        query: {
          ...this.$route.query,
          sort: sortType
        }
      })
    }
  }
}
</script>

<!-- Composition API -->
<script setup>
import { computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()

// 访问查询参数
const searchQuery = computed(() => route.query.q || '')
const currentPage = computed(() => {
  const page = parseInt(route.query.page) || 1
  return Math.max(1, page)
})
const sortBy = computed(() => route.query.sort || 'relevance')
const allQueryParams = computed(() => route.query)

// 监听查询参数变化
watch(
  () => route.query,
  (newQuery, oldQuery) => {
    console.log('查询参数变化 (Composition):', oldQuery, '→', newQuery)
    performSearch()
  },
  { deep: true }
)

// 立即执行一次
watch(
  () => route.query,
  () => performSearch(),
  { immediate: true }
)

function performSearch() {
  const { q, page, sort } = route.query
  console.log('执行搜索 (Composition):', { q, page, sort })
  // 调用 API 搜索...
}

function goToPage(page) {
  router.push({
    query: {
      ...route.query,
      page: Math.max(1, page)
    }
  })
}

function updateSort(sortType) {
  router.push({
    query: {
      ...route.query,
      sort: sortType
    }
  })
}
</script>
                        

哈希参数 (Hash Parameters)

哈希参数是 URL 中 # 后面的部分,通常用于页面内锚点定位。

示例哈希参数的使用

// 1. 导航到带哈希的URL
router.push({
  path: '/documentation',
  hash: '#getting-started' // 定位到"快速开始"章节
})

// 或简写为
router.push('/documentation#getting-started')

// 2. 在模板中使用
<router-link :to="{ path: '/about', hash: '#team' }">
  关于我们 - 团队介绍
</router-link>

// 3. 访问哈希参数
import { useRoute } from 'vue-router'
const route = useRoute()

console.log('当前哈希:', route.hash) // "#getting-started"
console.log('不带#的哈希:', route.hash.slice(1)) // "getting-started"

// 4. 监听哈希变化
watch(
  () => route.hash,
  (newHash) => {
    if (newHash) {
      // 滚动到对应元素
      const elementId = newHash.slice(1)
      const element = document.getElementById(elementId)
      if (element) {
        element.scrollIntoView({ behavior: 'smooth' })
      }
    }
  }
)

// 5. 更新哈希(不触发页面重新加载)
function updateHash(newHash) {
  router.push({ hash: newHash })
}

// 6. 移除哈希
function removeHash() {
  // 方法1: 导航到不带哈希的相同路径
  router.push({ path: route.path })

  // 方法2: 使用 replace 避免历史记录
  router.replace({ hash: null })
}
                        

参数验证和转换

在实际应用中,经常需要对路由参数进行验证和类型转换。

示例参数验证和类型转换

// 1. 路由配置中的验证
const routes = [
  {
    path: '/user/:id',
    component: User,
    beforeEnter: (to, from, next) => {
      const id = to.params.id

      // 验证用户ID
      if (!isValidUserId(id)) {
        // 重定向到404或错误页面
        next({ name: 'not-found' })
        return
      }

      // 验证通过
      next()
    }
  }
]

function isValidUserId(id) {
  // 验证用户ID格式
  return /^\d+$/.test(id) && parseInt(id) > 0
}

// 2. 在组件中进行参数验证和转换
export default {
  computed: {
    // 验证并转换参数
    validatedUserId() {
      const id = this.$route.params.id

      // 检查是否存在
      if (!id) {
        return null
      }

      // 转换为数字
      const numericId = parseInt(id)

      // 验证是否为有效数字
      if (isNaN(numericId) || numericId <= 0) {
        console.error('无效的用户ID:', id)
        return null
      }

      return numericId
    },

    // 安全的查询参数获取
    safePageNumber() {
      const page = this.$route.query.page
      if (!page) return 1

      const num = parseInt(page)
      return isNaN(num) || num < 1 ? 1 : num
    },

    // 数组参数处理
    tagArray() {
      const tags = this.$route.query.tags
      if (!tags) return []

      // 处理字符串形式的数组参数
      if (typeof tags === 'string') {
        return tags.split(',').filter(tag => tag.trim())
      }

      // 处理数组形式的参数
      if (Array.isArray(tags)) {
        return tags.filter(tag => tag && tag.trim())
      }

      return []
    }
  },

  watch: {
    validatedUserId(newId, oldId) {
      if (newId && newId !== oldId) {
        this.loadUserData(newId)
      }
    }
  },

  created() {
    // 创建时验证参数
    if (!this.validatedUserId) {
      this.$router.replace({ name: 'not-found' })
    }
  }
}

// 3. 使用 props 进行参数验证和传递
const routes = [
  {
    path: '/user/:id',
    component: User,
    props: (route) => ({
      // 在路由级别进行验证和转换
      id: validateAndConvertId(route.params.id),
      // 传递查询参数作为 props
      search: route.query.q || '',
      page: parseInt(route.query.page) || 1
    })
  }
]

function validateAndConvertId(id) {
  const num = parseInt(id)
  return isNaN(num) || num <= 0 ? null : num
}

// 4. 在组件中接收验证后的 props
export default {
  props: {
    id: {
      type: Number,
      required: true,
      validator: (value) => value > 0
    },
    search: {
      type: String,
      default: ''
    },
    page: {
      type: Number,
      default: 1,
      validator: (value) => value >= 1
    }
  },

  created() {
    // props 已经经过验证
    console.log('用户ID:', this.id)
    console.log('搜索词:', this.search)
    console.log('页码:', this.page)

    if (!this.id) {
      // 如果id无效(由于验证器,这种情况不会发生)
      this.$router.replace({ name: 'not-found' })
    }
  }
}
                        

参数传递模式对比

特性 路径参数 (Params) 查询参数 (Query) 哈希参数 (Hash)
URL中的位置 /path/:param /path?key=value /path#section
是否必需 通常是必需的(可选参数除外) 可选 可选
SEO影响 影响大,每个参数都是独立页面 影响较小,被视为同一页面的不同状态 无影响
浏览器历史 每次变化都会创建新历史记录 每次变化都会创建新历史记录 通常不会创建新历史记录
典型用途 资源标识符(用户ID、文章ID) 筛选、排序、分页参数 页面内锚点定位
可传递的数据类型 字符串 字符串、数字、数组、对象(需编码) 字符串
数据量限制 受URL长度限制 受URL长度限制 受URL长度限制
安全性 URL中可见 URL中可见 URL中可见

参数传递演示

路由参数演示

尝试不同的参数传递方式:

路径参数演示:
查询参数演示:
哈希参数演示:
混合参数演示:
当前路由信息:

等待演示...

点击上方按钮查看不同参数传递方式的效果

路由参数最佳实践

路由参数最佳实践
  • 参数验证:始终验证路由参数的有效性
  • 类型转换:及时将字符串参数转换为需要的类型
  • 参数清理:清理和规范化参数值
  • 监听变化:正确监听参数变化并重新加载数据
  • 错误处理:处理无效参数并提供友好的用户反馈
  • 使用 props:通过 props 传递参数,提高组件可复用性
  • URL设计:设计简洁、语义化的URL结构
  • 参数编码:对特殊字符进行正确的URL编码
常见问题
  • 参数未定义:访问不存在的参数会返回 undefined
  • 类型错误:查询参数始终是字符串,需要手动转换
  • 编码问题:特殊字符需要正确编码/解码
  • 数组参数:处理数组参数时要注意格式
  • 参数持久化:刷新页面时参数会丢失,需要合理处理

总结

Vue Router 的路由参数功能强大且灵活,通过合理使用不同类型的参数,可以构建出用户体验良好、结构清晰的单页面应用。

关键要点回顾:

  • 路径参数:用于标识资源,是URL路径的一部分
  • 查询参数:用于传递可选参数,适合筛选、排序、分页等场景
  • 哈希参数:用于页面内定位,不会触发页面重新加载
  • 参数访问:通过 $route.params$route.query$route.hash 访问
  • 参数监听:使用 watch 监听参数变化,及时更新组件状态
  • 参数验证:始终验证参数的有效性,确保应用稳定性

掌握路由参数的使用技巧,能够让你在开发 Vue.js 应用时更加得心应手。

测试你的理解

思考题
  1. 路径参数和查询参数在什么时候使用?它们有什么主要区别?
  2. 如何在组件中监听路由参数的变化?
  3. 如果需要在同一组件内响应参数变化并重新加载数据,应该怎么做?
  4. 如何验证和转换路由参数?
  5. 使用 props 传递路由参数有什么好处?