背景
近期大家在使用marsview
低代码平台搭建页面时,反馈了一个很好的问题:我在当前页面做了很多配置,一不小心手滑,导致页面返回了,所有内容都丢失了,你应该加一个定时保存或者返回二次确认的功能。
这个提议特别好,所以还是广大用户在使用后,才能给出宝贵的建议。
最终效果:
技术实现
关于浏览器返回,我们通常做法就是监听popstate
事件,还有一些方案就是react-router-dom
提供的一些hooks
实现。
popstate 实现
1. 监听浏览器返回 在编辑器页面渲染完成后,添加监听事件
useEffect(()=>{
const handlePopState = ()=>{
alert('确认返回吗?')
}
window.addEventListener('popstate',handlePopState)
return ()=>{
window.removeEventListener('popstate',handlePopState)
}
},[])
效果:
监听以后,确实可以阻断当前页面返回,但是我们发现点击确定后,依然返回了。也就是说,你能监听到,你也能添加动作,但是你无法阻止页面返回。
我尝试通过event.preventDefault()
或者Promise
都不能阻断,说明此方案有些问题。
2. 思考变通方案
浏览器之所以有返回和前进按钮,是因为里面有页面,页面通过history
管理页面的出入,既然popstate
事件无法阻断浏览器返回,我们是否可以制造假象,默认进去提前push
一个当前页面,这样返回的时候依然是当前页面?
useEffect(()=>{
history.pushState(null,'', location.pathname)
const handlePopState = ()=>{
alert('确认返回吗?')
}
window.addEventListener('popstate',handlePopState)
return ()=>{
window.removeEventListener('popstate',handlePopState)
}
},[])
效果测试:经过测试发现,这次返回,确实能够保留在当前页面。
3. 功能完善
useEffect(()=>{
history.pushState(null,'', location.pathname)
const handlePopState = ()=>{
Modal.confirm({
title: '确认离开',
content: '是否确认离开当前页面',
onOk: ()=>{
history.back();
},
onCancel: ()=>{
history.pushState(null,'', location.pathname)
}
})
}
window.addEventListener('popstate',handlePopState)
return ()=>{
window.removeEventListener('popstate',handlePopState)
}
},[])
这段代码很微妙,当监听到浏览器返回时,执行handlePopState
函数,弹出确认框,如果选择取消,则继续添加一条记录,这样做的目的是,下一次的返回依然是当前页面。
如果选择确定,那我们这次就要真的返回上一个页面了,直接调用history.back()
函数。
效果图:
当我很高兴的以为这样就结束的时候,突然发现了一个问题:点击确定按钮,不仅没有返回到上一个页面,同时又弹了一个确认框?
4. 解决死循环问题
仔细分析了一下代码,发现是执行history.back()
后触发的,那此时怎么呢?我有两个思路,一个是打个标记,如果点击过确定后,就不要执行该函数了,另外一个就是点击确定后,直接移出该监听事件。
useEffect(()=>{
history.pushState(null,'', location.pathname)
const handlePopState = ()=>{
Modal.confirm({
title: '确认离开',
content: '是否确认离开当前页面',
onOk: ()=>{
window.removeEventListener('popstate',handlePopState)
history.back();
},
onCancel: ()=>{
history.pushState(null,'', location.pathname)
}
})
}
window.addEventListener('popstate',handlePopState)
return ()=>{
window.removeEventListener('popstate',handlePopState)
}
},[])
重点就是,点击确定按钮后,直接移除该事件,这样就不会陷入死循环了。
注意:一般我们针对比较重要的编写页面在返回时添加二次弹框确认,但是为了更好的用户体验,建议针对用户是否编辑做一个判断,假如用户并没有编辑内容,尽量还是直接返回,只有发现内容变化了,再二次确认。
beforeunload 实现
看了一下官网解释,beforeunload
事件在用户关闭或者刷新时触发。但,我们要监听的是页面返回,所以并不能完全使用该事件实现,但是可以解决用户关闭页面或者刷新页面这个场景。
MDN解释:developer.mozilla.org/zh-CN/docs/…
1. 代码实现
useEffect(()=>{
const handlePopState = (event)=>{
Modal.confirm({
title: '确认离开',
content: '是否确认离开当前页面',
onOk: ()=>{
// TODO
},
onCancel: ()=>{
// TODO
}
})
event.preventDefault(); // 阻止默认行为(在某些浏览器中可能无效)
event.returnValue = ''; // 设置返回值为空字符串在某些浏览器中有效
return false;
}
window.addEventListener('beforeunload',handlePopState)
return ()=>{
window.removeEventListener('beforeunload',handlePopState)
}
},[])
我们必须在函数末尾通过event
和return
来阻止默认行为,从而阻止浏览器返回。
效果:
我们刷新页面时,发现浏览器自带有一个检测弹框,如果点击确定,直接就刷新了,点击取消才会弹我们自己的框。
点击浏览器自带的弹框取消按钮后,才会显示我们自己的弹框。
2. 点击确定按钮返回上一页
通过event.preventDefault()
和return false
后,其实浏览器已经被阻断了,我们弹框只是给用户看的,但是当用户点击确定后,我们需要手动返回上一页。
返回上一页依然使用history.back()
函数。
...代码省略
onOk: ()=>{
history.back();
},
...代码省略
点击取消其实不用管了,因为页面已经被阻断了。
react-router 实现
其实前面两种方案结合起来挺不错的,只不过要写很多代码,突然发现react-router-dom
也有这样的功能实现。
注意:我是基于react-router-dom@6.21.2
最新版本实现的,网上提到的useHistory
或者Prompt
等都是老版本,新版本已经不支持了。
代码实现
import { useBlocker } from 'react-router-dom'
export default function Editor(){
// 判断页面地址是否发生变化
const blocker = useBlocker(({ currentLocation, nextLocation }) => {
return currentLocation.pathname !== nextLocation.pathname;
});
useEffect(()=>{
if (blocker.state === 'blocked') {
Modal.confirm({
title: '确认离开',
content: '是否确认离开当前页面?',
onOk: () => {
blocker.proceed();
},
onCancel: () => {
blocker.reset();
},
});
}
},[blocker])
reutrn <>....</>
}
效果如图:
但是,这个方案不能监听页面刷新和关闭事件,只能监听返回,有一定小瑕疵。
定时器方案
有时候我们担心浏览器返回丢失信息,那我们完全可以加一个定时器让他定时保存,这样可以让损失最小化,比如间隔15秒就保存一下。
可能很多开发者,又会提出各种挑战说,万一我在14秒就关闭页面了呢?没有很完备的方案,我们只是尽可能的去减少损失。
- 粗糙的方案就是:间隔一定时间调用一下保存接口。
- 监听内容的变化后,间隔几秒调用一下保存接口。
- 监听内容无变化多久后,自动调用保存接口。
总之,你需要在特别的时机下,调用保存接口。由于代码比较简单,我就不实现了。
总结
popstate
事件主要用来监听页面返回事件,无法阻断页面返回,需要通过变通的方法间接实现。beforeunload
事件只能监听页面关闭和刷新事件,可以通过脚本阻断页面返回。react-router-dom
提供的hook
函数useBlocker
同样可以实现页面返回监听,我们去自定义二次确认弹框。
温馨提示:
marsview
开源了所有代码,包括前后端,欢迎大家交流学习。
页面截图: