最近在开发图床的文件下载功能时,遇到这样的一个问题。点击下载文件按钮 - 异步请求获取文件下载链接 - 回调中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.

来源于stackoverflow

大意是 用户操作之后是否操作之后,浏览器会检查是否在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,赋值给对应变量