我给弹窗添加了支持物理返回键 一
背景介绍
我们知道, 对于 app 端的弹窗, 物理返回键可以关闭弹窗, 但是在 web 端, 按物理返回键, 直接就返回上一页了。
返回键关闭弹窗, 无疑交互体验更好, 那么怎么实现呢?
首先向大家推荐一个好用的弹窗管理库: NiceModal, 下列代码都是基于该库以及 mui (大多是伪代码, 当然可以不用库或者使用其他库)
利弊分析及思路分析
基本实现思路
因为返回键会改变 history, 所以我们的实现思路肯定是通过监听 popstate
来关闭弹窗:
- 打开弹窗时
history.pushState()
popstate
事件发生时关闭弹窗- 同时当我们手动关闭弹窗时, 需要
history.back()
恢复history
栈
好处
- 交互体验更好
坏处
- 由于需要
history.pushState()
和history.back()
, 会破坏用户的浏览记录- 用户本来的浏览记录从 A 页面跳转 B 页面, 再返回 A 页面, 此时用户本来可以通过浏览器的 "前进 (forward)" 按钮回到 B 页面的, 如果我们在 A 页面执行
history.pushState()
的话, 用户就无法通过back / forward
回到 B 页面了
- 用户本来的浏览记录从 A 页面跳转 B 页面, 再返回 A 页面, 此时用户本来可以通过浏览器的 "前进 (forward)" 按钮回到 B 页面的, 如果我们在 A 页面执行
期望的调用方式
暂且将我们的方法命名为 useInjectHistory
// 声明
const TestModal = NiceModal.create(() => {
const modal = useModal();
useInjectHistory(modal);
return <Dialog>test modal</Dialog>;
});
// 调用
NiceModal.show(TestModal);
但是调用方需要有阻止弹窗关闭的能力, 也就是说 "弹窗关闭" 需要放在外部, 即:
// 声明
const TestModal = NiceModal.create(() => {
const modal = useModal();
useInjectHistory(modal, () => {
modal.hide();
});
return <Dialog>test modal</Dialog>;
});
// 调用
NiceModal.show(TestModal);
实现思路
export function useInjectHistory(
modal: NiceModalHandler<Record<string, unknown>>,
/**
* 如果需要在用户物理返回时关闭弹窗, 就在该方法中手动调用 modal.hide();
* 如果拒绝关闭弹窗, 就别 hide() 并 throw Error;
*/
onPopState: (e: PopStateEvent) => Promise<void>
) {
// mui 的 useEventCallback
const finalOnPopState = useEventCallback((e: PopStateEvent) => {
isTriggeredByPopStateRef.current = true;
// 当 popstate 发生时, 如果 onPopState 抛错 (即调用方拒绝关闭弹窗),
// 那么此处重新 history.pushState, 以恢复 history 栈
onPopState(e).catch(() => {
window.history.pushState(null, "", "#dialog");
});
});
// react-use 的 useEvent (也可自行使用其他方法, 反正就是监听 popstate, 然后执行 onPopState)
useEvent("popstate", finalOnPopState);
// useListen 见下文所述
useListen(modal.visible, () => {
if (modal.visible) {
// 弹窗打开时, push history
window.history.pushState(null, "", "#dialog");
} else if (!isTriggeredByPopStateRef.current)
// 其他地方 (非 popstate 事件) 触发弹窗关闭时, 恢复 history 栈
window.history.back();
});
}
useListen 见本站文章 一个监听变量变化的语法糖
2023-08-10 更新:
上面的实现有一个问题: 如果我页面上多个弹窗并存时,
popstate
事件会在所有弹窗中触发, 导致一个popstate
关闭了所有弹窗, 如何解决这个问题呢? 且听下回分解