低代码编辑器如何监听页面返回?

681 阅读6分钟

背景

近期大家在使用marsview低代码平台搭建页面时,反馈了一个很好的问题:我在当前页面做了很多配置,一不小心手滑,导致页面返回了,所有内容都丢失了,你应该加一个定时保存或者返回二次确认的功能。

这个提议特别好,所以还是广大用户在使用后,才能给出宝贵的建议。

最终效果:

image.png

技术实现

关于浏览器返回,我们通常做法就是监听popstate事件,还有一些方案就是react-router-dom提供的一些hooks实现。

popstate 实现

1. 监听浏览器返回 在编辑器页面渲染完成后,添加监听事件

useEffect(()=>{
    const handlePopState = ()=>{
        alert('确认返回吗?')
    }
    window.addEventListener('popstate',handlePopState)
    return ()=>{
        window.removeEventListener('popstate',handlePopState)
    }
},[])

效果:

image.png

监听以后,确实可以阻断当前页面返回,但是我们发现点击确定后,依然返回了。也就是说,你能监听到,你也能添加动作,但是你无法阻止页面返回。

我尝试通过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()函数。

效果图:

image.png

当我很高兴的以为这样就结束的时候,突然发现了一个问题:点击确定按钮,不仅没有返回到上一个页面,同时又弹了一个确认框?

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)
    }
},[])

我们必须在函数末尾通过eventreturn来阻止默认行为,从而阻止浏览器返回。

效果:

image.png

我们刷新页面时,发现浏览器自带有一个检测弹框,如果点击确定,直接就刷新了,点击取消才会弹我们自己的框。

image.png

点击浏览器自带的弹框取消按钮后,才会显示我们自己的弹框。

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 <>....</>
}

效果如图:

image.png

但是,这个方案不能监听页面刷新和关闭事件,只能监听返回,有一定小瑕疵。

定时器方案

有时候我们担心浏览器返回丢失信息,那我们完全可以加一个定时器让他定时保存,这样可以让损失最小化,比如间隔15秒就保存一下。

可能很多开发者,又会提出各种挑战说,万一我在14秒就关闭页面了呢?没有很完备的方案,我们只是尽可能的去减少损失。

  1. 粗糙的方案就是:间隔一定时间调用一下保存接口。
  2. 监听内容的变化后,间隔几秒调用一下保存接口。
  3. 监听内容无变化多久后,自动调用保存接口。

总之,你需要在特别的时机下,调用保存接口。由于代码比较简单,我就不实现了。

总结

  1. popstate事件主要用来监听页面返回事件,无法阻断页面返回,需要通过变通的方法间接实现。
  2. beforeunload事件只能监听页面关闭和刷新事件,可以通过脚本阻断页面返回。
  3. react-router-dom提供的hook函数useBlocker同样可以实现页面返回监听,我们去自定义二次确认弹框。

温馨提示:

marsview开源了所有代码,包括前后端,欢迎大家交流学习。

开源地址:GitHub - JackySoft/marsview: Marsview 是一款中后台方向的低代码可视化搭建平台,开发者可以在平台上创建项目、页面和组件,支持事件交互、接口调用、数据联动和逻辑编排等,开发者还可通过微服务快速集成到自己的业务系统中。 Marsview is a low code visualization platform for middle and backend direction, supporting event interaction, interface calling, data linkage, and logical orchestration.

页面截图:

image.png

image.png

image.png