ReactRouter中的Prompt实现

576 阅读2分钟

Prompt组件可以实现页面关闭的拦截,页面关闭的拦截比较好实现,直接监听beforeUnload事件即可。比较关键的一个是拦截后退按钮。

因为使用了React Router中的browserRouter,所以我们需要监听history对象中,state的变化。我们可以监听到popstate事件,在其中加入一些事件处理。但如何拦截这种变化呢,好像并不存在类似于eve.preventDefault这种事件。

之前看到过一种做法大概是每次提前push一个空的state,当我们检测到后退按钮触发时,首先这个空state被弹出,然后我们根据用户的选择,如果是确定后退,则再次手动pop一个state出来,这就实现了后退。如果用户选择了取消,则我们依然再push一个空state,保证下一次后退时依然可以实现拦截。

但这种做法多少感觉有点hack,我们看下Prompt组件是如何解决这个问题的。

Prompt组件实现回退拦截,最核心的功能来自history包中提供的block方法,我们看下block方法是如何实现的:

// https://github.com/remix-run/history/blob/6104a6a2e4/modules/createTransitionManager.js
// 外层同样是监听了popstate事件  
  function handlePop(location) {
    if (forceNextPop) {
      forceNextPop = false;
      setState();
    } else {
      const action = 'POP';

      transitionManager.confirmTransitionTo(
        location,
        action,
        getUserConfirmation,
        ok => {
          if (ok) {
            setState({ action, location });
          } else {
            // 关键的来了,如果prompt没有返回true,则将state revert
            revertPop(location);
          }
        }
      );
    }
  }

function revertPop(fromLocation) {
    const toLocation = history.location;

    // TODO: We could probably make this more reliable by
    // keeping a list of keys we've seen in sessionStorage.
    // Instead, we just default to 0 for keys we don't know.

    let toIndex = allKeys.indexOf(toLocation.key);

    if (toIndex === -1) toIndex = 0;

    let fromIndex = allKeys.indexOf(fromLocation.key);

    if (fromIndex === -1) fromIndex = 0;

    const delta = toIndex - fromIndex;

    if (delta) {
      forceNextPop = true;
      go(delta);
    }
  }

function confirmTransitionTo(
    location,
    action,
    getUserConfirmation,
    callback
  ) {
    // TODO: If another transition starts while we're still confirming
    // the previous one, we may end up in a weird state. Figure out the
    // best way to handle this.
    if (prompt != null) {
      // 这里就执行了我们提供了prompt函数
      const result =
        typeof prompt === 'function' ? prompt(location, action) : prompt;

      if (typeof result === 'string') {
        if (typeof getUserConfirmation === 'function') {
          getUserConfirmation(result, callback);
        } else {
          warning(
            false,
            'A history needs a getUserConfirmation function in order to use a prompt message'
          );

          callback(true);
        }
      } else {
        // Return false from a transition hook to cancel the transition.
        callback(result !== false);
      }
    } else {
      callback(true);
    }
  }

当我们提供的一个prompt函数,返回一个false后,block方法就会帮我们revertPop,就是从上一个state跳转回来。所以当我们快速点击后退的时候,有时会看到url栏发生变化。这个方法感觉上其实也有点hack,最好还是浏览器内核能够提供一种拦截后退操作的api,现在的实现多少都有点黑科技的味道。