ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

前端路由详解:嵌套路由、编程式导航与传参的工程实践

前端路由详解:嵌套路由、编程式导航与传参的工程实践 1. 路由嵌套到底在管理什么层级、配置与视图出口去年我接手一个后台管理项目时最先动手改的不是业务代码而是路由文件。原因很简单当时的项目把所有页面全部拍平成顶层路由每个二级页面里都手写了一遍侧边栏和顶部栏后来产品要加一个统一的底部操作条我光是改公共布局就改吐了。这种痛感让我意识到路由嵌套不是一个可选项而是中后台项目的刚需。1.1 嵌套路由的本质是UI 嵌套而非路径拼接很多初学者理解嵌套路由时容易把它简化成路径前面加了一截父路径。比如访问/user/profile就以为只是/user加/profile的字符串拼接。其实嵌套路由真正解决的是页面组件之间的父子渲染关系。以 Vue Router 为例一个典型的嵌套路由配置长这样const routes [ { path: /user, component: UserLayout, // 父组件 children: [ { path: profile, // 注意这里是相对路径不用加斜杠 component: UserProfile }, { path: settings, // 最终完整路径是 /user/settings component: UserSettings } ] } ]这个时候UserLayout组件内部必须放一个router-view /子路由组件才会渲染在父组件划定的区域内。如果漏掉这个出口页面会直接白屏而且报错信息还比较隐晦只会提示找不到匹配的组件渲染位置。这类问题我在排查同事代码时见过不止一次。这里有一个非常关键的路径细节children里如果写成绝对路径/profile最终 URL 就是/profile而不是/user/profile。很多人在配置嵌套路由时踩过这个坑症状是页面能打开但 URL 看着很怪面包屑也衔接不上。我的习惯是凡是 children 里的路径一律不写前导斜杠让子路径天然继承父路径心智负担最小。1.2 嵌套路由里的重定向与默认视图嵌套路由通常还需要处理一个问题访问父级路径/user时期望默认渲染某个子页面。常见做法是用redirectconst routes [ { path: /user, component: UserLayout, redirect: /user/profile, children: [ { path: profile, component: UserProfile }, { path: settings, component: UserSettings } ] } ]这样访问/user会自动跳到/user/profile。但某些场景下你并不想改变 URL只是想默认显示一个组件那就直接用空路径children: [ { path: , component: UserDashboard }, { path: profile, component: UserProfile } ]空路由和无重定向的区别说到底是URL 是否变化的区别。要不要保留/user这个 URL取决于你的业务语义。一般后台管理里我偏好直接用重定向因为菜单高亮、面包屑、权限判断都依赖清晰的 URL 路径。1.3 嵌套路由 vs 扁平路由选型不该靠感觉有些场景真的不必用嵌套路由。如果每个页面都是独立全屏页比如登录页、404页、注册页硬套嵌套反而增加层级排查问题时多绕一层。对比维度嵌套路由扁平路由UI 公共区域父组件统一承载每页自行引入URL 层级与 UI 层级强相关只表达页面本身配置复杂度中高需管理 children低直观适用场景后台、多标签页、分组模块独立页面、轻量活动页我个人的判断标准很简单如果多个页面共享同一个壳子侧边栏、顶栏、底部栏且壳子里的内容会动态切换那就该用嵌套路由如果壳子本身也是页面的一部分、或不同页面壳子长得不一样那就老老实实拍平。嵌套路由的核心价值是复用而不是为了层级好看。2. 编程式路由与导航式路由一个给用户一个给代码我经常在团队里被人问跳转页面到底用router-link还是router.push两者有什么区别这个问题表面上很简单但真要解释清楚其实涉及一个核心认知谁在发起跳转。2.1 导航式路由让用户的手指来决定先说导航式路由。在 Vue Router 里它就是router-link组件在 React Router 里对应的Link小程序里则是navigator组件。它们有一个共同特点跳转行为是直接在模板里声明好的渲染出来就是一个可点击的链接。router-link to/user/profile个人中心/router-link router-link :to{ name: UserProfile, query: { from: menu } } 个人中心带参数 /router-link导航式路由的优势是直观、语义化浏览器原生支持中键新标签打开、SEO 爬虫抓取等能力也天然保留。点击一个router-link本质上跟点击一个a标签没什么区别只是框架帮你拦截了默认行为改走前端路由逻辑。所以在菜单、列表、卡片这类天生就是让用户去点的场景我优先用导航式路由。2.2 编程式路由跳转之间的业务逻辑但如果你不是用户点击后跳转而是一段代码执行成功后跳转比如表单提交成功、登录鉴权通过、定时跳转这时候导航式路由就力不从心了。你总不能让用户去点一个藏在逻辑后面的链接这时候就得用编程式路由async function handleSubmit() { const success await submitForm(data) if (success) { router.push({ name: OrderDetail, params: { id: orderId } }) } else { router.push(/error) } }编程式路由把跳转动作变成了一次可控的方法调用让你可以放在任意 JS 逻辑里。它可以出现在事件回调、异步请求之后、条件判断分支中也可以被抽成独立的跳转工具函数。从职责上看导航式路由描述的是 UI 层面的可跳转编程式路由描述的是逻辑层面的去跳转。这里顺带提一句React Router 的对应写法是useNavigate()返回的navigate函数小程序则是wx.navigateTo。概念都一样换框架只是换 API 名字。2.3 两者的本质区别与选择逻辑维度导航式路由编程式路由使用位置模板/JSX 中任意 JS 逻辑中触发主体用户点击代码事件、流程控制可读性高UI 结构一目了然中需跟踪逻辑灵活性低高SEO 友好好一般典型场景菜单、列表、链接提交后跳转、登录跳转我个人的选型口诀是页面上的入口用导航式逻辑里的出口用编程式。如果你写一个导航菜单每个菜单项都是一个router-link这是最自然的但如果菜单的点击行为需要先做权限判断再决定跳到哪那这个菜单项反而不该用router-link得用按钮加 onClick逻辑里再调用编程式路由。判断的核心是跳转路径在前端是否已知、是否被 UI 静态声明。3. 路由传参query、params、props、state 的取舍路由传参是前端面试高频题但实际项目里很多人只会用 query 和 params导致传了个对象参数刷新就没了或分享链接时参数一堆。我在项目里把本地存储、URL、内存三种载体在路由传参上的差异分得清清楚楚才慢慢告别传参靠运气的状态。3.1 四种传参方式速览Vue Router 中常见传参方式基本是四种query、params、props、state。这里先给一个全局视角传参方式参数位置刷新后是否保留是否出现在 URL典型适用场景queryURL 查询串如?id1保留是列表到详情、分享链接params路径参数如/user/:id动态路由匹配时保留是RESTful 风格详情页props传给组件属性如props: true保留取决于你把它和哪个模式组合解耦组件与路由state内存中的历史状态不保留刷新即失否页面间透传临时数据3.2 params 传参的刷新困境动态路由是解药先讲最容易出问题的 params。很多人这样写// 跳转方 router.push({ name: UserProfile, params: { id: 1 } }) // 接收方 const route useRoute() console.log(route.params.id) // 第一次能打印刷新就空了问题本质在于params 传参只在内存中保存如果目标路由不是动态路由路径上没有定义/:id刷新后框架无法从 URL 里还原参数于是丢得干干净净。Vue Router 官方文档其实提醒过params 若未通过动态路由定义刷新就会失效。正确做法是配置动态路由const routes [ { path: /user/:id, // 路径上显式声明参数 name: UserProfile, component: UserProfile, props: true } ]这样router.push({ name: UserProfile, params: { id: 1 } })访问的 URL 是/user/1刷新后框架从路径里解析出id 1数据自然还在。所以我的建议很简单如果你希望参数在刷新后存活请把它放在 URL 能还原的地方——要么 query要么动态路由的路径参数。3.3 query 传参最稳妥但容易忽略编码query 传参是初学者最容易上手、也最不容易丢参数的方式。它的形式就是 URL 后面的?keyvalue比如/user/profile?id123sourcelist。// 跳转方 router.push({ path: /user/profile, query: { id: 123, source: list } }) // 接收方 const route useRoute() console.log(route.query.id) // 123 —— 注意是字符串 console.log(route.query.source) // listquery 传参的优点在于URL 完整保留了参数刷新没问题分享给别人的时候链接天然携带上下文。但它也有明显的坑——query 里的值都会变成字符串。如果你传一个对象数组不经过序列化处理读取端拿到的会是一串奇怪的文本。我习惯的做法是对象类数据先JSON.stringify一下再放进 query读取端再JSON.parse还原。但要注意 URL 有长度限制如果参数特别复杂就不要硬塞 query考虑换 state 或本地存储。3.4 props 传参让组件不再依赖路由对象props 传参是我最喜欢推荐新人在中大型项目里用的方式因为它的理念最干净组件不关心数据从哪来它只负责接收 props。Vue Router 支持三种 props 模式const routes [ { path: /user/:id, // 1. 布尔模式路由参数作为 props 传入 component: UserProfile, props: true }, { path: /order/:id, // 2. 对象模式静态 props component: OrderDetail, props: { from: list } }, { path: /search, // 3. 函数模式自定义映射逻辑 component: SearchPage, props: (route) ({ keyword: route.query.keyword, page: 1 }) } ]布尔模式最常用它把动态路由的params自动映射为组件的props组件内部直接defineProps([id])就能拿数据。这样组件既不用写useRoute()也不用关心路由层的数据格式测试和复用都方便很多。函数模式适合把 query 里的多个参数整理成规范的 props 结构尤其适合搜索页这类传参多且杂的场景。我建议正式项目里优先考虑动态路由 props: true的组合这种组合既保住了刷新不丢参又实现了组件与路由的解耦代码可读性明显好于在组件里到处写route.query。3.5 state 透传隐藏的临时数据通道还有一种容易被忽略的方式history.state。比如你从列表页跳去详情页中间临时带上一个用户当前选中的 tab 序号这个数据既不想出现在 URL 里也不希望刷新后长期保留那就可以用 state。router.push({ name: OrderDetail, params: { id: orderId }, state: { selectedTab: 2, fromPage: orderList } })读取端通过history.state取const state history.state console.log(state?.selectedTab)刷新页面后 history.state 会被清空这种用完即走的性质很适合存放一次性临时数据。但要注意state 不能跨页面分享也没有 URL 痕迹用户刷新或复制链接后就会丢失所以不要把核心业务数据放在这里面。3.6 参数设计的总原则综合下来我对路由传参的总体看法是需要收藏、分享、刷新存活的参数优先选动态路由 params或query参数语义是资源 ID 的用动态路由 params参数是过滤条件、来源标记的用 query组件想保持独立性、方便单测的用props: true或函数模式的 props临时且敏感、纯粹为了页面间少写一次接口调用的用 history.state任何对象/数组参数进入 URL 前先做序列化读取时统一反序列化。4. 跳转方式的细微差别push、replace、go 和记忆栈的关系很多人在跳转方式的认知上就停在push 是跳转go 是前进后退的层面但在嵌套路由 动态路由的项目里选错跳转方式会导致后退行为失控用户点了很多次返回都退不到预期页面。4.1 push 与 replace记忆栈里的差异浏览器维护着一个历史记录栈History Stack。router.push是在栈顶压入一条新记录router.replace则是把当前栈顶记录替换成新记录不增加栈的数量。用生活的例子理解push 像你在一个本子里新写了一页replace 像你撕掉当前这页换了一页新的。区别在返回键的行为上router.push(/order/1)后用户点返回回到前一个页面router.replace(/order/1)后用户点返回直接跳过被替换前的页面回到更早之前的页面。典型场景是登录跳转。用户从首页进入登录页登录成功后应使用replace跳回首页否则用户在首页按返回会莫名其妙回到登录页。类似的还有表单提交成功跳转、支付完成跳转这些场景一律用replace避免成功页面被塞进用户的后退路径里。// 登录成功后 router.replace(/dashboard)4.2 go、forward、back 的应用边界router.go(n)可以在历史栈里向前或向后跳 n 步forward()和back()分别是go(1)和go(-1)的语法糖。实际开发中我极少直接用go(3)这种写死步数的操作因为它依赖用户完整的历史栈很难预测。最常用的反而是router.back()或router.go(-1)配合页面里的返回按钮function goBack() { // 如果来源是外部则回首页否则后退一步 const from route.query.from if (from external) { router.replace(/) } else { router.back() } }这里有个比较容易忽略的问题在嵌套路由内使用router.back()如果历史栈里根本没有可退的记录点返回不会有任何反应。为了兜底我会判断window.history.state是否存在前一条记录或者干脆在入口统一记录来源避免出现返回按钮失灵的体验漏洞。4.3 嵌套路由与跳转方式叠加的边界场景嵌套路由里最常犯的错误是在子路由组件中调用router.push时路径写成了相对方式但语义不对。比如你当前在/user/profile想跳/user/settings结果写成了router.push(settings) // 相对跳转拼接当前路径变成 /user/profile/settings实际上 Vue Router 的router.push默认是绝对跳转相对跳转仅在某些限定场景下生效且容易写出歧义。我的习惯是编程式跳转一律写绝对路径或路由 name不要依赖当前层级做相对跳转。路由 name 是最好的——不关心 URL 怎么拼只要 name 对、参数对跳准无误。5. 实操中踩过的几个嵌套 传参的坑讲了半天理论说几个我在真实项目里踩过的坑。这几个问题背后都不是复杂原理但一旦碰上排查起来特别费时间因为报错点往往离真正的问题很远。5.1 坑一嵌套子路由的 params 没拼上URL 里根本没参数有一次同事反馈详情页从列表页跳过去能拿数据但手动输入网址打开就 404。查到最后发现详情页配置的是/detail这个静态路径跳转时却用了params: { id: 1 }。由于路径上没有/:idURL 变成/detail路由匹配时拿到的 params 是空的页面自然无法根据 id 拉数据。这个坑本质上就是第 3 节说的params 必须配动态路由。排查思路跳转后先看浏览器地址栏里的 URL如果参数没出现在地址栏里说明 params 没有对应到动态路径刷新丢参是必然结果。5.2 坑二同级嵌套路由切换时视图不更新或组件复用了旧数据嵌套路由下从/user/profile切到/user/settings如果两个页面用的是同一个组件实例比如都用UserTab组件Vue 会默认复用组件不会重新走生命周期钩子页面显示的可能是上一个路由留下的旧数据。解决办法常见有两个给router-view加key或者监听路由变化。router-view :keyroute.fullPath /watch( () route.fullPath, () { // 根据路由重新初始化数据 } )第一种更省心但每次路由变化都会销毁重建组件如果组件内部有重逻辑性能稍差第二种适合只想在特定参数变化时刷新数据的场景。我的选择标准是组件内部状态重的用 watch 精准更新组件简单依赖路由参数的直接加 key。5.3 坑三编程式跳转带着完整 query 跳转结果分享链接时参数全部暴露有段时间我们的订单详情页把产品名、下单渠道等一堆字段全都放在 query 里方便后端埋点。后来运营反馈分享出去的链接又长又杂改一个数字就能看到另一个订单的部分信息。虽然最后没出大事但那次之后我把所有非必要参数全部从 query 里挪走了必要业务参数用动态路由 params临时内部数据用 history.state留给 query 的只剩来源标记和埋点字段。这条经验的核心是query 里的所有数据都是 URL 的一部分既会被分享也可能被改写。所以只要参数不想被别人看见、不想被随意篡改就不该放 query。5.4 坑四404 兜底路由放在嵌套 children 里结果子路径全都不匹配嵌套路由里的 404 兜底不能只写在顶层。比如你配置了/user的 children访问/user/xxx时如果children里没有path: :pathMatch(.*)*这样的兜底也不会进入顶层 404而是白屏或者报错。{ path: /user, component: UserLayout, children: [ { path: profile, component: UserProfile }, { path: :pathMatch(.*)*, component: NotFound } ] }这个细节很少有人会在文档里专门说但对嵌套路由深浅不一的项目来说它决定用户乱输 URL 时的体验。排查的时候如果发现有的子路由 404 正常有的子路径白屏先看兜底路由是不是放在了正确的 children 层级。5.5 一些小但值钱的经验路由 name 一定要全局唯一且有命名规范比如UserProfile、OrderDetail。排查传参问题的时候用 name 跳转能减少大量路径写错的低级失误。团队项目建议统一封装跳转方法例如goDetail(id)、goBack()内部统一处理 params、query、state 和埋点。这样即使未来路由结构大改业务层的跳转逻辑也能少动。写路由守卫时别忘了嵌套路由的to.matched数组会存放每一层父路由记录。判断权限时通常要遍历matched逐层校验而不是只看to.path否则父级页面没有权限的子路由很容易被漏判。调试路由问题时最实用的手段就是打开浏览器开发者工具看 Network 面板里的请求是否真的发出去了、URL 是否符合预期。大多数路由跳转失败其实都是URL 本身不对。我在实际项目中最深的体会是路由模块表面上只是配置几张表、写几段跳转但它直接影响整个应用的信息架构。嵌套关系决定公共 UI 的复用程度跳转方式决定用户返回路径的体验传参方式决定页面刷新和分享时的数据保全。把这几个概念放在一起想清楚再写路由配置时基本不会再出现页面能跑但一刷新就废的尴尬局面。
返回列表