Code前端首页关于Code前端联系我们

Vue3 onBeforeRouteLeave到底怎么用?没生效/保存草稿/组合选项式API混用全解答

terry 44分钟前 阅读数 21 #Vue

做过表单类、编辑类页面的前端同学,应该都绕不开一个需求:用户离开当前页面前,得拦一下——比如问“还有未保存的内容,确定要走吗?”,或者“操作流程没完成,离开可能丢失进度”,在Vue Router 4搭配Vue3的环境里,这个需求主要靠组合式API的onBeforeRouteLeave钩子解决,但不少人上手时会踩坑:明明写了钩子代码,路由跳的时候根本没反应;或者只懂基础拦截,不知道怎么结合具体业务;组合式和选项式API混写的项目里,钩子又该怎么放?今天咱们就把这些问题聊透。

组合式API下的基础写法和注意事项

首先说最基础的场景:在使用<script setup>(组合式API的标准写法)的Vue组件里,怎么写onBeforeRouteLeave核心注意点:这个钩子不是Vue3核心库的,必须从vue-router里单独导入,而且只能在路由组件(也就是router-view渲染的直接子组件)里生效——嵌套组件里写它没用,这点和选项式API的beforeRouteLeave一模一样,很多人踩的第一个坑就是嵌套子组件里写了钩子没反应。

基础代码举个例子:比如做个“个人信息编辑”页面,先给页面留一个临时未保存状态的标记hasUnsaved,再在onBeforeRouteLeave里判断这个状态,弹出确认框。 这里要注意组合式API下的钩子参数结构:和选项式API一样,接收三个参数——to(目标路由对象)、from(当前要离开的路由对象)、next(控制路由跳转的函数),不过现在Vue Router 4推荐避免直接调用next的复杂跳转逻辑,尽量用返回值代替:返回false取消跳转,返回true或者undefined继续跳转,返回一个路由地址字符串或对象直接重定向,返回Promise的话就等Promise resolve之后再判断返回值,只有极少数场景(比如需要动态判断多次跳转)才用next

没生效?排查这4个高频原因

很多同学刚写就遇到“钩子没触发”的问题,别急,按顺序查这几点,90%的情况都能解决:

  1. 是不是路由组件? 再确认一遍!比如你有个组件叫UserForm,是被嵌套在UserCenter的某个插槽里的,而UserCenter才是router-view的直接子组件,那UserForm里的onBeforeRouteLeave肯定没用——必须把钩子挪到UserCenter,或者用事件总线、provide/inject让UserForm把未保存状态传给UserCenter,再由UserCenter的钩子拦截。
  2. 有没有正确从vue-router导入? <script setup>里经常忘写或者写错导入语句,正确的是import { onBeforeRouteLeave } from 'vue-router'
  3. 导航是哪种类型? onBeforeRouteLeave只拦路由导航——也就是通过router-linkrouter.push/replace/go这些Vue Router方法触发的跳转,如果是用户直接点浏览器的刷新按钮、关闭标签页、地址栏手动输入地址跳转,这个钩子不管用!那这时候怎么办?浏览器刷新/关闭的话,得用原生的beforeunload事件,地址栏手动跳且不是同源的话拦不住,同源的可以结合watch监听$route(选项式)或useRoute()的返回值变化,但不如原生beforeunload直接。
  4. 有没有嵌套路由的特殊情况? 如果是嵌套路由的父组件,onBeforeRouteLeave只在你完全离开父组件对应的路由路径时才触发——比如父路由是/user,子路由是/user/info/user/order,你从/user/info跳到/user/order,父组件的onBeforeRouteLeave不会触发;但如果跳到/home,父组件的钩子才会跑,如果需要在子路由之间切换时也拦截,得把子组件的未保存状态通过某种方式(比如props、provide/inject、pinia/vuex)传到父组件,父组件同时监听子路由变化,或者直接在子组件里写onBeforeRouteLeave

实战场景:表单未保存的双拦截方案(路由跳+浏览器刷新关闭)

刚才说了onBeforeRouteLeave不管浏览器原生操作,那完整的未保存拦截得用“双拦截”——路由跳用onBeforeRouteLeave,刷新/关闭用beforeunload,咱们结合pinia(或者vuex)做个更规范的状态管理,避免重复写逻辑。 首先在pinia里建一个useUnsavedStore,用来统一管理所有可能有未保存内容的页面的状态:比如用一个对象unsavedPages,key是当前路由的fullPath或者name,value是true(有未保存)或false(已保存),然后写两个方法:markUnsaved(标记当前页有未保存)、clearUnsaved(清除当前页的未保存标记)。 然后在每个需要拦截的路由组件里:

  1. 导入useRouteonBeforeRouteLeaveuseUnsavedStore
  2. 获取当前路由的namefullPath,记为currentPageKey
  3. 监听表单的变化(比如用watch监听表单数据的某个字段或者整个表单对象的deep变化),变化时调用markUnsaved
  4. 表单提交成功后,调用clearUnsaved
  5. onBeforeRouteLeave钩子,先从store里拿当前页的状态,如果有未保存,弹出自定义的确认框(比如element-plus的ElMessageBox.confirm),用户点确定的话返回true并调用clearUnsaved,点取消返回false
  6. 监听window.beforeunload事件,同样检查store的状态,如果有未保存,给event的returnValue赋值一个字符串(不同浏览器可能不会显示这个字符串,但赋值是必须的,才能触发浏览器自带的确认框)。
  7. 最后别忘了在组件卸载时(onUnmounted)清除当前页的未保存标记,并且移除beforeunload的事件监听,避免内存泄漏,也避免其他页面触发错误的拦截。

组合式和选项式API混写的项目里,钩子该怎么选?

有些老项目在升级Vue3时,是逐步从选项式API改成组合式的,这时候可能出现同一个组件里既有export default的选项式代码,又有<script setup>的组合式代码,或者项目里同时存在两种写法的路由组件。 原则是:只留一个钩子生效,避免逻辑冲突。 如果是同一个组件里混写:尽量统一用一种写法的钩子,比如如果大部分代码改成了<script setup>,就删掉选项式里的beforeRouteLeave;如果只是加了一小部分组合式逻辑,原来的beforeRouteLeave就保留,不用再写onBeforeRouteLeave。 如果是不同组件用不同写法:两者不冲突,各自在自己的路由组件里用就行——组合式的用onBeforeRouteLeave,选项式的用原来的beforeRouteLeave

onBeforeRouteLeave还能做哪些其他业务?

除了最常用的未保存拦截,onBeforeRouteLeave还有不少实用场景:

  1. 停止定时器或异步请求:比如页面里有一个轮询接口的定时器,或者一个正在加载大量数据的异步请求,离开时如果不停止,可能会造成内存泄漏,或者数据加载完成后修改已经卸载的组件的状态报错——这时候可以在onBeforeRouteLeave里清除定时器(clearIntervalclearTimeout),或者取消异步请求(比如axios的CancelToken或者fetch的AbortController)。
  2. 记录页面停留时间:比如做数据分析的项目,需要记录用户在每个页面的停留时长——可以在onMounted里记录开始时间,在onBeforeRouteLeave里记录结束时间,计算差值后发送给后端。
  3. 临时保存数据到本地存储:比如刚才的未保存表单,除了弹出确认框,还可以在用户确认离开前,把表单数据临时存到localStoragesessionStorage里,下次用户再进来时自动填充——这点要注意,本地存储的容量有限,别存太大的数据,而且敏感数据不能存。

好了,关于Vue3 onBeforeRouteLeave的核心内容就聊到这里,组合式API下必须从vue-router导入,只能在路由组件里用;没生效就按那4个高频原因排查;完整的未保存拦截要用“双拦截”;混写项目里统一钩子;它还能做定时器清理、停留时间记录、临时存数据这些事,如果还有其他具体的问题,欢迎在评论区留言讨论。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门