一、前言
详情页是所有应用的基本盘:商品详情、用户主页、订单明细------URL 里必须带"这一条数据是谁"的信息。本篇讲透三种传参方式:路径参数(:id)、查询参数(query)、props 解耦,以及新手必踩的"参数变了页面不刷新"大坑。
三种传参先混个脸熟
text
/user/1 → 路径参数:params.id = '1'(RESTful 风格,详情页标配)
/user?id=1 → 查询参数:query.id = '1'(列表筛选、搜索词)
props 解耦 → 前两种的"接收方式升级",组件不依赖 useRoute
二、路径参数:动态路由 :id
路由表里用 :参数名 声明"这个位置是可变的":
ts
// src/router/index.ts
const routes = [
{
path: '/user/:id', // :id 是动态段------/user/1、/user/2、/user/abc 都匹配
name: 'user',
component: () => import('@/views/UserView.vue')
}
]
vue
<!-- src/views/UserView.vue -->
<template>
<h2>用户详情页</h2>
<p>URL 是:{{ route.fullPath }}</p>
<p>params.id = {{ route.params.id }}</p>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router'
const route = useRoute()
// 注意类型:params 的值是 string | string[],不是 number!
const id = route.params.id
console.log(typeof id) // string
</script>
访问 /user/42 → route.params.id 得到字符串 '42'。要数字自己转:
ts
const id = Number(route.params.id)
if (Number.isNaN(id)) {
// 非法参数处理:跳 404 或提示
}
可选参数 用 ?:path: '/user/:id?'------/user 和 /user/1 都能匹配,用时记得判空。
【截图位置:访问 /user/42 显示参数的运行效果】
三、大坑登场:参数变了,页面却不刷新
复现现场:在详情页放一个"看下一位用户"的按钮:
vue
<template>
<p>当前用户:{{ route.params.id }}</p>
<!-- 模板里 route.params 是响应式的,这里会更新 -->
<RouterLink :to="`/user/${Number(route.params.id) + 1}`">下一位 →</RouterLink>
</template>
问题来了:如果页面数据是"进页面时用 onMounted 拉取的",点击链接 URL 变了,数据却不动。
原因:/user/1 → /user/2 匹配的是同一个路由 ,Vue Router 出于性能默认复用组件实例------组件不会重新创建,onMounted 不再执行(011 篇生命周期知识联动)。
解法 1:watch 监听参数(推荐,最常用)
ts
import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
// 进页面执行一次 + 参数变了再执行(immediate)
watch(
() => route.params.id,
(newId) => {
if (newId) loadData(Number(newId))
},
{ immediate: true }
)
function loadData(id: number) {
console.log('加载用户', id, '的数据')
}
解法 2:router-view 上加 key,强制重建组件(简单粗暴)
vue
<!-- App.vue -->
<RouterView :key="route.fullPath" />
URL 一变组件销毁重建,onMounted 重新执行------代价是整个页面全部重渲染,页面轻可用,页面重慎用。默认学解法 1。
四、query 查询参数:筛选与搜索的地盘
query 不需要路由表声明,拼在 URL 后面就能读:
ts
// 跳转:/search?kw=vue3&page=2
router.push({ name: 'search', query: { kw: 'vue3', page: 2 } })
// 读取(045 篇的 search 页)
const kw = route.query.kw // 'vue3',类型 LocationQueryValue = string | null
const page = Number(route.query.page ?? 1)
params vs query 对比表:
| params(路径参数) | query(查询参数) | |
|---|---|---|
| URL 长相 | /user/1 | /user?id=1 |
| 路由表 | 必须声明 :id | 不需要声明 |
| 语义 | 这条资源的"身份" | 查询条件/视图状态 |
| 典型场景 | 详情页 ID | 搜索词、页码、筛选条件 |
| 刷新后 | 保留(在 URL 里) | 保留(在 URL 里) |
记忆:身份进路径,条件进 query。
五、props 解耦:组件不再依赖 useRoute
问题:UserView 里到处 route.params.id,组件和路由器耦合死了,想单独复用/测试很难。路由配置提供 props 选项,把参数变成正经 props:
ts
// 路由配置加 props: true
{
path: '/user/:id',
name: 'user',
component: () => import('@/views/UserView.vue'),
props: true // 把 params 作为 props 传给组件
}
vue
<!-- UserView.vue:改用 props 接收,和普通组件毫无区别 -->
<template>
<p>用户 ID:{{ id }}(类型 {{ typeof id }})</p>
</template>
<script setup lang="ts">
// 不再需要 useRoute!
defineProps<{ id: string }>()
</script>
props 三种模式:
ts
props: true // 布尔模式:params 全量转 props
props: { id: 123 } // 对象模式:静态值(少用)
props: (route) => ({ id: Number(route.params.id) }) // 函数模式:自由加工(转数字常用)
函数模式是实战首选------顺手把字符串转成数字,组件拿到的就是干净的 number:
vue
<script setup lang="ts">
defineProps<{ id: number }>() // 函数模式加工后,直接是数字
</script>
六、综合实战:用户列表 → 详情 → 参数联动
三个文件走通全链路:
ts
// src/router/index.ts 追加
{
path: '/users',
name: 'users',
component: () => import('@/views/UserListView.vue')
},
{
path: '/user/:id',
name: 'user',
component: () => import('@/views/UserDetailView.vue'),
props: (route) => ({ id: Number(route.params.id) })
}
vue
<!-- src/views/UserListView.vue:列表 + 声明式跳转 -->
<template>
<h2>用户列表</h2>
<ul>
<li v-for="u in users" :key="u.id">
<RouterLink :to="{ name: 'user', params: { id: u.id } }">
{{ u.name }}
</RouterLink>
</li>
</ul>
</template>
<script setup lang="ts">
const users = [
{ id: 1, name: '小明' },
{ id: 2, name: '小红' },
{ id: 3, name: '小刚' }
]
</script>
vue
<!-- src/views/UserDetailView.vue:props 接参 + watch 响应参数变化 -->
<template>
<h2>用户详情:ID = {{ id }}</h2>
<p>模拟数据:{{ detail }}</p>
<button @click="next">下一位用户 →(参数变化,页面不重建)</button>
<RouterLink to="/users">← 回列表</RouterLink>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'
// 函数模式加工过,id 是干净的 number
const props = defineProps<{ id: number }>()
const router = useRouter()
const detail = ref('')
function load(uid: number) {
// 模拟请求详情
detail.value = `这是用户 ${uid} 的详细信息`
}
// 组件复用不重建 → 用 watch 响应参数变化(解法 1)
watch(() => props.id, load, { immediate: true })
function next() {
router.push({ name: 'user', params: { id: props.id + 1 } })
}
</script>
全链路品味:列表按 name 跳转带 params → 详情页函数模式把 id 加工成 number → watch immediate 首次加载 + 参数变化重载。
【截图位置:列表进详情、点"下一位"数据联动更新的运行效果】
七、踩坑记录
- params 是字符串 :
route.params.id + 1得到'11'(字符串拼接)------Number() 转换或用 props 函数模式加工 - 同路由参数变化组件不重建:onMounted 的数据请求只跑一次------watch 参数(推荐)或 RouterView 加 key(粗暴)
- query 值可能是数组 :同名参数出现多次时
?a=1&a=2,route.query.a 变数组------读取时统一处理成数组或取第一个 - 可选参数忘了判空 :
:id?匹配 /user 时 params.id 是 '' 或 undefined------使用前判断 - props: true 后组件里还去 useRoute:能跑但白解耦了------选了 props 模式就贯彻到底
- 404 误伤:/user/abc 也会匹配 :id------非法值在页面里判断后跳转 404(052 篇配 catchAll)
八、今日小结
- 动态路由
:id声明动态段,params 取值是字符串要转数字 - 同路由参数变化默认复用组件:watch 参数(immediate)是标准解法
- params 管身份、query 管条件;name 配 params、path 配 query
- props 解耦三模式,函数模式最实用(顺手转数字)
下篇预告
后台管理系统长什么样?顶部栏 + 侧边菜单 + 内容区,菜单里还能再开子页面。下一篇 048 嵌套路由与命名视图------children 配置搭出后台布局雏形。