Vue3进阶:路由、组件通信与Composition API实战解析
说说我第四天的 Vue 学习进度。前面几天把模板语法、指令、组件基础、生命周期这些地基打完了Day.4 我给自己定的主题是“路由负责页面跳转通信负责组件协作”。说实话没有路由和组件通信撑腰的 Vue 项目做来做去都像一个花哨的单页 Demo真正要搭一个能一个人从头写到尾的完整应用这两块躲不掉也绕不开。这篇内容就是 Day.4 的实操记录包含我踩过的坑和理解适合刚学完组件基础、准备往完整应用进阶的同学参考也适合那些学过一遍但知识点散落、想借着“路由 通信 组合式 API 初体验”这条线重新串一遍的人。我 Day.4 的计划很简单先啃 Vue Router 4理解路由到底解决了什么问题再搞定父子组件之外的通信手段最后初步认识 Composition API 的写法顺手把它接到一个模拟项目 X 里。这个模拟项目 X 就是一个极简的内容管理后台包含登录页、首页、用户列表、用户详情、设置页全部用 Vue 3 Vite 搭的。功能不复杂但覆盖了路由嵌套、动态路由、路由守卫、跨组件传值、组合式逻辑抽取这些核心场景。一天的进度学下来我对 Vue 项目结构的理解比前两天要通透了非常多。1. 第四天为什么要从“页面流”和“数据流”切入1.1 前三天的基础铺垫卡在了一个核心瓶颈上前三天的状态就是“单页组件自嗨”写一个计数器没问题写一个待办列表没问题把父组件的数据传给子组件也没问题但一旦到了“我要做一个多页面应用”的时候我发现自己连入口都不知道从哪下手。问题主要卡在两个点上第一多页面之间的跳转以及跳转时参数的传递整个脑子里没有一套完整的路径设计观念第二深层嵌套的组件需要获取顶层数据时props 链会传递得又臭又长业务还没复杂到那种程度代码已经能闻到坏味道了。Day.4 的课程安排就是专门针对这两个瓶颈设计的先把 Vue Router 啃透再做通信方案选型顺便把 Options API 和 Composition API 做一次横向对比。这个学习顺序安排得非常合理因为路由解决的是应用骨架通信解决的是骨架里的数据流动两者一起打通才能支撑起一个完整的应用。1.2 为什么 Day.4 这批内容的适用场景特别实用Day.4 的知识点并不是炫技而是实战刚需。拿我手头的模拟项目 X 来说登录页跳首页要用路由首页进入某个用户的详情页要动态传 userId未登录用户直接访问后台页面需要被拦截这些需求没有路由是真的实现不了。组件通信也一样用户列表页筛选出结果后要把筛选条件告诉列表组件设置页修改主题后要让全站生效这些场景在 props 单向数据流下会很不顺手需要 provide/inject 或共享状态来补充。这一天的内容学完我最大的感受是它把所有零散的前端知识点串成了一条线相当于从“写组件”进步到了“设计一个小组件系统”。这种认知转变比单纯记住几个 API 要值钱得多。2. 路由核心实操从安装到嵌套路由全部打通2.1 路由安装与初始配置的几个关键细节我用的是 Vite 构建的 Vue 3 项目装路由只需要一条命令npm install vue-router4注意这里必须带4因为 Vue 3 对应的是 Vue Router 4如果你以前不过脑直接复制 Vue 2 的vue-router3大概率会碰到 API 不兼容的问题。安装完之后在src下建一个router文件夹创建index.js我当时的配置长这样import { createRouter, createWebHistory } from vue-router import Home from ../views/Home.vue const routes [ { path: /, name: Home, component: Home }, { path: /login, name: Login, component: () import(../views/Login.vue) } ] const router createRouter({ history: createWebHistory(), routes }) export default router然后回到main.js里让应用感知路由import { createApp } from vue import App from ./App.vue import router from ./router createApp(App).use(router).mount(#app)模板中用到两个核心渲染标签一个是router-link一个是router-view。我有一次犯过很低级的错误router-link写对了但忘记在App.vue里放router-view导致页面始终渲染不出来。这两个标签的分工要记牢前者负责触发导航后者负责渲染当前匹配到的路由组件。提示Vue Router 4 的主要 API 都和 Vue 3 的组合式风格靠拢了比如在组件内用useRouter和useRoute来拿路由实例和当前路由对象而不是像 Vue 2 里那样this.$router这个习惯要尽早转过来。2.2 动态路由和嵌套路由的场景解析动态路由本质上是“路径参数化”。我模拟项目 X 里的用户详情页就是一个典型场景/user/list是用户列表点击某一行要跳到/user/123、/user/456路径路径不同但页面组件是同一个变化的只是里面展示的数据。配置方法是在路由路径里加冒号参数{ path: /user/:id, name: UserDetail, component: () import(../views/UserDetail.vue) }在组件里通过useRoute()读取参数import { useRoute } from vue-router const route useRoute() const userId route.params.id这里有一个非常隐蔽的坑从/user/123跳转到/user/456时UserDetail.vue组件默认会被复用created或onMounted钩子不会重新触发所以页面上的数据不会自动更新。解决思路有两个要么在路由跳转时给router-view加上:key强制重建要么在组件里用watch监听route.params.id。我强烈建议先在项目里跑通watch方案因为它对参数变化的响应更可控也更接近真实业务里“同一组件接收不同参数刷新数据”的交互逻辑。嵌套路由则用来处理“页面中又有子页面区域”的情况。比如后台首页有侧边栏用户列表、设置中心点击任何一个子菜单侧边栏保持不变只有右侧内容区域变化。这时父路由配置children数组父组件模板里再放一个router-view子路由就会渲染到这个位置。新手最常见的困惑是父子路由的path拼接方式其实规则很简单子路由path不以/开头就表示相对路径会自动拼接父路径以/开头就会覆盖父路径变成绝对的顶层路径。我建议统一用不带/开头的相对写法这样结构一眼就看到归属关系。3. 路由守卫给每个页面加上“门禁”逻辑3.1 三种守卫的使用场景我把守卫理解成“路由导航的过程控制器”它可以在导航确认之前、之中、之后执行代码。Day.4 里我重点用到了全局前置守卫router.beforeEach它最典型的场景是登录态校验。模拟项目 X 里我定义了一个requiresAuth的 meta 字段用它标识哪些页面必须登录才能访问{ path: /admin, name: Admin, component: () import(../views/Admin.vue), meta: { requiresAuth: true } }然后在路由实例上挂守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ name: Login }) } else { next() } })这段代码的思路是如果目标路由标记了requiresAuth且本地没有 token那就强制跳回登录页否则放行。这里我踩过一个小坑死循环。一开始我在跳转Login页时也给它设置了requiresAuth: true结果反复被拦截控制台直接报 “Maximum call stack size exceeded”。解决办法就是登录页不要加这个 meta或者在守卫里做一个判断if (to.path /login) return next()。3.2 页面加载顺序与守卫的执行机制搞清楚守卫的执行顺序排查问题的时候会省力不少。Vue Router 4 的导航流程大致是触发导航 - 调用全局前置守卫beforeEach- 如果有路由独享守卫beforeEnter则执行 - 解析异步路由组件 - 调用全局解析守卫beforeResolve- 导航确认 - 调用全局后置钩子afterEach。实操中我主要用beforeEach就覆盖绝大部分需求比如登录校验、页面访问统计、动态设置页面标题等。设置页面标题是一个让人很有成就感的例子监听to.meta.title再配合document.title赋值即可。这个需求让我意识到路由 info 不只是“路径映射”还能承载模块权限、页面标题之类的元数据。把元数据集中在路由配置里统一管理页面组件内部就可以省掉很多“这个页面属于哪个模块”之类的重复判断。4. 组件通信进阶不同场景选择不同的数据通道4.1 props 和 emit最正统的单向数据流路由打通之后Day.4 的后半段回到了组件通信。最基本的肯定是 props 和 emit。props 用于父组件向子组件传递数据emit 用于子组件向父组件抛事件。这个我并不陌生但我在 Day.4 里重新纠正了一个认知props 最好不要传递对象引用并直接修改它应当保持单向数据流。如果确实要改应该由父组件自己改或者子组件emit一个事件让父组件去改。这种风格会让数据变更来源一目了然代码一多的时候排查“谁改了数据”会特别舒服。4.2 provide/inject跨层传递的“直通车”当我遇到“设置页顶部的主题色要影响列表页里的徽章颜色”这种跨多层组件的场景props 链就会拖得很长中间层组件完全不关心这个数据但为了透传也要写一遍 props特别烦人。此时provide和inject是一个优雅的方案。在顶层组件里用provide提供数据import { provide, ref } from vue const themeColor ref(#409EFF) provide(themeColor, themeColor)几层之下的后代组件里用inject接收import { inject } from vue const themeColor inject(themeColor)需要注意的是inject接收到的如果是 ref 对象它天然是响应式的修改它会同步到 provide 方。这个特性让我顿悟provide/inject 的适用场景是“跨层级读共享数据”但滥用它会让组件之间的耦合变得隐晦因为追踪数据来源变得更加困难。我的建议是层级浅用 props跨越好几层且这些中间层不关心数据时再用 provide/inject。4.3 插槽让组件变得“可填空”插槽这项能力我在 Day.4 才认真用起来。模拟项目 X 里有几个结构相似但内容不同的卡片列表我把公共的布局骨架抽成一个容器组件把不同的内容区域用插槽暴露出来。具名插槽可以指定名称比如卡片组件里给标题区和操作区各留一个插槽作用域插槽则允许子组件把内部数据“反向交给”父组件使用父组件可以决定这段 ui 怎么渲染。template div classcard header slot nametitle / /header main slot :useruser / /main /div /template父组件在使用时通过v-slot拿到作用域里的userCard template #title用户卡片/template template #default{ user }{{ user.name }}/template /Card插槽概念难吗不难但它带来的组件设计思路转变特别大用好了组件的复用能力会上一个台阶。4.4 事件总线与共享状态到底该怎么选我还在 Day.4 里测试了mitt一个像 Vue 2 时代 EventBus 一样的事件总线库。它用起来很简单import mitt from mitt; const emitter mitt()任何组件都可以emitter.emit(update, data)发消息别的组价emitter.on(update, (data) ...)接收。这套机制在非父子组件通信中很方便但我想提醒的是事件总线最大的问题是不好排查数据流你很难从代码上直观看出“到底谁发了这个消息谁在听”。小型 Demo 无所谓业务越复杂越要谨慎别让它成为代码里的“隐藏状态”。如果项目到了需要大量共享状态和明确数据流的时候直接上状态管理库比如 Pinia会是更好的选择。Day.4 我先了解到这个层级的选型边界不急着深入因为明天的计划就是 Pinia。Demo 阶段用 mitt 做事件总线能够帮助我理解通信的本质是“发布 - 订阅”这对后续理解状态管理很有帮助。5. Composition API 初体验为什么它让逻辑更“看得见”5.1 Options API 的痛点在业务逻辑变复杂时浮现Day.4 的重头戏之一是初步认识 Vue 3 的 Composition API。Vue 2 时代大家习惯的写法是 Options API也就是把组件拆成data、computed、methods、watch等选项块。短小的组件这么写很清晰但当一个组件里同时管理多个业务模块时问题就来了用户数据的处理逻辑分散在data里、computed里、methods里methods里又有筛选逻辑、又有计数逻辑、又有网络请求逻辑阅读代码时必须在选项块之间来回跳特别是组件第一遍写完后隔几周再回去改时间成本非常高。然后是mixin尝试解决复用问题时带来的命名冲突和隐式依赖问题。Day.4 让我意识到Composition API 是为了从“组织代码的位置”切换到“组织代码的逻辑”本质上是一次更彻底的重构。5.2 setup 函数、ref 和 reactive 的正确上手姿势setup是一个新的组件选项它在组件创建之前执行专门用来组合逻辑。在setup里声明响应式状态时有两种主要选择ref适合基础类型和简单值reactive适合对象和数组。表面上看它们只是 API 差异其实底层都依赖 Vue 3 的 Proxy 响应式机制。我一开始最不习惯的是ref在模板里自动解包而在 script 里必须写.value。后来我用一个口诀记住了模板写名字代码写.value。一段最基础的对比// Options API 风格 export default { data() { return { count: 0, userList: [] } }, methods: { increment() { this.count }, async loadUserList() { const res await fetch(/api/users) this.userList await res.json() } } } // Composition API 风格 import { ref, onMounted } from vue export default { setup() { const count ref(0) const userList ref([]) const increment () { count.value } const loadUserList async () { const res await fetch(/api/users) userList.value await res.json() } onMounted(loadUserList) return { count, userList, increment } } }对比看下来我的体会是Composition API 让这段代码的可阅读性和可复用性都更好了因为你可以把count、increment、userList、loadUserList放在一起逻辑内聚性更强之后抽取自定义 hook 时也非常顺手。但它也有学习曲线需要重新习惯“响应式引用”的思维方式以及setup里所有变量别人都能访问带来的小自由。5.3 把公共逻辑顺手抽成一个自定义 hookDay.4 的收尾练习我把模拟项目 X 里的用户列表请求逻辑单独提出来做成了一个useUserList函数import { ref, onMounted } from vue export function useUserList() { const userList ref([]) const loading ref(false) const fetchUserList async () { loading.value true try { const res await fetch(/api/users) userList.value await res.json() } finally { loading.value false } } onMounted(fetchUserList) return { userList, loading, fetchUserList } }任何组件只需要调用const { userList, loading } useUserList()就能获得完全独立的一套状态。这种方式让逻辑复用不再依赖mixin也没有命名冲突问题。这个小小的重构给我带来的成就感超过前面任何一天因为它让我第一次体会到“逻辑组合”比“继承代码块”更适合现代组件开发。6. 常见问题与排查技巧实录6.1 页面白屏什么也不显示我在 Day.4 里至少碰到三次白屏原因各不相同。第一次是真的忘了在App.vue里放router-view这个好排查看一眼模板就行第二次是路由路径写错了访问/user/123但配置只有/user/:id写成了/user:id这种低级错误其实最容易出现第三次是异步组件 import 路径写错了控制台直接报模块找不到。遇到白屏不用慌固定顺序检查三件事模板渲染出口有没有、路由配置路径对不对、组件导入路径通不通。6.2 点击跳转报错Duplicate navigationVue Router 4 在重复跳转到同一个路由时会发出NavigationDuplicated警告。比如你已经位于/user/123再点一次跳转到/user/123就会触发。它不算致命错误但控制台红彤彤的看着闹心。我初期图省事直接忽略后来发现一个更优雅的办法是写一个简单的封装或者给每个导航判断当前路由是否相同这能顺便让代码更健壮。不过也提醒自己不要过度封装避免学习期的代码反而看不懂了。6.3 同一动态路由参数变化但视图不更新这个前面提过本质上是因为组件复用导致钩子不重新执行。我的解决思路是用watchimport { watch } from vue import { useRoute } from vue-router const route useRoute() watch(() route.params.id, (newId) { fetchUserDetail(newId) })这段代码在很多实战项目里都是标准解法能让页面在路径参数变化时主动更新数据。6.4 provide/inject 修改不同步我在早期写 provide 的时候犯了一个错误只 provide 了一个普通字符串provide(themeColor, #409EFF)结果子组件里改成别的颜色父组件这里的字符串纹丝不动全站一并更新更是无从谈起。后来改了写法提供的是一个ref响应式引用通过修改ref.value才能触发全局更新。这个坑说明provide/inject 虽然使用方便但响应式来源一定要找对否则就会出现“改了不生效”的迷之 bug。6.5 调试工具怎么用才高效Day.4 我还把浏览器调试工具试了一下在 Vue 插件里能清晰看到当前路由匹配到的组件和各组件注入的依赖。排查 provide/inject 问题、确认路由守卫是否按预期执行时这个面板比打印日志直观非常多。我的习惯是任何不理解的响应式行为都先打开调试工具看一遍数据来源而不是无脑加日志。7. 我的最终体会与一个小建议Day.4 的收获比前三天加起来还要“重”因为它终于解决了我从“会写组件”到“会搭页面骨架”之间最大的跨度问题。路由让我理解了单页应用的本质是“根据当前 URL 渲染对应组件”通信让我理解了组件之间的数据不能全靠复制粘贴渗透Composition API 让我看到了 Vue 3 更现代化的组织思路。我踩过的坑并不高级基本都是基础细节但正因为亲手把它们踩出来了对这些知识的记忆才会特别深。最后分享一个小技巧学路由和组件通信的时候完全可以不依赖正式的后端接口先用本地 JSON 数据或者 Mock 数据把页面流和数据流串起来只要“跳转能通、参数能传、数据能改”项目的骨架就算立住了。我模拟项目 X 就是这么跑的等到第五天接入真正的状态管理时这个“骨架响应式”的底子会让我脚步稳很多。