这里说的是网页上的window.location
,用户获取和操作网页地址的一个强大的JS对象。
背景
我们知道,用户从一个网页跳转到另一个网页时,location
就会发生变化。同样的,如果我们对location
进行修改,或者调用对应的函数,我们就期望页面发生跳转。比如:
-
location
跳转window.location.assign('https://www.google.com');
-
location
替换window.location.replace('https://www.google.com');
那么问题来了,页面一定会跳转吗?
现实
最近遇到了一个问题,就是当用户提交了表单之后,所有代码都正常执行了,但是过了几秒,用户又提交了一次。结果因为已经提交过了,重复提交就会一直失败,用户就在那不停地点啊点啊,最后绝望而去。
整段代码已经简单到已经没有多少想象空间了:
try {
await submit();
window.location.replace('/success');
} catch (err) {
trackError(err);
}
就酱,用户第一次提交并没有报错,却没有跳转到成功页面,而导致了重复提交。也就是说,第一次提交成功了,location.replace
也成功执行了,但是却没有跳转。
为了确定我的猜想,我加了更多的上报:
track('start');
try {
await submit();
window.location.replace('/success');
track('success');
} catch (err) {
trackError(err);
}
track('finish');
结果,在收到报错的情况下,果然前一次提交的start
、success
、finish
都上报了。也就是说,所有代码都正常运行了,页面却没有跳转!
问题只能出在location
上了。所以这个原生方法关键时刻掉链子了吗?
实际上,这里有一个重要的问题我没有意识到:location
的赋值是同步的,但是页面的加载却是异步的。也就是说,代码已经执行完了,浏览器才在后台异步地加载新页面,一般来说,链接建立并开始传输数据,页面才会发生跳转。
经过试验,发现跳转过程中浏览器有如下特点:
- 如果连接失败,有的浏览器会迅速跳转到一个错误页面,有的浏览器会卡住很长时间,整个页面可操作。
- 如果尚未连接成功,浏览器会一直停留在老页面上,整个页面可操作。
- 如果连接成功,但是数据未完全加载,浏览器会跳转到新页面,并渲染已有数据(或者白屏)。
所以,如果浏览器表现为正在连接,而且整个页面是可操作的,那就坑大了。
通过一些其他不可描述的手段,我终于发现,用户确实是因为网络问题,加载/success
页面失败了。结果用户就在原来的页面上继续提交,一直失败。
那么,如何防止用户重复提交以及友好地提示失败呢?
防御
我们预期的操作应该是,提交成功后页面跳转,提交失败时允许用户重试。那么如何防止出现上面的问题呢?
- 可以显示加载中的提示,防止用户频繁操作,然后加一个timer,等待页面跳转。如果页面跳走了,timer也就失效了。
- 可以给
submit
方法添加一个标记,如果已经成功提交了,下次就直接尝试跳转。当然,这里其实更应该由后端做防御,前端也可以处理一下以避免产生误导用户的错误提示,比如前面的无止境的“提交失败”。
最终代码如下:
const delay = time => new Promise(resolve => setTimeout(resolve, time));
let submitResult;
async function memoizedSubmit() {
if (submitResult) return submitResult.result;
const result = await submit();
submitResult = { result };
return result;
}
showLoading();
try {
await memoizedSubmit();
window.location.replace('/success');
await delay(5000);
console.log('阿欧,5秒钟后仍然没有跳走,看来是失败了。');
// 这里可以做一些友好的提示
} catch (err) {
trackError(err);
}
hideLoading();
总结
其实这个问题归根结底是页面的异步加载加上网络的问题,而网络的问题是防不胜防的,我们永远无法预计到用户什么时候进个电梯、上个厕所、换个姿势,网络就断了,但是我们要保证的是网络恢复后,流程还可以走下去。
最后,location
也不是那么的靠谱。
网友评论