一、在导航守卫里用from参数直接拿上一个路由
在Vue项目开发里,经常会遇到需要知道“用户从哪个页面跳过来”的场景,比如做返回按钮、埋点统计、权限校验这些功能时,获取上一个路由(previous route)就成了关键需求,那Vue Router到底怎么实现这个功能?不同场景下有哪些技巧?下面一步步拆解答案。
Vue Router的导航守卫机制,本身就提供了获取“来源路由”的入口——from
参数,不管是全局守卫、路由独享守卫,还是组件内守卫,都能通过这个参数拿到上一个路由的信息。
先看全局前置守卫(router.beforeEach
),这是最常用的全局拦截逻辑,举个例子:
// 全局守卫,每次路由跳转前触发 router.beforeEach((to, from, next) => { console.log('从哪个页面来?', from.path) // 打印上一个路由的路径 console.log('要到哪个页面去?', to.path) next() // 必须调用next放行,否则路由跳转会卡住 })
这里的from
就是上一个路由的路由对象,里面包含path
(路径)、name
(路由命名)、meta
(自定义元信息)、params
(动态参数)等关键属性。
再看组件内的导航守卫,比如beforeRouteEnter
(组件渲染前触发):
export default { // 组件内守卫,此时组件实例还没创建,this访问不到 beforeRouteEnter(to, from, next) { console.log('从', from.path, '进入当前组件') next() }, // 组件内守卫,路由参数变化时触发(/user/:id 从id=1跳到id=2) beforeRouteUpdate(to, from, next) { console.log('参数变化,上一个路由是', from.path) next() }, // 组件内守卫,离开当前组件时触发 beforeRouteLeave(to, from, next) { console.log('要离开当前组件,跳转到', to.path, ';之前是从', from.path, '来的') next() } }
注意哦,beforeRouteEnter
执行时组件实例还没生成,所以不能用this
;但beforeRouteUpdate
和beforeRouteLeave
里可以用this
访问组件数据~
还有路由独享守卫(在路由配置里写beforeEnter
):
const routes = [ { path: '/about', component: About, // 只有进入/about时才触发这个守卫 beforeEnter: (to, from, next) => { console.log('从', from.path, '进入/about页面') next() } } ]
这种方式适合给某个路由单独加“来源判断”逻辑,不用全局拦截所有路由~
借助路由历史记录手动维护上一个路由
导航守卫能覆盖“正常路由跳转”的情况,但如果用户是直接在地址栏输入URL、刷新页面,这时候导航守卫里的from
可能是undefined
(比如首次进入应用时,没有上一个路由),这时候就得自己维护路由历史记录啦。
思路很简单:每次路由跳转后,把当前路由存起来,作为下一次的“上一个路由”,可以用全局变量、Vuex、Pinia或者浏览器存储(sessionStorage、localStorage)来实现。
举个用全局变量的例子(适合简单项目):
// 在router.js里定义一个变量存上一个路由 let previousRoute = null router.afterEach((to, from) => { // 路由跳转完成后,把当前to存为下一次的from previousRoute = from }) <p>// 然后在需要的地方(比如组件里)导入这个变量 import router from './router' console.log('上一个路由是', previousRoute)
但全局变量有个问题:页面刷新后会丢失数据,所以更可靠的是用浏览器存储,比如sessionStorage(会话级存储,关闭标签页就清空):
// 全局后置守卫,每次路由跳转后记录 router.afterEach((to, from) => { if (from) { // 防止首次进入时from为undefined sessionStorage.setItem('previousRoute', JSON.stringify(from)) } }) <p>// 在组件里读取 export default { mounted() { const prevRoute = JSON.parse(sessionStorage.getItem('previousRoute')) console.log('上一个路由(可能是刷新前的)', prevRoute) } }
如果用状态管理工具(比如Pinia),可以更优雅地维护:
// store/routeStore.js import { defineStore } from 'pinia' export const useRouteStore = defineStore('route', { state: () => ({ previousRoute: null }), actions: { setPreviousRoute(route) { this.previousRoute = route } } }) <p>// router.js里引入并使用 import { useRouteStore } from './store/routeStore' router.afterEach((to, from) => { const routeStore = useRouteStore() routeStore.setPreviousRoute(from) })</p> <p>// 组件里使用 import { useRouteStore } from './store/routeStore' export default { setup() { const routeStore = useRouteStore() console.log('上一个路由', routeStore.previousRoute) } }
这种方式不管是路由跳转、刷新还是地址栏输入,只要之前有过路由记录,就能拿到相对可靠的“上一个路由”~
特殊场景:直接输入URL或刷新页面怎么处理?
前面提到,用户直接在地址栏输入URL或者刷新页面时,Vue Router的导航守卫里的from
可能是undefined
(因为这时候没有“路由跳转”行为,是全新的页面加载),这时候要分两种情况处理:
首次进入应用:这时候确实没有上一个路由,逻辑上可以设为null
或者默认路由(比如首页)。
刷新页面:这时候想拿到“刷新前的上一个路由”,就得靠持久化存储(比如sessionStorage、localStorage),前面讲的“手动维护路由历史”方案就能解决这个问题——刷新前把路由信息存在存储里,刷新后再读出来。
举个完整的刷新处理例子:
// router.js router.beforeEach((to, from, next) => { // 刷新时,from可能是undefined,所以先从sessionStorage取 const storedPrev = sessionStorage.getItem('previousRoute') if (storedPrev) { // 这里可以把from替换成存储的路由(仅作逻辑处理,实际导航守卫的from是只读的) console.log('刷新前的上一个路由', JSON.parse(storedPrev)) } next() }) <p>router.afterEach((to, from) => { if (from) { sessionStorage.setItem('previousRoute', JSON.stringify(from)) } })
这样一来,就算用户刷新页面,也能通过sessionStorage恢复“上一个路由”的信息啦~
Vue Router 3.x和4.x版本的差异
Vue生态里,Vue 2搭配Vue Router 3.x,Vue 3搭配Vue Router 4.x,两个版本在“获取上一个路由”的细节上有不少区别,得注意兼容性~
导航守卫的语法变化:
- Vue Router 3.x的全局守卫是
router.beforeEach((to, from, next) => { next() })
,必须调用next()
来控制导航; - Vue Router 4.x的全局守卫是
router.beforeEach((to, from) => { return true })
,通过返回值(或Promise)来控制导航,不再需要next
。
举个Vue Router 4.x的全局守卫例子:
// Vue Router 4.x router.beforeEach((to, from) => { console.log('从', from.path, '到', to.path) return true // 放行,也可以返回false阻止,或返回路由对象重定向 })
组件内守卫的写法差异:
Vue 3的组件用setup语法糖时,组件内守卫要换成组合式API的写法,比如在<script setup>
里,用onBeforeRouteUpdate
和onBeforeRouteLeave
:
// Vue 3 + Vue Router 4.x 的组件内守卫 <script setup> import { onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router' <p>onBeforeRouteUpdate((to, from) => { console.log('参数变化,上一个路由是', from.path) })</p> <p>onBeforeRouteLeave((to, from) => { console.log('离开当前组件,之前从', from.path, '来的') }) </script>
而Vue Router 3.x在Vue 2的选项式API组件里,还是用beforeRouteEnter
这类选项式守卫~
路由实例的获取方式:
Vue Router 3.x中,在组件里用this.$router
获取路由实例;Vue Router 4.x中,在setup语法糖里要通过useRouter
组合式API获取:
// Vue 3 + Vue Router 4.x <script setup> import { useRouter } from 'vue-router' const router = useRouter() console.log(router.currentRoute.value) // 当前路由信息 </script>
这些版本差异得留意,否则容易出现“代码明明对,就是不生效”的情况~
实际项目中的应用场景
知道了怎么获取上一个路由,得落地到真实需求里才有价值,下面举几个常见场景,看看怎么结合技术方案实现~
场景1:自定义返回按钮
很多项目的头部有“返回”按钮,需要跳转到上一个页面,如果是普通跳转,用this.$router.back()
就行,但有时候要特殊处理(比如从特定页面来的要跳转到其他地方),这时候结合上一个路由的判断:
// Vue 2 + Vue Router 3.x 示例 <template> <button @click="goBack">返回</button> </template> <script> export default { data() { return { prevRoute: null } }, beforeRouteEnter(to, from, next) { next(vm => { vm.prevRoute = from // 把from赋值给组件实例的prevRoute }) }, methods: { goBack() { if (this.prevRoute && this.prevRoute.path === '/special-page') { // 从特殊页面来的,跳转到指定页面 this.$router.push('/target-page') } else { // 否则返回上一页 this.$router.back() } } } } </script>
场景2:面包屑导航
面包屑需要显示“当前页面的上级页面”,这时候上一个路由(或父级路由)就是关键,比如路由配置是嵌套路由:
const routes = [ { path: '/user', name: 'User', component: UserLayout, children: [ { path: 'profile', name: 'UserProfile', component: UserProfile } ] } ]
在UserProfile组件里,上一个路由可能是User页面,所以面包屑可以显示“用户中心 > 个人资料”,这时候用导航守卫的from参数,或者路由的父级关系(this.$route.parent
)来实现~
场景3:页面访问统计
统计用户从哪个页面进入当前页面,用于分析流量来源,可以在全局后置守卫里记录:
// 结合埋点工具,比如百度统计、Google Analytics router.afterEach((to, from) => { if (from) { // 发送埋点数据:来源页面from.path,目标页面to.path window._hmt.push(['_trackPageview', to.path, from.path]) } })
场景4:权限控制
有些页面要求必须从特定页面跳转过来才能访问(比如支付成功页,必须从支付页跳转),这时候在路由守卫里判断from:
router.beforeEach((to, from, next) => { if (to.name === 'PaymentSuccess' && from.name !== 'Payment') { // 不是从支付页来的,重定向到首页 next({ name: 'Home' }) } else { next() } })
这些场景覆盖了大部分业务需求,核心都是“先拿到上一个路由,再做逻辑判断”~
常见问题与解决方案
实际开发中,“获取上一个路由”容易碰到一些坑,这里总结几个高频问题和应对方法~
问题1:from参数是undefined
原因:用户直接输入URL、刷新页面、首次进入应用时,没有“上一个路由”,所以from为undefined。
解决:结合“手动维护路由历史”的方案,用sessionStorage或状态管理工具存之前的路由;同时在代码里做兜底判断(比如if (!from) return
)。
问题2:嵌套路由下from的准确性
场景:比如父路由是/user
,子路由是/user/profile
,从/user
跳到/user/profile
时,from是/user
,这没问题;但如果从其他页面(比如/home
)跳到/user/profile
,from是/home
,这也符合预期,但要注意,嵌套路由的导航守卫里,from是“直接的来源路由”,不是父路由哦~
解决:如果需要父路由信息,可以用to.matched
数组(包含所有嵌套的父路由),或者手动维护父路由关系。
问题3:动态路由参数变化时from的内容
场景:路由是/user/:id
,从/user/1
跳到/user/2
,这时候触发的是beforeRouteUpdate
守卫,from是/user/1
的路由对象,包含params: { id: 1 }
。
注意:这时候from的path是/user/1
,name如果是动态路由的命名(比如'User'
),params里能拿到旧的id,所以可以正常区分参数变化前后的路由~
问题4:Vue Router 4.x中useRouter获取
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。