我给弹窗添加了支持物理返回键 一

背景介绍

我们知道, 对于 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 页面了

期望的调用方式

暂且将我们的方法命名为 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 关闭了所有弹窗, 如何解决这个问题呢? 且听下回分解

如非特别声明,本站作品均为原创,遵循【自由转载-保持署名-非商用-非衍生 创意共享 3.0 许可证】。

对于转载作品,如需二次转载,请遵循原作许可。