【Safari/IOS 兼容性】 從js visibilitychange Safari下無效說開去

一、Safari下問題說明

在 Safari 瀏覽器下,無論是桌面端 Safari,還是 iOS Safari,visibilitychange 事件不總是觸發的。

對於窗口最小化,Tab 隱藏等行爲 visibilitychange 事件是正常的,但是如果是點擊頁面某個鏈接發生的當前頁導航跳轉,則 visibilitychange 事件不會觸發。

所以,雖然 visibilitychange 看起來兼容性不錯,IE10+支持,但是實際使用的時候還是有一些問題的,上述問題在 caniuse 上也是有對應的描述的。

 

這就會給我們的業務開發帶來困擾,例如,有一個數據上報的需求,希望用戶不再訪問此頁面的時候,進行一次數據上報,則如果使用 visibilitychange 事件進行處理,Safari 瀏覽器下就會有數據異常的情況發生。

document.addEventListener('visibilitychange', function logData() {
  if (document.visibilityState === 'hidden') {
    navigator.sendBeacon('/log', { /* 要發送的數據 */ });
  }
});

那有沒有什麼辦法解決這個問題呢?

那就是使用 pagehide 事件。

二、和pageshow/pagehide的區別

1. 功能區別

雖然都是有顯示與隱藏的含義,但是 visibilitychange 指的是頁面的可見與不可見,pageshow/pagehide 指的是頁面的進入與離開。

我們可以通過下面一段測試代碼瞭解兩者功能上的區別:

<div id="result"></div>
log = function (content) {
    result.innerHTML += content + '<br>';
};

window.addEventListener('pageshow', function () {
    log('pageshow: 頁面顯示');
});
window.addEventListener('pagehide', function () {
    log('pagehide: 頁面隱藏');
});

document.addEventListener('visibilitychange', function () {
    if (document.hidden) {
        log('visibilitychange: 頁面隱藏');
    } else {
        log('visibilitychange: 頁面顯示');
    }
});

 

具體描述爲:

  • 頁面進入,包括刷新會觸發 pageshow;
  • 選項卡切換,只會觸發 visibilitychange 顯示與隱藏;
  • 前進和後退,所有瀏覽器都會依次觸發 pagehide,visibilitychange 和 pageshow;
  • 如果是點擊某個鏈接跳轉出去,則Safari瀏覽器會出現不一樣的表現。

大家若有興趣,可以訪問這裏感受下事件變化的觸發。其實上面的第 4 點大家可以在 Safari 瀏覽器下測試下,點擊頁面鏈接然後再返回,會發現 visibilitychange 事件並未執行。

 

2. 用法區別

'visibilitychange' 事件通常都是掛載在 document 對象上,雖然現在最新的瀏覽器也支持掛載在 window 對象上,不過由於 Safari 14 之前的版本不支持,因此,是不推薦使用下面的語法的:

window.addEventListener('visibilitychange', () => {});

而 pageshow 和 pagehide 事件都是通過 window 對象進行註冊的。

3. 兼容性區別

pageshow 和 pagehide 事件是 IE11 及其以上瀏覽器支持的,而 visibilitychange 事件是 IE10 及其以上版本支持的。

具體如下截圖示意:

 

雖然 pageshow 和 pagehide 的兼容性略遜一籌,但是人家穩定啊,以及放眼整個世界,使用 IE10 瀏覽器的用戶微乎其微,因爲 IE10 就是個過渡版本。

三、unload 和 beforeunload 事件呢?

除非是要兼容古老的 IE 瀏覽器,以及在桌面端瀏覽器環境下阻止用戶退出網頁(如,您寫的內容尚未保存,是否退出,如下代碼所示),否則,沒有任何理由使用 unload 和 beforeunload 事件,尤其是移動端的頁面。

window.addEventListener('beforeunload', function (event) {
  if (pageHasUnsavedChanges()) {
    event.preventDefault();
    return event.returnValue = '您寫的內容尚未保存,是否退出?';
  }
});

因爲用戶訪問完一個頁面,往往是直接切換到其他 APP,然後通過殺進程關掉整個瀏覽器 APP,unload 事件就不會觸發。

以及另外一個比較重要的原因,unload 和 beforeunload 會阻止瀏覽器把頁面存入緩存,影響瀏覽器前進和後退時候的響應速度。

 

四、痛快點,終極方案是什麼?

回到一開始,只是說了 pagehide 解決 Safari 的問題,可具體該如何解決呢?

很簡單,判斷是不是 Safari 瀏覽器,然後額外增加一個 pagehide 事件:

document.addEventListener('visibilitychange', function logData() {
    if (document.visibilityState === 'hidden') {
      navigator.sendBeacon('/log', postData );
    }
});
if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) {
    window.addEventListener('pagehide', function () {
        navigator.sendBeacon('/log', postData );
    });
}

但是,上面的實現其實是有風險的,因爲你並不知道哪一天 Safari 瀏覽器會改變自己的策略,也就是說不定 Safari 16 或者後面某一個版本 pagehide 也會觸發 visibilitychange 行爲,則上面的代碼又會有重複上報的問題。

所以,比較穩妥,且自己不需要動腦子的方法,就是拾人牙慧,使用他人已經做好的項目進行開發,例如谷歌實驗室開源的這個名爲 PageLifecycle.js 的項目:github.com/GoogleChromeLabs/page-lifecycle

使用如下:

<script src="./lifecycle.es5.js"></script>
<script>
lifecycle.addEventListener('statechange', function(event) {
    console.log('狀態變化:' + event.oldState + ' → ' + event.newState);
});
</script>

此時,當我們導航跳轉再返回,就會出現如下截圖所示的輸出效果:

 

您也可以訪問這裏親自感受下輸出結果。

Safari 下雖然細節上有差異,但是從 passive → hidden 這個狀態和 Chrome 瀏覽器是一致的,如下截圖所示:

 

所以,我們希望頁面離開時候上報數據,可以試試下面的代碼,理論上應該是沒問題的:

lifecycle.addEventListener('statechange', function(event) {
    if (event.oldState == 'passive' && event.newState == 'hidden') {
        navigator.sendBeacon('/log', postData);
    }
});

上述截圖除了 passive 和 hidden 這了兩個狀態,還出現了 active 和 frozen,這些狀態都表示什麼意思呢,是瀏覽器原本就有的,還是 PageLifecycle.js 自定義的呢?

都是瀏覽器都有的,寫入規範標準的狀態,都屬於頁面生命週期的一部分。

 

關於sendBeacon

https://www.cnblogs.com/sybboy/p/16469617.html

 

五、瞭解頁面的生命週期

完整的頁面生命週期狀態包括這些:

  • ACTIVE 激活
  • PASSIVE 未激活(頁面可以看到,但焦點不在此頁面,打開開發者工具可以觸發此狀態)
  • HIDDEN 隱藏,最小化、標籤頁切換都屬於隱藏
  • FROZEN 凍結
  • TERMINATED 結束 (頁面被關閉)
  • DISCARDED 廢棄(頁面內容被瀏覽器清空)

其中,從 HIDDEN 狀態到 FROZEN 狀態之間的變化是有新的 API 事件名稱檢測的,分別是 resume 事件和 freeze 事件,使用示意如下:

document.addEventListener('freeze', (event) => {
  // 頁面被凍結
});

document.addEventListener('resume', (event) => {
  // 頁面解凍了
});

Web 網頁完整的生命週期流程見下面的高清大圖(看不清可雙指放大,或點擊小圖查看),原圖是英文的,源自 google 官方的這篇文章,自己重新翻譯了下,方便大家的學習。

 

DISCARDED 廢棄

其中,廢棄狀態是後來纔有的,原本是沒有的,目的是爲了釋放不必要的內存開銷。

如果經常使用 Chrome 瀏覽器,應該都有遇到過這樣的現象,就是一個很久沒有訪問的標籤頁再切換過去的時候,頁面會重新加載一遍。

之所以會加載,是因爲瀏覽器爲了節約內存,把這個長時間不使用的頁面給廢棄了,所有頁面的內存、緩存通通捨棄。

我個人是不太喜歡這樣的處理的,因爲有些頁面,特別是圖特別多的大型的文檔(如 figma 設計稿),每次切換過去,都要重新 loading 一次,很不爽的。

關於這個,可以所囉嗦兩句。

原本 IE 時代,Chrome 還沒出現的時候,瀏覽器的標籤頁,如果你開了多個,只要 1 個崩掉了,整個瀏覽器都會崩潰,其他的標籤頁數據就會丟失。

當然 Chrome 出來的時候,其中宣傳的一個優點就是每個標籤頁面獨立,A 頁面崩潰不會影響 B 頁面,但是,這種不崩潰策略是以犧牲內存爲代價的,因此,那個時候,經常有網絡圖戲謔 Chrome 是個內存怪獸

 

而現在的這種凍結+廢棄的策略,雖然省了內存,但是犧牲了用戶體驗,正所謂魚和熊掌不可兼得,所以終極解決方法還是加大內存,16G內存走起。

在 Chrome 68 之後,我們可以使用 document.wasDiscarded 判斷頁面是不是處於廢止狀態。

以及,也可以在 Chrome 瀏覽器地址欄中輸入 chrome://discards 查看各個頁面的狀態。

例如,我現在看了下(省略中間十幾個大

可以看到,除了幾個新打開不久的頁面,其他頁面都已經 DISCARDED 掉了,慘!

不說了,我要去找運維申請加內存條了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章