Skip to the content.

跳转拦截

如何在 form 表单未提交的情况下阻止页面跳转?

前言

当我们在一个包含表单的页面里填了一部分内容,忘记点击保存按钮,或者误触误点了其他部分,导致跳转了页面,那么我们没有保存的内容就丢了。特别是表单类目比较多的场景下,可能会使用户丧失耐心。

表单的使用在中后台项目中特别多。同时像常规网站的用户信息编辑等场景下,也特别需要注意这个问题,来提升用户体验。

前置知识

浏览器拦截

window.onbeforeunload = funcRef 当窗口即将被卸载(关闭)时,会触发该事件的回调。可以拦截页面刷新和页面跳转、页面关闭,显示浏览器默认的拦截弹窗。 image.png

单页面应用

spa 页面,切换 hash、history 路由是不牵涉到页面卸载的,所以自然也不会触发beforeunload的监听和回调的。

那么我们就需要额外的方法来实现“页面跳转”拦截。

方案设计

  1. 实现一个拦截的方法(/组件);
  2. 需要拦截的页面,注册这个拦截方法,传入判断是否需要拦截的函数和拦截方法;
  3. 导航前置守卫中调用拦截方法
  4. 关闭窗口,刷新页面时候调用onbeforeunload事件回调,判断是否需要拦截。

具体实现

  1. 拦截的方法
async intercept() {
    const needIntercept = this.needIntercept();
    if (needIntercept) {
        let goNext = true;
        try {
            await this.confirmDialog('填写的内容尚未提交,是否立即提交?', '保存', '不保存');
            goNext = await this.$refs.target?.save?.();
        } catch (action) {
            goNext = action !== 'close';
            if (goNext) {
                this.$refs.target?.recovery?.()
            }
        }
        return goNext;
    } else {
        this.$refs.target?.recovery?.()
        return true;
    }
}

needIntercept返回是否有数据变化。

当数据变化时,弹窗提示“填写的内容尚未提交”,是否保存?

needIntercept () {
    return JSON.stringify(this.members) !== this.nativeMembers
}

这里可以用序列化的方式比较前后数据有没有变化,也算是比较通用的方法之一。

JSON.stringify,还有第二个参数,类型为函数或者数组,数组表示需要序列化哪些数据项,可选。具体可参考mdn 更多通用的比较表单数据变化的方法,大佬们可以在评论区告诉我。

  1. 在需要拦截的页面,注册这个拦截方法
created() {
    this.register = registerInterceptor(this.needIntercept, this.intercept);
}
//不要忘记移除拦截器
beforeDestroy() {
    destroyInterceptor(this.register);
}

在页面创建之后,注册这个拦截器。传入判断是否需要拦截的函数,和拦截回调函数。

function registerInterceptor(intercept, callback) {
  const _id = v4();
  LEAVE_INTERCEPTORS[_id] = {
    intercept,
    callback,
  };
  if (!interceptor_running) {
    interceptor_running = true;
    window.addEventListener("beforeunload", interceptBeforeBrowserLeave);
  }
  return _id;
}
  1. 导航前置守卫中调用拦截方法
router.beforeEach(async (to, from, next) => {
  try {
    const nextloop = await interceptBeforeRouteLeave()
    if (!nextloop) {
      return next(false)
    }
  } catch {
    return next(false)
  }

根据拦截器返回结果判断路由是否通过。

// 执行全部的拦截器,返回拦截器执行结果
// 全部返回true才能返回true
function interceptBeforeRouteLeave() {
  const interceptors = Object.keys(LEAVE_INTERCEPTORS)
    .filter(key => LEAVE_INTERCEPTORS[key].intercept())
    .map(key => LEAVE_INTERCEPTORS[key].callback());
  const results = await Promise.all(interceptors);
  return results.every(result => !!result);
}

这里之所以把拦截器存储起来,放在LEAVE_INTERCEPTORS里,是为了满足页面理由多个 tab 都需要拦截功能的情况,比如下面这种场景: image.png

三个 tab 下面都有表单输入,当切换 tab 时,仍然是在当前页面操作,但是当切换路由时,三个 tab 下面的保存提示应该被一次触发弹窗,提示用户保存输入内容。

  1. 关闭窗口,刷新页面时候调用onbeforeunload事件回调,判断是否需要拦截。 细心地同学一经发现了,我们在上线的代码里面已经提现了浏览器拦截的部分,
if (!interceptor_running) {
  interceptor_running = true;
  window.addEventListener("beforeunload", interceptBeforeBrowserLeave);
}

每次注册拦截器的时候,会判断是否已经加入了浏览器拦截,没有的话就会加入。

function interceptBeforeBrowserLeave(e) {
  const intercept = Object.keys(LEAVE_INTERCEPTORS).some(key => LEAVE_INTERCEPTORS[key].intercept());
  console.log("intercept", intercept);

  e = e || window.event;
  if (intercept) {
    if (e) {
      e.returnValue = intercept;
    }
    return intercept;
  }
}

浏览器拦截逻辑实现比较简单,就是some拦截器对象数组中是否有 intercept === true的情况,如果有就触发浏览器拦截提示。

总结

我们在埋头肝需求的时候,往往会忽略用户体验,不管是 C 端项目,还是中后台项目,使用体验才是最能体现出前端价值的地方,也是前端自我成就的捷径。

所以在开发的过程中,我们应该站在使用者的角度,不断优化性能和使用体验,这也是进阶高级前端的必要技能。

码字不易,周末尤甚,动动手点个赞鼓励一下,这将成为我持续输出的动力。2022 一起加油,做一个优秀的表单工程师!

返回首页