一、环境准备
vue + ant-design-vue + ts + mitt
具体效果:如果监听的值发生变化,且未保存时,点击目录或者更改url地址等进行路由跳转的操作,会出现一个弹窗进行确认
二、代码用例
- 事件总线
ts
// utils/eventBus.ts
import mitt from 'mitt';
export default mitt();
- 拦截通用方法
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?.();
}