异步接口回调中打开新页面被浏览器拦截解决方案
最近在开发图床的文件下载功能时,遇到这样的一个问题。点击下载文件按钮 - 异步请求获取文件下载链接 - 回调中window.open(url)
打开新页面,结果并没有按预期打开新窗口,而是被浏览器给拦截住了
代码
axios.get(`/auth/file/download?guid=${guid}`).then(url => {
window.open(url, '_blank');
}).catch(e => {
console.log(e);
});
被浏览器拦截
对于开发者,知道放开拦截就很简单,但是对于普通用户就是个体验问题了,要解决这个问题,首先要知道为啥会出现这个问题
The general rule is that popup blockers will engage if window.open or similar is invoked from javascript that is not invoked by direct user action.
注:如果在js中调用window.open
或类似的弹框程序不是由用户操作直接触发的,那么浏览器将会启用弹出窗口拦截器
可是点击操作完全是由用户触发的,怎么就会被拦截呢?因为请求是异步的,当请求发出去之后用户的操作基本就结束了。请求返回回调中的操作会被浏览器当作不是由用户操作直接触发的,然后拦截掉,以下是问题的解决方法
方法一把请求改成同步
//
let xhr = new XMLHttpRequest();
xhr.open('GET', `/auth/file/download?guid=${guid}`, false);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
window.open(xhr.responseText, '_blank');
}
};
xhr.send();
基本上这个方法可以应付大部分的情况了,但是也会由列外,比如:
- axios不支持同步请求的配置,又不想写原生XMLHttpRequest
- 因为交互需要,只能是异步请求
方法二 预先打开空白页,请求返回后再刷新页面
目前针对必须是异步请求的情况下,比较常见的解决方案是请求发起前先open一个新窗口,当url返回后再刷新:
asnc downloadFile(guid) {
e.preventDefault();
var newWindow = window.open('about:blank', '_blank');
const url = await ajax.get(`/auth/file/download?guid=${guid}`);
if (!url) {
newWindow.close();
} else {
wi.location.href = url;
}
};
当然如果请求较快而且能返回url的情况下基本上是没什么问题的,但是当请求错误关闭新开的window时就会出现闪动的情况,体验很不好。
方法三 先开定时器调用window.open
, 请求回来后再赋值
在扒拉解决方法的时候发现了这段评论
Through experiments I've got to understand that stack depth has nothing to do with popup blocker.
It actually checks whether window.open is called within 1 second after user action or not
. Tested in Chrome 46 and Firefox 42.
大意是 用户操作之后是否操作之后,浏览器会检查是否在1s内js调用window.open, 然后呢???。然后在1s内调用的话就可以打开,否则拦截?
具体官方文档找不到,只能通过实践来试试,实践证明在chrome
, firfox
, safari
中效果一致
// 可以打开
downloadFile(guid) {
setTimeout(() => {
window.open('https://baidu.com', '_blank');
}, 1000);
}
// 打不开,被拦截
downloadFile(guid) {
setTimeout(() => {
window.open('https://baidu.com', '_blank');
}, 1001);
}
知道这个有什么用呢,还是不能在异步请求回调中调用window.open
打开页面。我们换种思路试试,不一定需要在异步请求回调中调用window.open
,但是可以延迟1s调用,意味着我们可以在外部开定时器1s后调用window.open
,然后在1s之内拿到需要open的url
downloadFile(guid) {
let url = '';
ajax.get(`/auth/file/download?guid=${guid}`).then(data => {
url = data;
}).catch(e => {
console.log(e);
});
setTimeout(() => {
window.open('https://baidu.com', '_blank');
}, 1000);
}
实践证明,该方法可行。但是有点瑕疵,一般的请求都会在100ms以内,如果直接设置1s,那么太影响用户体验了,为了提升用户体验,可以稍微麻烦点这样做:
downloadFile(guid) {
let url = '';
let timer1 = null;
let timer2 = null;
let timer3 = null;
timer1 = setTimeout(() => {
if (url) {
clearTimeout(timer2);
clearTimeout(timer3);
console.log('timer1');
window.open(url, '_blank');
}
}, 100);
timer2 = setTimeout(() => {
if (url) {
clearTimeout(timer3);
console.log('timer2');
window.open(url, '_blank');
}
}, 300);
timer3 = setTimeout(() => {
if (url) {
console.log('timer3');
window.open(url, '_blank');
}
}, 1000);
ajax.get(`/auth/file/download?guid=${guid}`).then(data => {
url = data;
}).catch(e => {
clearTimeout(timer);
});
}
总结
该问题解决方案有3种,具体采用哪种看具体需求
- 异步请求改为同步请求
- 先
window.open
打开一个新窗口,拿到新窗口的句柄,等请求返回拿到url后在替换新窗口的链接 - 结合setTimeout方法,先开启1s内定时器调用
window.open
, 然后调用异步请求在1s内拿到url,赋值给对应变量
小手抖一抖,给我点个赞
0条评论