嵌套路由与编程式导航
Vue Router 4 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
多页应用的路由结构很少是平的。这一节掌握嵌套路由(父布局 + 子页面)、命名视图、编程式导航的三种形态(push/replace/go),以及查询参数与路由状态的管理。
问题场景
后台管理页:左侧菜单固定,右侧内容随路由切换。用平铺路由的话,每个页面都要自己渲染侧边栏,切换时侧边栏跟着重建、滚动位置丢失。另一个问题:筛选条件(页码、关键词)存在组件状态里,刷新就丢,用户无法分享「第 2 页搜 vue」的链接。
核心解释
嵌套路由用 children 表达「父布局 + 子页面」:父路由给 /admin 配布局组件,children 里配 ""(默认子页)、"users"、"settings" 三个子路由。子路由的 path 不写前导斜杠,拼出来是 /admin/users。
AdminLayout.vue 里必须有 <RouterView />,子路由才渲染得出来:
<!-- layouts/AdminLayout.vue -->
<script setup>
import { RouterLink, RouterView } from "vue-router";
</script>
<template>
<div class="admin">
<aside>
<RouterLink to="/admin">概览</RouterLink>
<RouterLink to="/admin/users">用户</RouterLink>
<RouterLink to="/admin/settings">设置</RouterLink>
</aside>
<section>
<RouterView />
</section>
</div>
</template>
注意子路由的 path 不写前导斜杠("users" 而不是 "/users"),拼出来是 /admin/users;写 "/users" 会变成绝对路径,脱离父布局。空字符串 "" 是父路径的默认子页。
编程式导航三种形态:
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
function goDetail(id) {
router.push(`/courses/${id}`); // 新增历史记录
}
function replaceFilter(keyword) {
router.replace({ query: { q: keyword } }); // 替换当前记录,后退不会回到旧筛选
}
function goBack() {
router.go(-1); // 等价于 history.back()
}
</script>
查询参数与路由状态:筛选条件放 route.query,组件用 computed 派生,URL 就是唯一状态源:
<script setup>
import { computed, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
const route = useRoute();
const router = useRouter();
const page = computed(() => Number(route.query.page ?? 1));
const keyword = computed(() => String(route.query.q ?? ""));
function setPage(next) {
router.push({ query: { ...route.query, page: next } });
}
</script>
刷新、分享、前进后退都自动正确,因为状态在 URL 里。副作用(请求数据)用 watch([page, keyword], fetch) 触发。
命名视图:一个页面同时渲染多个组件(比如侧栏 + 主区)时,在路由上用 components 对象配 <RouterView name="sidebar">,适合布局差异大的页面,日常用得少。
示例拆解
把「列表页筛选」做成 URL 驱动,验证三个行为:刷新保留、分享链接正确、后退回到上一组筛选:
<!-- views/CoursesView.vue -->
<script setup>
import { computed, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
const route = useRoute();
const router = useRouter();
const keyword = ref(String(route.query.q ?? ""));
const track = computed(() => String(route.query.track ?? "all"));
watch(
() => route.query,
(q) => {
keyword.value = String(q.q ?? "");
}
);
function applyFilter() {
router.push({
query: {
...(keyword.value ? { q: keyword.value } : {}),
...(track.value !== "all" ? { track: track.value } : {}),
},
});
}
</script>
<template>
<div>
<input v-model="keyword" placeholder="搜索课程" @keyup.enter="applyFilter" />
<button @click="applyFilter">筛选</button>
<p>当前:q={{ route.query.q ?? "无" }},track={{ track }}</p>
</div>
</template>
注意 applyFilter 里对空值做了「不写入 query」处理,URL 才不会出现 ?q=&track=all 这种噪音。侦听 route.query 时同步回输入框,保证「后退」后界面和 URL 一致。
动手任务
- 建一个
/admin布局 + 三个子页,验证切换子页时侧边栏不重建(用console.log在onMounted里观察)。 - 给列表页加「分页」,页码写进
query.page,验证刷新后仍停在第 2 页。 - 用
router.replace实现搜索框实时筛选(不产生历史记录),对比push版本的后退行为。
常见错误
- 子路由 path 写了前导斜杠:变成绝对路径,父布局被跳过。
- 父组件忘了
<RouterView />:子路由渲染不出来,页面空白。 - 筛选状态只放组件 ref:刷新丢、分享丢、后退不同步。放 query。
router.push传了同一个路径:Vue Router 会报「Avoided redundant navigation」并拒绝导航(开发环境警告)。比较后再决定是否导航,或捕获错误。
检查与复盘
- 能写出父子嵌套路由 + 布局组件
- 能区分 push / replace / go 三种导航的语义
- 能说出「URL 作为筛选状态源」的三个好处
参考资料
- Vue Router 官方文档「嵌套路由」:children 与 RouterView
- Vue Router 官方文档「编程式导航」:push/replace/go 与 query
读完这一课了?
标记完成后,工作台会安排接下来的学习。