在瀏覽器中異步打開新窗口

前言

之前開發(fā)中遇到一個需求,是需要向后端先請求數(shù)據(jù),拿到一個 url 后,然后用該 url 打開新窗口,但按照該流程寫好代碼開始調(diào)試時,卻發(fā)現(xiàn)新窗口被瀏覽器攔截了,查資料發(fā)現(xiàn)這是瀏覽器的安全機(jī)制,用戶主動觸發(fā)的行為才能打開新窗口,下面先介紹一下打開新窗口的幾種方法。

在瀏覽器中打開新窗口的幾種方法

  1. window.open
// 以百度為例
const _window = window.open('https://www.baidu.com');
  1. 模擬<a>元素點(diǎn)擊事件
function openWindow(url) {
  const a = document.createElement('a');
  a.href = url;
  a.target = '_blank';
  a.click();
}

openWindow('https://www.baidu.com');
  1. 模擬表單提交
function openWindow2(url) {
  let form = document.createElement('form');
  form.action = url;
  form.target = '_blank';
  form.method = 'GET';
  document.body.appendChild(form);
  form.submit();
  // clear
  document.body.removeChild(form);
  form = null;
}

openWindow2('https://www.baidu.com');

但這種方法 url 的 query 參數(shù)無法被正確提交。

  1. 利用 Event 對象
function openWindow3(url) {
  let event;
  // IE11及以下瀏覽器不支持Event constructor
  if (typeof Event === 'function') {
    event = new Event('click');
  } else {
    event = document.createEvent('Event');
    event.initEvent('click', false, false);
  }
  const el = document.createElement('button');
  el.addEventListener('click', () => {
    window.open(url);
  }, false);
  el.dispatchEvent(event);
}

openWindow3('https://www.baidu.com');

該方法本質(zhì)上也是 window.open 方法

異步回調(diào)中打開新窗口

用 vue 做個簡單的例子

<div id="app">
  <button @click="open">open</button>
  <button @click="open1">open1</button>
  <button @click="open2">open2</button>
  <button @click="open3">open3</button>
</div>
const app = new Vue({
  el: '#app',
  data() {
    return {
      url: 'https://www.baidu.com',
    };
  },
  methods: {
    open() {
      console.log('open a new window');
      window.open(this.url);
    },
    open1() {
      openWindow(this.url);
    },
    open2() {
      openWindow2(this.url);
    },
    open3() {
      openWindow3(this.url);
    },
  },
});

在同步過程中,這4種方法都能夠正確的打開新窗口,現(xiàn)在我們將同步改為異步。

const mockFetch = (url) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(url);
    }, 1500);
  });
};

···
async open() {
  const url = await mockFetch(this.url);
  window.open(url);
},
async open() {
  const url = await mockFetch(this.url);
  openWindow(url);
},
async open2() {
  const url = await mockFetch(this.url);
  openWindow2(url);
},
async open3() {
  const url = await mockFetch(this.url);
  openWindow3(url);
},
···

可以看到,異步過程中,使用這4種方法去打開新窗口都會被瀏覽器攔截,因?yàn)榛卣{(diào)上下文中的函數(shù)已經(jīng)不是用戶主動的行為了,這樣迫使我們尋求新的解決思路,而不是糾結(jié)于怎樣打開新窗口。

解決方案

上面提到,由于瀏覽器的安全機(jī)制,會將異步過程中打開新窗口的行為攔截,因此,該問題有兩種解決思路。

將請求改為同步請求

我們可以將請求改為同步,這樣瀏覽器就會認(rèn)為打開新窗口的行為是由用戶主動觸發(fā)的,并直接打開新窗口。

$.ajax 為例,可以在 options 中添加 async: false 選項(xiàng),這樣就將請求改為同步,從而保證不被瀏覽器攔截。

$.ajax({
  type: 'GET',
  url: 'xxx',
  async: false,
}).then(() => {});

但其本質(zhì)依然是將 XMLHttpRequest.open() 方法中的第三個參數(shù) async 設(shè)置為 false

XMLHttpRequest.open Syntax

設(shè)置 async 為 false,則請求變?yōu)橥健?/p>

const XHR =  new XMLHttpRequest();
...
XHR.open('GET', 'xxx', false);
...

注意:這種解決方案會阻塞 js 主線程,block 用戶與頁面的交互,所以這種方案雖然能解決問題,但不推薦使用。

提前打開新窗口,待請求 resolve 后,再將新窗口重定向

window.open 為例

...
async open() {
  const newWindow = window.open();
  const url = await mockFetch(this.url);
  newWindow.location.href = url;
},
...

這樣就會先打開一個空白頁面,等到請求成功返回以后,新窗口就會跳轉(zhuǎn)至對應(yīng)的頁面。

但該方案也存在問題:

  1. 如果請求失敗,需要關(guān)閉之前打開的空白窗口,影響用戶體驗(yàn)
  2. 如果請求 pending 時間過長,則新窗口等待時間過長,用戶體驗(yàn)也比較差,所以可以專門做一個 loading 的落地頁,然后后端進(jìn)行重定向。

額外補(bǔ)充

之前的代碼,是將請求 pending 時間模擬為 1500 ms,而當(dāng)該時間小于 1000 ms 時,發(fā)現(xiàn)異步回調(diào)中打開新窗口的行為,沒有被瀏覽器攔截,而是直接打開了新窗口。這是因?yàn)闉g覽器認(rèn)為 pending 時間過短的異步回調(diào)中執(zhí)行的函數(shù)在一定程度上也是安全的,故可以直接打開新窗口。

注:上述情況在 Chrome 瀏覽器中測試,還未在其他瀏覽器中測試。

總結(jié)

由于瀏覽器的安全機(jī)制,會將異步過程中打開新窗口的行為攔截,故我們可以將請求改為同步,或在請求之前打開新窗口,并在請求成功后重定向新窗口這兩種方法解決這一問題,而在實(shí)際應(yīng)用時,應(yīng)該使用第二種方法解決。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容