首页 / 资讯中心 / 文章详情

Vue防页面误关闭:beforeunload实战与状态管理方案

Vue防页面误关闭:beforeunload实战与状态管理方案 ★ FEATURED ARTICLE
1. 项目概述为什么 Vue 里“关页面弹提示”这么难搞又非得搞你有没有遇到过这种场景用户正在编辑一份重要表单填了大半突然手滑点了浏览器右上角的 ×或者误按了 F5 刷新——页面瞬间白屏所有未保存的内容灰飞烟灭。你作为开发者第一反应肯定是“得加个提示”于是翻文档、搜 Stack Overflow、抄代码三分钟写完window.addEventListener(beforeunload, ...)本地测试一切正常。结果一上线Chrome 控制台立刻报错[Violation] Permissions Policy violation: unload is not allowed in this documentVue 2 项目里甚至根本没反应Vue 3 项目在某些版本下提示框只闪一下就消失。更尴尬的是用户反馈说“明明没填东西为啥总弹窗”——这已经不是功能实现问题而是体验崩坏。这个标题“vue实现浏览器关闭或刷新弹窗提示beforeunload/unload”表面看只是调用一个原生 API但背后牵扯的是现代浏览器安全策略演进、Vue 生命周期与 DOM 事件的耦合机制、单页应用状态管理的边界判断以及真实业务中“什么才算‘有未保存内容’”的语义定义。它不是一段能直接复制粘贴的 5 行代码而是一套需要在 Vue 实例生命周期、路由守卫、表单状态监听、浏览器兼容性、甚至用户心理预期之间反复权衡的轻量级交互协议。核心关键词vue、beforeunload、unload、弹窗提示每一个都带着坑vue决定了你不能裸写全局事件必须和响应式系统对齐beforeunload是唯一被现代浏览器保留的、能触发原生提示的钩子但它被严格限制——只能返回字符串且现代浏览器会忽略自定义文案统一显示“离开此页面”这是故意设计的防钓鱼机制unload已被废弃仅用于日志上报等无交互场景而“弹窗提示”这个词本身就有误导性——它不是你用alert()或Element UI Dialog实现的那种漂亮弹窗而是浏览器强制接管的、不可定制的、带明确风险提示的原生对话框。适合谁来读这篇如果你是刚学 Vue 的新手正被面试官问到“怎么防止用户误关页面”别急着背代码先理解为什么beforeunload不是你想弹就弹的按钮如果你是做了三年 Vue 的中级开发者还在用this.$router.beforeEach拦截所有路由跳转来模拟“防丢失”那这篇会告诉你为什么这种方案在标签页切换、地址栏输入、书签点击等场景下必然失效如果你是技术负责人正为 SaaS 系统的文档编辑模块设计数据保护策略这篇会给你一套可落地的状态检测逻辑、兼容性兜底方案以及比“弹窗”更重要的——如何用视觉反馈降低用户误操作概率。它不教你造轮子而是帮你把轮子装在正确的位置上。2. 核心思路拆解为什么不能直接写window.addEventListenerVue 的生命周期才是关键战场很多人第一次尝试时习惯性地在mounted钩子里写mounted() { window.addEventListener(beforeunload, (e) { e.preventDefault(); e.returnValue 你有未保存的内容确定要离开吗; }); }然后发现Vue 2 里完全没反应Vue 3 Composition API 里onMounted里写也无效甚至控制台报错。这不是代码写错了而是你把战场选错了位置。beforeunload事件的触发时机极其特殊——它发生在浏览器准备卸载当前文档的最后时刻此时 Vue 实例的beforeUnmount或unmounted钩子可能还没执行也可能已经执行完毕但更重要的是Vue 的响应式系统在此刻已停止工作this或ref的值无法保证实时性。你监听的window对象是全局的但你的判断逻辑比如“表单是否已修改”依赖 Vue 的响应式数据这两者在beforeunload触发瞬间是脱节的。2.1 Vue 2 与 Vue 3 的根本差异响应式代理的“存活时间”Vue 2 使用Object.defineProperty实现响应式实例销毁时会清理所有watcher但beforeunload事件监听器是挂载在window上的它不随 Vue 实例销毁而自动移除。这就导致两个严重问题内存泄漏组件多次创建销毁后监听器越积越多状态错乱监听器里访问的this.formData可能是旧实例的数据甚至this已指向undefined。Vue 3 的Proxy响应式更强大但beforeunload触发时setup()函数早已执行完毕ref和reactive创建的响应式对象虽然还在内存中但其effect依赖收集已停止computed不再更新。你写的if (isFormDirty.value) { ... }中的isFormDirty.value可能还是上次watch触发时的旧值。提示beforeunload是浏览器内核级事件它不经过 Vue 的事件循环也不触发 Vue 的任何生命周期钩子。把它想象成“操作系统即将关机前的最后一声警报”此时所有应用层的框架逻辑都已暂停。2.2 真正的解决方案把状态判断前置让beforeunload只做“开关”正确的思路不是让beforeunload去“计算”是否该提示而是让它只读取一个预先计算好、稳定可靠、无需响应式更新的布尔值。这个值必须在用户操作如输入、选择、上传发生时由 Vue 的响应式系统实时更新并同步到一个全局可访问的变量中。beforeunload监听器只需检查这个变量决定是否触发提示。我们把这个变量称为shouldPromptBeforeUnload。它的更新时机必须严格绑定在 Vue 的响应式变化之后且不能依赖watch的异步队列因为beforeunload是同步事件。最佳实践是在所有可能修改表单状态的操作后立即同步设置该标志位。例如一个文本输入框template input v-modelformData.title inputmarkAsDirty / /template script export default { data() { return { formData: { title: } } }, methods: { markAsDirty() { // 关键这里不是简单设 this.isDirty true而是同步更新全局标志 window.__VUE_UNLOAD_PROMPT__ true; // 同时为了 Vue 内部状态一致也更新响应式数据 this.isDirty true; } } } /script2.3 为什么unload被废弃它和beforeunload的本质区别网络热词里频繁出现permissions policy violation: unload is not allowed这源于 Chrome 80 对unload事件的严格限制。unload在页面完全卸载后触发此时 DOM 已销毁JavaScript 执行环境即将关闭你无法进行任何有意义的交互比如弹窗、AJAX 请求只能做极轻量的日志记录。而beforeunload发生在卸载前浏览器仍保持完整执行环境允许你阻止默认行为即显示提示框。但beforeunload的权限也被收紧它只允许返回字符串且浏览器会忽略你的文案统一显示标准提示。这是为了防止恶意网站用恐吓性文案如“您的电脑已被感染”诱导用户点击“取消”。所以所谓“自定义弹窗”从技术上就是不可能的——你只能告诉浏览器“我有未保存内容请让用户确认”至于怎么显示由浏览器决定。注意beforeunload的触发条件非常苛刻。它只在用户主动触发页面卸载时生效比如点击关闭标签页、点击刷新按钮、在地址栏输入新 URL 回车。它不会在以下情况触发用户通过 Vue Router 编程式导航如router.push()用户点击a标签跳转页面因 JavaScript 错误崩溃浏览器进程被强制杀死。 这意味着单靠beforeunload无法覆盖所有“数据丢失”场景必须结合路由守卫。3. 核心细节解析从零搭建一套健壮的防丢失方案一个真正可用的方案必须解决四个核心问题状态检测的准确性、事件监听的可靠性、跨组件状态共享、以及浏览器兼容性兜底。下面逐层拆解。3.1 状态检测什么是“有未保存内容”不能只看v-model绑定很多教程教你在v-model的input里设isDirty true这太粗糙了。真实业务中“未保存”意味着表单字段值与初始值不同富文本编辑器内容与初始 HTML 不同文件上传列表有新增/删除表格行被添加、编辑或删除甚至包括“用户点击了‘保存’按钮但 AJAX 还在 pending 中”。所以状态检测必须是语义化的、可配置的、可复用的。我们设计一个useFormDirty组合式函数Vue 3或 mixinVue 2// composables/useFormDirty.js import { ref, watch, onBeforeUnmount } from vue export function useFormDirty(initialData, options {}) { const { // 检测精度shallow浅比较、deep深比较默认、custom自定义函数 compareMode deep, // 自定义比较函数接收 (current, initial) boolean customCompare null, // 是否在首次加载时就标记为脏比如从服务器拉取数据后默认算“已修改” autoMarkOnLoad false } options const isDirty ref(autoMarkOnLoad) const initialRef ref(JSON.parse(JSON.stringify(initialData))) // 深比较工具函数简化版生产环境建议用 lodash.isEqual const deepEqual (a, b) { if (a b) return true if (!a || !b || typeof a ! object || typeof b ! object) return false const keysA Object.keys(a), keysB Object.keys(b) if (keysA.length ! keysB.length) return false for (let key of keysA) { if (!keysB.includes(key)) return false if (!deepEqual(a[key], b[key])) return false } return true } const markAsDirty () { isDirty.value true } const markAsClean () { isDirty.value false // 更新初始快照 initialRef.value JSON.parse(JSON.stringify(initialData)) } // 监听 initialData 变化比如从 API 重新加载数据 watch(() initialData, (newVal) { if (newVal compareMode deep) { // 如果新数据与当前快照不同则重置脏状态 if (!deepEqual(newVal, initialRef.value)) { markAsClean() } } }, { immediate: true }) // 返回响应式状态和方法 return { isDirty, markAsDirty, markAsClean, // 提供一个“是否应该提示”的计算属性避免外部直接读 isDirty shouldPrompt: isDirty } }这个函数的关键在于它不依赖watch的异步更新markAsDirty()是同步调用initialRef存储的是初始数据的深拷贝避免引用污染compareMode支持不同精度比如对于大型数组shallow比较length和首尾元素即可避免性能损耗autoMarkOnLoad解决了“用户打开编辑页数据已存在但尚未修改此时不应提示”的问题。3.2 全局标志位为什么用window.__VUE_UNLOAD_PROMPT__而不是localStoragebeforeunload监听器需要一个全局、同步、低延迟的访问方式。localStorage是同步 API但写入时会触发storage事件且在某些浏览器中存在微小延迟sessionStorage同理。而window对象上的属性是纯内存操作毫秒级响应。我们约定一个全局变量名window.__VUE_UNLOAD_PROMPT__双下划线前缀表示私有避免冲突。在useFormDirty的markAsDirty里同步赋值const markAsDirty () { isDirty.value true window.__VUE_UNLOAD_PROMPT__ true // 关键同步写入全局标志 }同时在markAsClean里清除const markAsClean () { isDirty.value false window.__VUE_UNLOAD_PROMPT__ false }这样beforeunload监听器只需读取这个变量const handleBeforeUnload (e) { if (window.__VUE_UNLOAD_PROMPT__) { e.preventDefault() e.returnValue // 必须返回空字符串或任意字符串否则不生效 } }3.3 Vue 2 与 Vue 3 的集成方式mixin vs composableVue 2Options API使用 mixin 封装避免每个组件重复写逻辑。// mixins/formDirtyMixin.js export default { data() { return { isFormDirty: false } }, methods: { markFormAsDirty() { this.isFormDirty true window.__VUE_UNLOAD_PROMPT__ true }, markFormAsClean() { this.isFormDirty false window.__VUE_UNLOAD_PROMPT__ false } } } // 在组件中使用 export default { mixins: [formDirtyMixin], data() { return { formData: { name: , email: } } }, mounted() { // 初始化时将 formData 快照存入 mixin 的内部状态 this.initialData JSON.parse(JSON.stringify(this.formData)) }, watch: { formData: { handler() { // 比较当前值与初始值 const isDifferent JSON.stringify(this.formData) ! JSON.stringify(this.initialData) if (isDifferent) { this.markFormAsDirty() } else { this.markFormAsClean() } }, deep: true } } }Vue 3Composition API使用onBeforeUnmount清理监听器这是 Vue 3 的优势。script setup import { onBeforeUnmount, onMounted } from vue import { useFormDirty } from /composables/useFormDirty const initialData { name: , email: } const { isDirty, markAsDirty, markAsClean, shouldPrompt } useFormDirty(initialData) // 在 input 事件中调用 const handleInput () { markAsDirty() } // 组件卸载时确保清理全局标志虽然 beforeunload 会自己处理但保险起见 onBeforeUnmount(() { if (window.__VUE_UNLOAD_PROMPT__) { window.__VUE_UNLOAD_PROMPT__ false } }) // 全局 beforeunload 监听器只注册一次 onMounted(() { const handleBeforeUnload (e) { if (window.__VUE_UNLOAD_PROMPT__) { e.preventDefault() e.returnValue } } window.addEventListener(beforeunload, handleBeforeUnload) // 卸载时移除监听器 onBeforeUnmount(() { window.removeEventListener(beforeunload, handleBeforeUnload) }) }) /script3.4 路由守卫弥补beforeunload的盲区beforeunload只管浏览器级操作不管 Vue Router 的编程式导航。用户点击“保存并返回列表页”如果router.push(/list)时表单是脏的beforeunload不会触发。这时必须用router.beforeEach拦截// router/index.js router.beforeEach((to, from, next) { // 检查 from 页面是否有未保存内容 if (from.meta?.requiresSave window.__VUE_UNLOAD_PROMPT__) { const answer window.confirm(你有未保存的内容确定要离开吗) if (answer) { next() } else { next(false) // 取消导航 } } else { next() } })但window.confirm是阻塞式弹窗体验差。更好的做法是在目标路由组件中通过beforeRouteLeave守卫结合next(false)和自定义 UI 弹窗script setup import { onBeforeRouteLeave } from vue-router onBeforeRouteLeave((to, from, next) { if (window.__VUE_UNLOAD_PROMPT__) { // 显示自定义弹窗如 Element Plus 的 Dialog showCustomDialog().then((confirmed) { if (confirmed) { next() } else { next(false) } }) } else { next() } }) /script这样beforeunload负责浏览器原生操作路由守卫负责 SPA 内部导航两者互补。4. 实操过程从零开始一步步实现一个可复用的防丢失模块现在我们把前面所有设计整合成一个开箱即用的模块。假设你正在开发一个 Vue 3 Vite 的项目目标是为所有编辑页面提供统一的防丢失能力。4.1 第一步创建核心 HookusePageUnloadGuard新建文件src/composables/usePageUnloadGuard.jsimport { ref, onMounted, onBeforeUnmount, getCurrentInstance } from vue import { useRouter, useRoute } from vue-router // 全局标志位确保只初始化一次 let isInitialized false const unloadPromptFlag ref(false) // 初始化全局监听器 const initGlobalListener () { if (isInitialized) return const handleBeforeUnload (e) { if (unloadPromptFlag.value) { e.preventDefault() e.returnValue // 必须返回字符串 } } window.addEventListener(beforeunload, handleBeforeUnload) // 清理函数 const cleanup () { window.removeEventListener(beforeunload, handleBeforeUnload) } // 在页面卸载时清理虽然浏览器会自动清理但显式调用更稳妥 window.addEventListener(unload, cleanup) isInitialized true return cleanup } // 主 Hook export function usePageUnloadGuard(options {}) { const { // 是否启用可用于某些页面禁用 enabled true, // 自定义提示文案仅用于自定义弹窗不影响 beforeunload message 你有未保存的内容确定要离开吗 } options const router useRouter() const route useRoute() const instance getCurrentInstance() // 标志位是否应该提示 const shouldPrompt ref(false) // 设置提示状态 const setPrompt (value) { shouldPrompt.value value unloadPromptFlag.value value } // 清除提示状态 const clearPrompt () { shouldPrompt.value false unloadPromptFlag.value false } // 初始化全局监听器只在第一个调用时执行 if (!isInitialized) { const cleanup initGlobalListener() // 组件卸载时清理 onBeforeUnmount(() { if (cleanup) cleanup() }) } // 如果启用了路由守卫则注册 if (enabled router) { // 在组件内注册路由守卫 if (instance) { instance.proxy?.$options.beforeRouteLeave [ (to, from, next) { if (shouldPrompt.value) { // 这里可以调用你自己的 UI 弹窗服务 // 例如import { useDialog } from /composables/useDialog // const { openConfirm } useDialog() // openConfirm(message).then(confirmed { // if (confirmed) next() // else next(false) // }) // 为演示这里用原生 confirm实际项目请替换 if (window.confirm(message)) { next() } else { next(false) } } else { next() } } ] } } return { shouldPrompt, setPrompt, clearPrompt, // 提供一个便捷方法当表单提交成功后自动清除 onFormSubmitSuccess: () { clearPrompt() } } }4.2 第二步在编辑组件中使用创建一个编辑页面src/views/EditUser.vuetemplate div classedit-page h2编辑用户/h2 form submit.preventhandleSubmit div classform-group label姓名/label input v-modelformData.name inputhandleInput / /div div classform-group label邮箱/label input v-modelformData.email inputhandleInput / /div button typesubmit保存/button button typebutton clickhandleCancel取消/button /form /div /template script setup import { ref, reactive, onMounted } from vue import { usePageUnloadGuard } from /composables/usePageUnloadGuard // 模拟从 API 获取的初始数据 const initialData { name: 张三, email: zhangsanexample.com } const formData reactive({ ...initialData }) // 使用防丢失 Hook const { shouldPrompt, setPrompt, clearPrompt, onFormSubmitSuccess } usePageUnloadGuard({ message: 您修改了用户信息尚未保存确定要离开吗 }) // 输入时标记为脏 const handleInput () { setPrompt(true) } // 取消编辑 const handleCancel () { // 重置表单 Object.assign(formData, initialData) clearPrompt() } // 提交表单 const handleSubmit async () { try { // 模拟 API 调用 await new Promise(resolve setTimeout(resolve, 500)) // 保存成功清除提示 onFormSubmitSuccess() // 跳转回列表页 // router.push(/users) } catch (error) { console.error(保存失败, error) } } // 组件挂载时检查初始数据是否与当前表单一致 onMounted(() { const isSame JSON.stringify(formData) JSON.stringify(initialData) if (!isSame) { setPrompt(true) } }) /script style scoped .edit-page { max-width: 600px; margin: 0 auto; padding: 20px; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; font-weight: bold; } input { width: 100%; padding: 8px; border: 1px solid #ccc; } button { margin-right: 10px; padding: 8px 16px; } /style4.3 第三步处理复杂表单富文本、文件上传真实项目中表单往往不止input。比如富文本编辑器quill或tiptap它们的内容不是通过v-model绑定的需要手动监听template div client-only tiptap-editor :contentformData.content updatehandleContentUpdate / /client-only /div /template script setup import { ref } from vue import { usePageUnloadGuard } from /composables/usePageUnloadGuard const { setPrompt } usePageUnloadGuard() const formData ref({ content: p初始内容/p }) const handleContentUpdate (newContent) { // Tiptap 的 update 事件传递的是 HTML 字符串 if (newContent ! formData.value.content) { formData.value.content newContent setPrompt(true) } } /script对于文件上传监听input[typefile]的change事件input typefile changehandleFileChange / script setup const handleFileChange (e) { const files e.target.files if (files.length 0) { // 将文件列表转为可序列化的对象如文件名、大小 const fileInfos Array.from(files).map(f ({ name: f.name, size: f.size })) // 存入 formData formData.value.attachments fileInfos setPrompt(true) } } /script4.4 第四步兼容性处理与降级方案尽管beforeunload在现代浏览器中广泛支持但仍有例外iOS Safari 15.4 为防止滥用完全禁用了beforeunload的提示功能即使你写了e.preventDefault()也不会弹窗某些企业内网浏览器如旧版 IE可能不支持beforeunloadPWA 应用在离线模式下行为异常。因此必须提供降级方案视觉反馈 明确的保存状态指示。在页面顶部添加一个醒目的状态条div v-ifshouldPrompt classsave-status-bar span⚠️ 您有未保存的更改/span button clickhandleSave立即保存/button button clickclearPrompt放弃更改/button /divCSS 样式.save-status-bar { position: fixed; top: 0; left: 0; right: 0; background: #fff3cd; color: #856404; padding: 12px 20px; text-align: center; z-index: 1000; border-bottom: 1px solid #ffeaa7; display: flex; justify-content: space-between; align-items: center; } .save-status-bar button { background: #ffc107; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; }这个状态条在shouldPrompt为true时显示用户可以一键保存或放弃既解决了 iOS Safari 的兼容性问题又提升了整体用户体验——毕竟比起突兀的浏览器弹窗一个清晰的状态提示更友好。5. 常见问题与排查技巧实录那些让你抓狂的“为什么没弹窗”在实际项目中我踩过的坑比写过的代码还多。下面整理出最典型的 7 个问题附带排查步骤和终极解决方案。5.1 问题一Chrome 控制台报[Violation] Permissions Policy violation: unload is not allowed现象控制台红色报错但beforeunload逻辑似乎还能运行。原因分析这不是beforeunload的问题而是你代码里不小心调用了window.unload或document.addEventListener(unload, ...)。现代浏览器Chrome 95已完全禁用unload事件任何对其的监听都会触发此警告。排查步骤在控制台搜索unload定位到所有相关代码检查是否在mounted或created钩子中写了window.addEventListener(unload, ...)检查第三方库如某些老版本的 analytics SDK是否内部调用了unload。解决方案彻底删除所有unload相关代码将unload的逻辑迁移到beforeunload中注意beforeunload里不能做耗时操作只能做快速判断如果必须做卸载日志改用navigator.sendBeacon()// 替代 unload 的日志上报 const sendUnloadLog () { const logData { page: location.href, timestamp: Date.now() } navigator.sendBeacon(/api/log, JSON.stringify(logData)) } window.addEventListener(beforeunload, () { sendUnloadLog() // 注意sendBeacon 是异步且可靠的 })5.2 问题二Vue 2 项目中beforeunload完全不触发现象代码写得和文档一模一样但关页面就是没反应。原因分析Vue 2 的mounted钩子执行时this指向当前组件实例但window.addEventListener的回调函数里的this是window无法访问组件数据。更致命的是Vue 2 的data是函数返回的对象this在事件回调里是undefined。排查步骤在mounted里console.log(this)确认this是否为组件实例在beforeunload回调里console.log(this)确认this是否为window检查beforeunload回调里是否用了this.xxx访问数据。解决方案使用箭头函数保持this上下文mounted() { // ❌ 错误普通函数this 指向 window // window.addEventListener(beforeunload, function(e) { ... }) // ✅ 正确箭头函数this 指向组件实例 window.addEventListener(beforeunload, (e) { if (this.isDirty) { e.preventDefault() e.returnValue } }) }或者将状态判断逻辑提前只在回调里读取全局变量mounted() { // 在 mounted 里设置全局标志 this.$nextTick(() { window.__VUE_UNLOAD_PROMPT__ this.isDirty }) window.addEventListener(beforeunload, (e) { if (window.__VUE_UNLOAD_PROMPT__) { e.preventDefault() e.returnValue } }) }5.3 问题三用户刷新页面后提示框一闪而过来不及点击“离开”现象beforeunload提示框出现不到 1 秒就自动关闭。原因分析这是 Chrome 的“防滥用”策略。如果你在beforeunload回调里执行了耗时操作如console.log大量数据、调用fetch、触发 Vue 的nextTick浏览器会认为你在阻塞主线程从而强制关闭提示框。排查步骤在beforeunload回调里只留最简代码e.preventDefault(); e.returnValue ;逐步添加其他代码观察何时出现闪退检查是否有console.table、JSON.stringify大对象等操作。解决方案beforeunload回调里只做一件事读取全局标志位并返回所有状态计算、日志记录、API 调用必须在beforeunload之前完成使用performance.now()监控回调执行时间确保 1ms。5.4 问题四Vue Router 编程式导航router.push不触发提示现象用户点击“保存并返回”页面跳走了但没提示。原因分析beforeunload只监听浏览器原生卸载router.push是前端路由跳转不触发页面卸载。排查步骤确认导航是否为router.push或router.replace检查是否在目标路由组件中注册了beforeRouteLeave守卫检查beforeRouteLeave是否被next(false)正确拦截。解决方案如前所述必须在目标组件即“离开的页面”中使用beforeRouteLeave如果是全局拦截使用router.beforeEach但要注意from.meta.requiresSave的设置// 在编辑页的路由配置中添加 meta { path: /user/edit/:id, component: EditUser, meta: { requiresSave: true } }5.5 问题五表单提交后再次进入编辑页shouldPrompt仍为true现象用户保存成功跳转到列表页再点进同一个用户编辑页面顶部显示“有未保存更改”。原因分析shouldPrompt状态没有在页面离开时重置。beforeunload只在关页面时触发router.push不会触发它所以标志位一直为true。排查步骤在beforeRouteLeave守卫中next()之后是否调用了clearPrompt()在onUnmounted钩子中是否清除了全局标志检查usePageUnloadGuard的clearPrompt方法是否被正确调用。解决方案在beforeRouteLeave的next()回调中清除状态onBeforeRouteLeave((to, from, next) { if (shouldPrompt.value) { if (confirm(...)) { clearPrompt() // ✅ 关键在确认后清除 next() } else { next(false) } } else { next() } })或者在目标页面列表页的mounted钩子中主动清除// ListUsers.vue mounted() { // 离开编辑页后重置全局状态 window.__VUE_UNLOAD_PROMPT__ false }5.6 问题六iOS Safari 下完全不弹窗且无任何报错现象在 iPhone 上测试关页面、刷新一点反应都没有。原因分析iOS Safari 15.4 出于隐私考虑**彻底移除了 beforeunload
阅读完成 · 觉得有帮助?
咨询建站