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,现在的实现多少都有点黑科技的味道。