跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

Next.js 与 Chakra UI:实现页面未保存修改离页提示与导航控制

本文详细介绍了在 Next. js 应用中,如何结合 Chakra UI 实现用户离页时未保存修改的提示功能。通过自定义 useNavigationObserver Hook,巧妙地拦截 Next.js 路由 跳转事件,阻止默认导航行为,并提供弹窗询问用户是否继续。用户确认后,再手动导航至目标页面,确保数据完整性与用户体验。 背景与问题分析 在现代 Web 应用中,当用户在表单页面进行编辑但尚未保存时,如果尝试离开当前页面(例如点击导航链接或使用 浏览器 返回按钮),通常需要一个提示来防止 数据丢失 。在 Next.js 应用中,虽然浏览器提供了 beforeunload 事件来处理页面卸载,但它无法直接控制 Next.js 的客户端路由跳转。Next.js 的 router.events.on('routeChangeStart') 事件可以监听路由变化,但默认情况下,它并不能直接阻止路由的完成,这导致即使检测到未保存的更改并尝试打开弹窗,路由仍可能继续,弹窗也无法正常显示。 核心挑战在于: 阻止默认路由行为 :在 routeChangeStart 阶段有效阻止 Next.js 的路由跳转。 保留目标路径 :在阻止路由后,能够记住用户最初想要跳转到的目标路径。 恢复导航 :当用户确认离开后,能够重新触发到目标路径的导航。 UI 交互 :结合 UI 组件库(如 Chakra UI)显示模态对话框。 解决方案:自定义 useNavigationObserver Hook 为了解决上述问题,我们可以创建一个自定义的 useNavigationObserver Hook。这个 Hook 的核心思想是利用 Next.js 路由事件的特性,通过“抛出假错误”的方式来中断路由跳转,并在用户确认后,手动恢复导航。 useNavigationObserver Hook 代码解析
import { useRouter } from "next/router"; import { useCallback, useEffect, useRef } from "react"; // 定义一个独特的错误消息,用于识别并阻止路由错误 const errorMessage = "Please ignore this error."; // 抛出一个假错误以欺骗 Next.js 路由 const throwFakeErrorToFoolNextRouter = () => { // eslint-disable-next-line no-throw-literal throw errorMessage; }; // 拦截并阻止 Next.js 内部的 PromiseRejectionEvent,防止假错误被报告 const rejectionHandler = (event: PromiseRejectionEvent) => { if (event.reason === errorMessage) { event.preventDefault(); // 阻止默认行为,即阻止控制台报告错误 } }; interface Props { shouldStopNavigation: boolean; // 是否需要阻止导航的标志 onNavigate: () => void; // 当导航被阻止时触发的回调,用于打开提示弹窗 } const useNavigationObserver = ({ shouldStopNavigation, onNavigate }: Props) => { const router = useRouter(); const currentPath = router.asPath; // 当前页面路径 const nextPath = useRef(""); // 存储用户尝试跳转的目标路径 const navigationConfirmed = useRef(false); // 标记用户是否已确认离开 // 阻止路由事件并抛出假错误 const killRouterEvent = useCallback(() => { // 触发 'routeChangeError' 事件,Next.js 会认为路由失败 router.events.emit("routeChangeError", "", "", { shallow: false }); throwFakeErrorToFoolNextRouter(); // 抛出假错误以中断 Promise 链 }, [router]); useEffect(() => { navigationConfirmed.current = false; // 每次组件挂载或依赖更新时重置确认状态 const onRouteChange = (url: string) => { // 如果 URL 已经改变,但我们想阻止导航,需要将浏览器历史状态推回当前路径 // 这是因为在 routeChangeStart 发生时,浏览器地址栏可能已经更新了 if (currentPath !== url) { window.history.pushState(null, "", router.basePath + currentPath); } // 只有当满足以下条件时才阻止导航: // 1. shouldStopNavigation 为 true (即有未保存的更改) // 2. 目标 URL 与当前 URL 不同 // 3. 用户尚未确认导航 if ( shouldStopNavigation && url !== currentPath && !navigationConfirmed.current ) { nextPath.current = url.replace(router.basePath, ""); // 存储目标路径 onNavigate(); // 调用传入的回调函数,通常用于打开弹窗 killRouterEvent(); // 阻止路由继续 } }; router.events.on("routeChangeStart", onRouteChange); window.addEventListener("unhandledrejection", rejectionHandler); // 监听未处理的 Promise 拒绝 return () => { router.events.off("routeChangeStart", onRouteChange); window.removeEventListener("unhandledrejection", rejectionHandler); }; }, [ currentPath, killRouterEvent, onNavigate, router.basePath, router.events, shouldStopNavigation, ]); // 用户确认离开后,调用此函数以继续导航 const confirmNavigation = () => { navigationConfirmed.current = true; // 标记已确认 router.push(nextPath.current); // 导航到之前存储的目标路径 }; return confirmNavigation; }; export { useNavigationObserver };
Hook 关键点解释: throwFakeErrorToFoolNextRouter & rejectionHandler : Next.js 内部的路由跳转是基于 Promise 实现的。当 routeChangeStart 触发后,如果后续的 Promise 链被中断(例如抛出错误),Next.js 会认为路由失败并停止导航。 我们通过 router.events.emit("routeChangeError") 配合 throw errorMessage 来模拟一个路由错误,从而中断正常的路由流程。 rejectionHandler 监听全局的 unhandledrejection 事件,捕获并阻止我们抛出的假错误被浏览器控制台报告,保持控制台的整洁。 window.history.pushState : 在 routeChangeStart 事件触发时,Next.js 可能会在内部更新浏览器地址栏的 URL,即使路由尚未完成。 为了在阻止导航后将地址栏 URL 恢复到当前页面的路径,我们使用 window.history.pushState(null, "", router.basePath + currentPath)。这确保了用户在看到提示弹窗时,浏览器地址栏仍然显示当前页面的 URL。 nextPath 和 navigationConfirmed : nextPath useRef 用于存储用户最初尝试访问的目标路径。 navigationConfirmed useRef 是一个标志,当用户在弹窗中选择“是”时,将其设置为 true。这使得 confirmNavigation 函数可以绕过 shouldStopNavigation 检查,直接进行导航。 confirmNavigation : Comet AI 浏览器 下载 Comet AI 浏览器,体验由 Perplexity AI 驱动的革命性上网方式。内置 AI 助手可实时总结网页、跨标签页对比信息、自动执行任务。告别繁琐操作,让 AI 成为你的浏览副驾,大幅提升研究与工作效率。支持 Windows、macOS、Android 和 iOS。 下载 这是 useNavigationObserver Hook 返回的函数。当用户在提示弹窗中点击“是”时,调用此函数。它将 navigationConfirmed 设置为 true,然后使用 router.push(nextPath.current) 重新发起导航到用户最初选择的页面。 在 Next.js 组件中集成 现在,我们将 useNavigationObserver Hook 集成到 Next.js 组件中,以实现离页提示功能。
import { useState } from "react"; import { Box, Grid, GridItem, Input, Flex, Button, useColorModeValue, useDisclosure, // Chakra UI Hook for managing modal state } from "@chakra-ui/react"; // ... 其他导入,如 PasswordEditor, TopNav, services, showMsg, deep-equal 等 import { useNavigationObserver } from "@/hooks/useNavigationObserver"; // 导入自定义 Hook const RecordEditing: React.FC = ({ type, record, user, }) => { const [recordObj, setRecordObj] = useState(record); const [password, setPassword] = useState(record.password); const [isDirty, setIsDirty] = useState(false); // 跟踪表单是否有未保存的更改 const defaultRecord = { ...record, password }; // 初始记录状态,用于比较 const title = type === "new" ? "New Record" : "Edit Record"; const router = useRouter(); const { recordId } = router.query; const buttonBg = useColorModeValue("#dbdbdb", "#2a2c38"); // 使用 Chakra UI 的 useDisclosure 管理弹窗的打开/关闭状态 const { isOpen, onOpen, onClose } = useDisclosure(); // 使用自定义的 useNavigationObserver Hook const navigate = useNavigationObserver({ shouldStopNavigation: isDirty, // 当 isDirty 为 true 时阻止导航 onNavigate: () => onOpen(), // 导航被阻止时打开 Chakra UI 弹窗 }); // 检查表单数据是否与初始数据不同,更新 isDirty 状态 const setDirtyInputs = () => { if (!isDeepEqual(defaultRecord, { ...recordObj, password })) { setIsDirty(true); } else { setIsDirty(false); } }; // 处理输入变化,并更新 isDirty 状态 const handleInputChange = (e: React.ChangeEvent) => { setRecordObj((prevState) => ({ ...prevState, [e.target.id]: e.target.value, })); setDirtyInputs(); // 每次输入变化后检查脏状态 }; // 处理表单提交 const handleSubmit = () => { setIsDirty(false); // 提交后重置脏状态 // ... 保存逻辑 ... if (type === "new") { postMethod(`/api/user/${user.id}/records`, { ...recordObj, password, }) .then(() => router.push("/")) .then(() => showMsg("Record saved", { type: "success" })) .catch(() => showMsg("Something went wrong", { type: "error" })); } else { updateMethod(`/api/user/${user.id}/records/${recordId}`, { ...recordObj, password, }) .then(() => router.push("/")) .then(() => showMsg("Record updated", { type: "success" })) .catch(() => showMsg("Something went wrong", { type: "error" })); } }; return ( {/* 表单输入字段 */} Title {/* 其他输入字段 ... */} {/* Chakra UI 弹窗组件 */} {/* 假设 AlertModal 是一个自定义组件,内部封装了 Chakra UI 的 AlertDialog */} ); }; export default RecordEditing;
组件集成关键点: isDirty 状态管理 : isDirty 是一个布尔状态,用于指示表单数据是否与初始加载的数据不同。 setDirtyInputs 函数负责比较当前表单数据 (recordObj 和 pass word ) 与 defaultRecord (初始数据) 的深层相等性,并更新 isDirty 状态。 handleInputChange 在每次输入变化时调用 setDirtyInputs。 handleSubmit 在数据保存成功后将 isDirty 重置为 false。 useDisclosure : Chakra UI 提供的 useDisclosure Hook 简化了模态框、抽屉等组件的打开/关闭状态管理,提供了 isOpen、onOpen 和 onClose。 useNavigationObserver 的使用 : 通过 const navi gate = useNavigationObserver({ shouldStopNavigation: isDirty, onNavigate: () => onOpen() }); 初始化 Hook。 shouldStopNavigation 被设置为 isDirty,这意味着只有当表单有未保存的更改时,Hook 才会尝试阻止导航。 onNavigate 回调设置为 onOpen(),当导航被阻止时,它会触发 Chakra UI 弹窗的显示。 AlertModal (或 AlertDialog) : AlertModal 是一个用于显示提示信息的自定义组件,它接收 isOpen、onClose 和 callBackAction 作为 props。 当用户点击“是”(确认离开)时,AlertModal 会调用 callBackAction,即我们从 useNavigationObserver Hook 返回的 navigate 函数,从而恢复到目标路径的导航。 当用户点击“否”(取消离开)时,AlertModal 会调用 onClose() 关闭弹窗,用户停留在当前页面。 注意事项与总结 “假错误”机制 : 这种通过抛出错误来阻止 Next.js 路由的方式,虽然有效,但本质上是一种利用框架内部机制的“hack”。在未来的 Next.js 版本中,其内部路由实现可能会发生变化,从而影响此方法的兼容性。 浏览器 beforeunload : 这种方法主要针对 Next.js 的客户端路由跳转。对于用户直接关闭浏览器标签页或输入新 URL 的情况,beforeunload 事件仍然是更合适的选择(尽管它只能显示一个浏览器内置的确认提示,无法自定义 UI)。 用户体验 : 确保提示信息清晰明了,让用户明白离开页面会丢失未保存的更改。 性能 : deep-equal 库在处理大型对象时可能会有性能开销。如果表单数据非常复杂,可以考虑更优化的脏检查策略。 通过 useNavigationObserver Hook,我们成功地在 Next.js 应用中实现了一个健壮的离页提示功能,极大地提升了用户体验和数据安全性。这种模式提供了一个可复用的解决方案,可以轻松集成到任何需要此功能的表单组件中。

相关文章