当window.open被ios安全机制拦截,我掏出3种方案,终于跳转成功!

4,456 阅读5分钟

一、前言

今天在开发H5的时候,遇到了一个bug,就是在ios环境,在某些情况下执行window.open不生效,所以正好趁此机会研究了一下window.open

二、window.open介绍

open方法的调用方式可以看出,open方法是定义在Window接口上,正因为如此它有三个参数:

window.open(url, target, windowFeatures)
  1. url:「可选参数」,表示你要加载的资源URL或路径,如果不传,则打开一个url地址为about:blank的空白页。

顺便介绍一下,about:blankchrome浏览器的一个命令,该指令会打开浏览器的一个内建空白页面,而不是从网络上下载,类似的命令还有about:downloadsabout:extensionsabout:history等等,具体chrom浏览器提供了哪些命令,你可以通过在浏览器地址栏输入about:about查看。

  1. target:「可选参数」,它可以给以下两种值。
    • 第一种是target关键字
      • _self:当前标签页加载;
      • _blank(默认值):新标签页打开;
      • _parent:作为当前浏览环境的父级浏览上下文打开,没有父级浏览上下文,效果与_self相同;
      • _top:作为最顶级的浏览上下文打开,没有顶级浏览上下文,效果与_self相同。
    • 第二种是一个字符串:表示加载资源的浏览上下文的名称,也就是标签页的名称,如果这个名称在现有的标签页中不存在,则会开启一个新的标签页,如果存在,会跳转到这个标签页。

这里顺便提一下,我在平时开发中曾经写出这样的代码:

const handleClick = () => {
    window.open(url, 'blank');
}

这个方法是绑定在一个点击按钮上面的,结果我的同事在点击多次这个按钮时,第一次会打开一个新窗口,而后续的点击都跳转到第一次点击打开的那个窗口,于是我去排查了一下,发现自己把_blank写成了blank,于是浏览器把它解析成了标签页的名字,而不是target关键词,我那时候感觉挺有意思,第一次详细去了解了这个target

  1. windowFeatures:「可选参数」,它是一个字符串,用来描述窗口的特性,其格式是"key1=value1, key2=value2",即将keyvalue=号连接拼接成字符串,多个key value逗号隔开,比如我们要打开一个宽为500,高为600的窗口可以这么写:
window.open(url, 'new-window', 'width=500,height=600');

它可以描述如下窗口特性:

  1. width:内容区域宽度,最小值为100,
  2. height:内容区域高度,最小值100,
  3. left:距离用户操作系统工作区左侧的距离,
  4. top:距离用户操作系统工作区顶部的距离。
  5. ...

这四个应该是最常用的,其它的不太常用我这里就不列举了。

至于这个windowFeatures的作用呢,平常开发中用的不太多,我能想到的场景就是比如你要通过url打开一个预览页面,让用户看里面的一些内容,就可以用这个试试。

三、bug复现

先写一个能复现问题的demo:

async function jump() {
  await fetch('/xxx');
  window.open('https://www.xxx.cn');
}

正常情况下执行window.open是能正常新标签页打开传入的url的,但是一旦前面用await做了异步操作后,再执行window.open,就不生效了。

然后我又尝试了a标签,发现效果也是一样的,无法打开新标签页。

async function jump() {
  await fetch('/xxx');
  let a = document.createElement('a');
  a.setAttribute('target', 'blank');
  a.href = 'https://www.xxx.cn';
  a.click()
  a = null;
}

四、原因分析

  1. 安全机制拦截:IOS的Safari浏览器为了防止恶意网站通过window.open/a标签打开其他网站,于是对它们的调用有所限制,如果不是由用户直接交互触发的,而是由程序自动触发的,Safari会拦截这个操作。
  2. 异步操作:在AJAX回调中执行window.open/a标签跳转,被浏览器认为是非用户交互行为,所以被拦截。

五、解决方案

方案1:改用location.href

既然window.opena标签跳转不行,那就换成location.href就好了,因为safari不会拦截location.href

async function jump() {
  await fetch('/xxx');
  location.href = 'https://www.xxx.cn';
}

当然并不是所有场景下都适合用location.href,因为location.href会刷新页面,所以需要根据具体场景来选择。

方案2:先打开一个空标签页

通过window.open("", "_blank")先打开一个空标签页,然后等待请求完成后,修改这个新标签页的url。

async function jump() {
  const newWin = window.open("", "_blank"); // 提前打开一个窗口
  const { jumpUrl } = await fetch('/xxx');
  if (jumpUrl) {
    newWin.location = jumpUrl;
  } else {
    newWin.close();
    // ... 
  }
}

但这里有个体验问题,我这里根据有没有jumpUrl进行跳转,如果没有jumpUrl,我需要调用close方法关闭刚才提前打开的那个窗口,而这样用户就会体验到的流程就是,先出来一个新窗口,随后被秒关闭,这样用户体验很差。

方案3:setTimeout/requestAnimationFrame

在我的业务场景中,是必须要用window.open的,所以只能另寻他法,最终找到了一个解决方案,就是在window.open之前加一个setTimeout,在回调中执行window.open,这样就能避免被safari拦截。

async function jump() {
  await fetch('/xxx');
  setTimeout(() => {
    window.open('https://www.xxx.cn');
  }, 0)
}

后面测试了一下,发现requestAnimationFrame也可以。

async function jump() {
  await fetch('/xxx');
  requestAnimationFrame(() => {
    window.open('https://www.xxx.cn');
  })
}

六、最终我采取的方案

我最终是通过方案3setTimeout解决了问题,如果setTimeout不生效,可以尝试加点延时看看,比如100毫秒,我这边实测的ios机型都能生效,所以就没加延时。

七、小结

本文主要介绍了window.open的用法,以及我自己在平时开发中踩的坑,希望对大家平常开发有帮助!

如果针对上面的问题,更好的解决方案,欢迎在下方留言评论!一起学习。