背景
用户进入平台后,在某个步骤用户流失比较大。因此打算增加一个后退弹窗确认,减少用户流失。
实现方案
整个项目是单页面形式,使用 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} />
)
}