react 自定义弹框拦截

2,358 阅读2分钟

背景

用户进入平台后,在某个步骤用户流失比较大。因此打算增加一个后退弹窗确认,减少用户流失。

实现方案

整个项目是单页面形式,使用 react-router + historyJs 进行路由控制。查看 historyJs github 发现它提供了 history.block api。原理是监听 popstate 事件,详细可以查看 文档

history.block 可以传入一个参数,类型可以是字符串,或者是一个回调函数。返回值是一个函数,执行后可以解除 block。

// 传入字符串
const unblock = history.block('Are you sure you want to leave this page?')

// 传入回调函数
const unblock2 = history.block((location, action) => {
  // action 是跳转的动作,包括 PUSH、POP、REPLACE
  // 返回的字符串就是 prompt 内容
  // 也可以返回 boolean 值。false 拦截(后面我们会用到此特性),true 不拦截
  if (action === 'POP') return 'Are you sure you want to leave this page?'
})

unblock()

自定义 prompt

默认拦截确认框弹窗使用 window.confirm,我们也可以在初始化 history 配置 getUserConfirmation 进行自定义。

const history = createHistory({
  getUserConfirmation(message, callback) {
    // callback(true) 跳转
    // callback(false) 拦截
  }
});

history.js
// 默认
function getConfirmation(message, callback) {
  callback(window.confirm(message));
}

存在的问题

添加 history.block 代码后发现 ios safari 无法后退不好使了。

通过 debugger 源码,排查后发现,后退操作当我们调用 history.block 传入或者回调返回字符串时,每次都会去调用 getConfirmation

  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.
    try {
    if (prompt != null) {
      var result = typeof prompt === 'function' ? prompt(location, action) : prompt;

      if (typeof result === 'string') {
        if (typeof getUserConfirmation === 'function') {
          // safari 没有执行 getUserConfirmation
          getUserConfirmation(result, callback);
        } else {
          process.env.NODE_ENV !== "production" ? warning(false, 'A history needs a getUserConfirmation function in order to use a prompt message') : void 0;
          callback(true);
        }
      } else {
        // Return false from a transition hook to cancel the transition.
        callback(result !== false);
      }
    } else {
      callback(true);
    }
    } catch (err) {
      console.error(err)
    }
  }

但是在 ios safari 下根本没有调用 getUserConfirmation, 因此跳转失效了。在 github 社区上也有很多类似的 issue 但是具体原因没有找到。

最终方案

既然传入字符串,在 safari 上无法调用 getUserConfirmation。 那我们就在 history.block 传入或者回调返回 boolean 类型,再根据具体逻辑做相应的跳转。我们也可以把这个功能抽离成 Prompt 组件。部分代码如下

Prompt.jsx
// history.block 初始化
// 根据 props.when 进行拦截控制
componentDidMount() {
  this.unblock = history.block((nextLocation) => {
    if (this.props.when) {
      this.setState({
        isShowModal: true,
        nextLocation,
      })
    }
    return !this.props.when
  })
}

componentWillUnmount() {
  // 解除 block
  this.unblock()
}

onConfirm = () => {
  // 解除 block
  this.unblock()
  // 根据逻辑进行跳转处理
  // 用户要回退/进入的路径存保存在 nextLocation
  // go or goBack
}

// 自定义 UI
render() {
  const { isShowModal } = this.state

  if (!isShowModal) {
    return null
  }

  // 确定拦截,自定义 UI
  return (
    <div style={{ pisition: 'fixed', height: '100%', width: '100%', zIndex: 99 }}>
      <div onClick={this.onCancel}>取消</div>
      <div onClick={this.onConfirm}>确定</div>
    </div>
  )
}


index.jsx
// 调用 Prompt 组件
render() {
  return (
    ...
    <Prompt when={true} />
  )
}