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

一、环境准备

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?.();
}
相关推荐
yqcoder几秒前
vue2 和 vue3 生命周期的区别
前端·javascript·vue.js
excel11 分钟前
前端人必备的 JavaScript API 全面指南(含 postMessage、File、Stream、Web 组件等)
前端
m0_738120725 小时前
CTFshow系列——命令执行web53-56
前端·安全·web安全·网络安全·ctfshow
Liu.7747 小时前
uniappx鸿蒙适配
前端
山有木兮木有枝_8 小时前
从代码到创作:探索AI图片生成的神奇世界
前端·coze
言兴9 小时前
秋招面试---性能优化(良子大胃袋)
前端·javascript·面试
WebInfra10 小时前
Rspack 1.5 发布:十大新特性速览
前端·javascript·github
雾恋10 小时前
我用 trae 写了一个菜谱小程序(灶搭子)
前端·javascript·uni-app
烛阴11 小时前
TypeScript 中的 `&` 运算符:从入门、踩坑到最佳实践
前端·javascript·typescript
Java 码农12 小时前
nodejs koa留言板案例开发
前端·javascript·npm·node.js