前言
之前開發(fā)中遇到一個需求,是需要向后端先請求數(shù)據(jù),拿到一個 url 后,然后用該 url 打開新窗口,但按照該流程寫好代碼開始調(diào)試時,卻發(fā)現(xiàn)新窗口被瀏覽器攔截了,查資料發(fā)現(xiàn)這是瀏覽器的安全機(jī)制,用戶主動觸發(fā)的行為才能打開新窗口,下面先介紹一下打開新窗口的幾種方法。
在瀏覽器中打開新窗口的幾種方法
- window.open
// 以百度為例
const _window = window.open('https://www.baidu.com');
- 模擬<a>元素點(diǎn)擊事件
function openWindow(url) {
const a = document.createElement('a');
a.href = url;
a.target = '_blank';
a.click();
}
openWindow('https://www.baidu.com');
- 模擬表單提交
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ù)無法被正確提交。
- 利用 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
設(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)的頁面。
但該方案也存在問題:
- 如果請求失敗,需要關(guān)閉之前打開的空白窗口,影響用戶體驗(yàn)
- 如果請求 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)該使用第二種方法解決。