状态未保存,拦截页面跳转通用方法

一、环境准备

vue + ant-design-vue + ts + mitt

具体效果:如果监听的值发生变化,且未保存时,点击目录或者更改url地址等进行路由跳转的操作,会出现一个弹窗进行确认

二、代码用例

  1. 事件总线
ts 复制代码
// utils/eventBus.ts
import mitt from 'mitt'; 
export default mitt();
  1. 拦截通用方法
ts 复制代码
// useRouteLeaveGuard.ts
import { computed, onBeforeUnmount, onMounted, ref, Ref, watch } from 'vue';
import { useRouter, RouteLocationNormalized, NavigationGuardNext } from 'vue-router';
import { Modal } from 'ant-design-vue';
import { cloneDeep, debounce } from 'lodash-es';

import mitt from '/@/utils/eventBus';
// 定义事件类型
const RouteLeaveEventKey = Symbol();

interface RouteLeaveGuard {
  hasUnsavedChange?: Ref<boolean>;
  hasUnsavedContent?: Ref<any>;
  message?: string;
}
// 定义全局状态类型
export interface GuardStatus {
  allowJump: boolean;
  removeGuard?: () => void;
}

// 监听路由守卫
export function useRouteLeaveGuard({
  hasUnsavedChange,
  hasUnsavedContent,
  message = '您有未保存的更改,确定要离开吗?',
}: RouteLeaveGuard) {
  const router = useRouter();
  // 路由守卫
  let currentGuard;
  // 保存传入hasUnsavedContent初始值
  const initialData = ref(cloneDeep(hasUnsavedContent?.value));
  // 判断传入hasUnsavedContent是否发生了更改
  const isDirty = ref<boolean>(false);

  // 暴露重置脏状态的方法
  const resetDirtyState = () => {
    initialData.value = cloneDeep(hasUnsavedContent?.value);
    isDirty.value = false;
  };

  // 当hasUnsavedChange 和 isDirty 均为false时 允许跳转
  const allowJump = computed(() => {
    return !hasUnsavedChange?.value && !isDirty.value;
  });

  const handleRouteChange = async (_, from: RouteLocationNormalized, next: NavigationGuardNext) => {
    if (allowJump.value) {
      next();
      return;
    }
    try {
      const confirmed = await new Promise<boolean>((resolve) => {
        Modal.confirm({
          title: '确认离开',
          content: message,
          onOk: () => resolve(true),
          onCancel: () => resolve(false),
        });
      });

      if (confirmed) {
        next();
      } else {
        next(false);
        // 处理浏览器后退按钮的特殊情况
        if (window.history.state && window.history.state.forward === from.path) {
          window.history.pushState(null, '', from.fullPath);
        }
      }
    } catch (error) {
      console.error(error);
    }
  };

  // 处理页面刷新/关闭
  const handleBeforeUnload = (e: BeforeUnloadEvent) => {
    if (!allowJump.value) {
      e.preventDefault();
      e.returnValue = message;
      return message;
    }
  };

  // 注册路由守卫
  const setupGuard = () => {
    currentGuard = router.beforeEach(handleRouteChange);
  };

  // 清除路由守卫
  const removeGuard = () => {
    if (currentGuard) currentGuard();
  };

  onMounted(() => {
    setupGuard();
    window.addEventListener('beforeunload', handleBeforeUnload);
  });

  onBeforeUnmount(() => {
    removeGuard();
    mitt.emit(RouteLeaveEventKey, {
      allowJump: true,
    });
    window.removeEventListener('beforeunload', handleBeforeUnload);
  });

  // 监听传入的hasUnsavedContent是否发生了改变
  if (hasUnsavedContent) {
    watch(
      () => hasUnsavedContent.value,
      debounce((newVal) => {
        if (initialData.value === undefined) {
          initialData.value = cloneDeep(newVal); // 初始化
        } else {
          isDirty.value = JSON.stringify(newVal) !== JSON.stringify(initialData);
        }
      }, 300),
      { deep: true, immediate: true }
    );
  }

  // 状态变化时触发全局事件
  watch(
    allowJump,
    (newVal) => {
      mitt.emit(RouteLeaveEventKey, {
        allowJump: newVal,
        removeGuard,
      });
    },
    { immediate: true }
  );

  // 返回 resetDirtyState 供外部调用(关键新增)
  return { resetDirtyState };
}

// 监听状态变化的函数
export function useRouteLeaveListener(callback: (allowed: GuardStatus) => void) {
  // 注册监听回调
  const handler = (status: GuardStatus) => callback(status);

  mitt.on(RouteLeaveEventKey, handler);

  onBeforeUnmount(() => {
    mitt.off(RouteLeaveEventKey, handler);
  });
}

三、通用方法在 vue中具体使用

ts 复制代码
import { useRouteLeaveGuard } from 'useRouteLeaveGuard';
 
const formData = ref({ name: '' });
const hasUnsavedChange = ref(false);

const { resetDirtyState } = useRouteLeaveGuard({
  hasUnsavedChange,
  hasUnsavedContent: formData, // 传入需要检测的内容
});

// 保存操作
const handleSave = async () => {
  await api.saveData(formData.value);
  hasUnsavedChange.value = false; // 标记已保存
  resetDirtyState(); // 重置脏状态
};

// 如果场景复杂:可通过hasUnsavedChange一个字段进行判断

四、事件总线使用(特殊场景情况使用)

在另一处代码,获取当前使用拦截功能页面的保存状态

ts 复制代码
import { useRouteLeaveListener, GuardStatus } from 'useRouteLeaveGuard';

const leaveStatus = ref<GuardStatus>({
  allowJump: true,
});
// 状态发生变化时回调
useRouteLeaveListener((status: GuardStatus) => {
  leaveStatus.value = status;
});

// 获取具体的状态值,进行判断
if(leaveStatus.value?.allowJump){
    // 可以跳转
} else {
    // 如果允许跳转
    leaveStatus.value?.removeGuard?.();
}
相关推荐
Patrick_Wilson7 小时前
修好 bug 只是开始:一次由监控需求反向重构日志结构的复盘
前端·监控·数据可视化
kyriewen7 小时前
面试官让我优化一个卡死的表格,我打开 Claude 五秒重写,他放下咖啡问我要不要来当组长
前端·javascript·面试
小粉粉hhh8 小时前
React(二)——dom、组件间通信、useEffect
前端·javascript·react.js
Csvn8 小时前
🤖 AI 生成前端代码的 5 个典型翻车现场及修复指南
前端
IT_陈寒9 小时前
SpringBoot自动装配坑了我一天,原来问题出在这
前端·人工智能·后端
索西引擎10 小时前
【React】key 属性:协调算法中的元素标识机制与最佳实践
前端·javascript·react.js
只会cv的前端攻城狮10 小时前
动态组件架构设计:从配置到事件驱动全链路解析
前端·vue.js
飞天狗10 小时前
Next.js 16 App Router 实战:服务端组件与流式渲染
前端·next.js
OpenTiny社区10 小时前
TinyEngine 2.11版本发布:AI 进一步融入画布,Vue 工程可一键转DSL
前端·vue.js·github
小雪_Snow10 小时前
JavaScript 中的三种常用事件
开发语言·前端·javascript