為了防止惡意指令碼濫用彈出式視窗、全螢幕等敏感 API,瀏覽器會透過使用者啟用方式控管對這些 API 的存取權。使用者啟用是指瀏覽工作階段與使用者動作相關的狀態:如果狀態為「啟用」,通常表示使用者目前正在與網頁互動,或是自網頁載入後已完成互動。使用者手勢是常見的詞彙,但容易造成誤解。舉例來說,使用者滑動或揮動手勢不會啟動網頁,因此從指令碼角度來看,這不是使用者啟動動作。
目前主要瀏覽器在使用者啟用功能控制啟用式 API的方式上,顯示出截然不同的行為。在 Chrome 中,實作方式是以符記為基礎的模型為依據,但這項做法過於複雜,無法在所有啟用閘道 API 中定義一致的行為。舉例來說,Chrome 一直允許透過 postMessage()
和 setTimeout()
呼叫,存取啟用閘道 API 的部分功能;此外,Promises、XHR、Gamepad 互動等功能不支援使用者啟用。請注意,其中有些是常見且長期存在的錯誤。
在 72 版中,Chrome 會提供使用者啟用功能第 2 版,讓所有需要啟用才能使用的 API 都能完成使用者啟用程序。這可解決上述不一致的問題 (以及其他幾個問題,例如 MessageChannels),我們認為這可簡化使用者啟用功能的網頁開發作業。此外,新實作方式還提供參考實作方式,以因應新規格的提案,該規格旨在讓所有瀏覽器在長期內整合在一起。
使用者啟用功能 v2 的運作方式為何?
新的 API 會在影格階層中的每個 window
物件上維持兩位元的使用者啟用狀態:一個是歷史使用者啟用狀態的固定位元 (如果影格曾經歷過使用者啟用),另一個是目前狀態的暫時位元 (如果影格在約一秒內曾經歷過使用者啟用)。設定後,黏性位元在影格生命週期中不會重設。每次使用者互動都會設定暫時位元,並在一段到期間隔 (約一秒) 後或透過呼叫啟用消耗型 API (例如 window.open()
) 重設。
請注意,不同啟用限制 API 會以不同方式依賴使用者啟用;新 API 不會變更任何這些 API 專屬行為。舉例來說,每個使用者啟動只能允許一個彈出式視窗,因為 window.open()
會像以往一樣消耗使用者啟動,Navigator.prototype.vibrate()
會在畫面 (或任何子畫面) 曾經顯示使用者動作時繼續生效,以此類推。
異動內容
- 使用者啟用功能第 2 版將使用者啟用功能在影格邊界內的可見度概念正式化:使用者與特定影格互動時,現在會啟用所有包含影格 (且僅限這些影格),不論其來源為何。(在 Chrome 72 中,我們已提供暫時性解決方法,可將可見度擴大至所有同源框架。一旦我們找到明確將使用者啟用傳遞至子畫面的方法,就會移除這個解決方法。)
- 如果啟用閘道 API 是從已啟用的影格呼叫,但從事件處理常式程式碼外部呼叫,只要使用者啟用狀態為「已啟用」(例如未過期或未使用),就會運作。在使用者啟用功能 v2 推出前,這項操作會無條件失敗。
- 在到期時間間隔內,多個未使用的使用者互動會融合為單一啟用事件,對應至最後一次互動。
啟用閘道 API 中的一致性示例
以下是兩個使用彈出式視窗 (使用 window.open()
開啟) 的範例,說明 User Activation v2 如何讓啟用限制的 API 行為保持一致。
連接的 setTimeout()
呼叫
這個範例來自我們的 setTimeout()
示範。如果 click
處理常式嘗試在 1 秒內開啟彈出式視窗,無論程式碼如何「組合」延遲時間,都應會成功。使用者啟用功能 v2 符合這項預期,因此下列每個事件處理常式都會在 click
上開啟彈出式視窗 (延遲 100 毫秒):
function popupAfter100ms() {
setTimeout(callWindowOpen, 100);
}
function asyncPopupAfter100ms() {
setTimeout(popupAfter100ms, 0);
}
someButton.addEventListener('click', popupAfter100ms);
someButton.addEventListener('click', asyncPopupAfter100ms);
在沒有使用者啟用功能 v2 的情況下,第二個事件處理常式在我們測試的所有瀏覽器中都會失敗。(在某些情況下,第一個嘗試也會失敗)。
跨網域 postMessage()
呼叫
以下是 postMessage()
示範的範例。假設跨來源子頁框中的 click
處理常式直接將兩則訊息傳送至父頁框。父項影格應可在收到下列任一訊息時開啟彈出式視窗 (但不能同時收到這兩種訊息):
// Parent frame code
window.addEventListener('message', e => {
if (e.data === 'open_popup' && e.origin === child_origin)
window.open('about:blank');
});
// Child frame code:
someButton.addEventListener('click', () => {
parent.postMessage('hi_there', parent_origin);
parent.postMessage('open_popup', parent_origin);
});
如果沒有使用者啟用功能 v2,父項框架在收到第二個訊息時,就無法開啟彈出式視窗。如果第一則訊息「連結」至另一個跨來源框架 (也就是第一個接收器將訊息轉寄給其他接收器),則第一則訊息也會失敗。
這項功能適用於 User Activation v2,無論是原始表單還是鏈結都適用。