隆重推出 VisionViewport

Jake Archibald
Jake Archibald

如果我告訴你,有超過一個檢視區,你會怎麼做?

BRRRRAAAAAAAMMMMMMMMMM

而您目前使用的可視區域,其實是可視區域中的可視區域。

BRRRRAAAAAAAMMMMMMMMMM

有時 DOM 提供的資料會參照其中一個視區,而非另一個視區。

BRRRRAAAAM… 等等,什麼?

沒錯,請看這裡:

版面配置可視區域與視覺可視區域

上方的影片顯示捲動和捏合放大網頁的畫面,右側的迷你地圖則顯示網頁中視區的位置。

在一般捲動期間,一切都很順暢。綠色區域代表 position: fixed 項目會貼在的版面配置可視區

當系統引入捏合縮放功能時,情況就會變得怪異。紅色方塊代表可視區域,也就是我們實際可見的頁面部分。這個可視區域可以移動,而 position: fixed 元素會保持原位,並附加至版面配置可視區域。如果我們在版面配置可視區域的邊界進行平移,它會一併拖曳版面配置可視區域。

提升相容性

很遺憾,網路 API 所參照的視窗不一致,而且在不同瀏覽器之間也存在差異。

舉例來說,element.getBoundingClientRect().y 會傳回版面配置視區中的偏移量。這很棒,但我們通常會想要在網頁中指定位置,因此我們會寫下:

element.getBoundingClientRect().y + window.scrollY

不過,許多瀏覽器會為 window.scrollY 使用視覺可視區域,這表示當使用者進行雙指撥動時,上述程式碼會中斷。

Chrome 61 會改變 window.scrollY,改為參照版面配置可視區,也就是說,即使使用捏合縮放,上述程式碼仍可正常運作。事實上,瀏覽器會逐漸變更所有位置屬性,以便參照版面配置可視區。

除了一個新屬性以外…

將視覺可視區域公開給指令碼

新的 API 會將可視區域公開為 window.visualViewport。這是草稿規格,已獲得跨瀏覽器核准,並在 Chrome 61 中推出。

console.log(window.visualViewport.width);

window.visualViewport 提供的內容如下:

visualViewport 個房源
offsetLeft 視覺可視區域左邊緣與版面配置可視區域之間的距離,以 CSS 像素為單位。
offsetTop 視覺可視區域頂端邊緣與版面配置可視區域之間的距離,以 CSS 像素為單位。
pageLeft 視覺可視區域左邊緣與文件左邊界線之間的距離,以 CSS 像素為單位。
pageTop 視覺可視區域頂端邊緣與文件頂端邊界之間的距離,以 CSS 像素為單位。
width 視覺可視區域的寬度 (以 CSS 像素為單位)。
height 視覺可視區域的高度 (以 CSS 像素為單位)。
scale 雙指撥動所套用的縮放比例。如果內容因縮放而變成原來的兩倍大小,則會傳回 2。這不會受到 devicePixelRatio 影響。

還有幾個事件:

window.visualViewport.addEventListener('resize', listener);
visualViewport 個事件
resize widthheightscale 變更時觸發。
scroll offsetLeftoffsetTop 變更時觸發。

示範

本文開頭的影片是使用 visualViewport 製作,請在 Chrome 61 以上版本中查看。這部影片使用 visualViewport 將迷你地圖固定在視覺檢視區的右上方,並套用反向比例,因此即使使用捏合縮放功能,迷你地圖的大小一律保持不變。

注意事項

只有在視覺檢視區域變更時才會觸發事件

這似乎是顯而易見的狀態,但我第一次使用 visualViewport 時,這一點讓我大吃一驚。

如果版面配置視區的大小會調整,但視覺視區不會調整,您就不會收到 resize 事件。不過,如果版面配置可視區域的大小未調整,視覺可視區域的寬度/高度也未變更,就會出現不尋常的情況。

真正的問題是捲動畫面。如果發生捲動動作,但可視區域視窗相對於版面配置區域視窗仍保持靜止,您就不會在 visualViewport 上收到 scroll 事件,而這類情況非常常見。在一般文件捲動期間,視覺可視區域會鎖定在版面配置可視區域的左上方,因此 scroll 不會在 visualViewport 上觸發

如果您想瞭解視覺檢視區的所有變更,包括 pageToppageLeft,就必須監聽視窗的捲動事件:

visualViewport.addEventListener('scroll', update);
visualViewport.addEventListener('resize', update);
window.addEventListener('scroll', update);

避免使用多個事件監聽器重複工作

就像在視窗上監聽 scrollresize 一樣,您可能會呼叫某種「更新」函式。不過,這些事件通常會同時發生。如果使用者調整視窗大小,系統會觸發 resize,但通常也會觸發 scroll。為提升效能,請避免多次處理變更:

// Add listeners
visualViewport.addEventListener('scroll', update);
visualViewport.addEventListener('resize', update);
addEventListener('scroll', update);

let pendingUpdate = false;

function update() {
    // If we're already going to handle an update, return
    if (pendingUpdate) return;

    pendingUpdate = true;

    // Use requestAnimationFrame so the update happens before next render
    requestAnimationFrame(() => {
    pendingUpdate = false;

    // Handle update here
    });
}

我已為此提出規格問題,因為我認為可能有更好的方法,例如單一 update 事件。

事件處理常式無法運作

由於 Chrome 錯誤,這項功能無法運作

錯誤做法

有問題 - 使用事件處理常式

visualViewport.onscroll = () => console.log('scroll!');

請改採以下做法:

正確做法

可行 - 使用事件監聽器

visualViewport.addEventListener('scroll', () => console.log('scroll'));

偏移值會四捨五入

我認為 (希望) 這是另一個 Chrome 錯誤

offsetLeftoffsetTop 會經過四捨五入,因此在使用者放大時,這兩個值的準確度會降低。您可以在示範期間看到這項問題。如果使用者放大並緩慢平移,迷你地圖會在未縮放的像素之間對齊

事件速率緩慢

與其他 resizescroll 事件一樣,這些事件不會在每個影格觸發,特別是在行動裝置上。您可以在示範期間看到這個問題:一旦您使用雙指撥動縮放功能,迷你地圖就無法繼續鎖定在可視區域。

無障礙設定

示範中,我使用 visualViewport 來對抗使用者的雙指撥動縮放。這對這個特定示範來說是合理的做法,但您應先三思,再做任何會覆寫使用者想要放大畫面的要求的動作。

visualViewport 可用來改善無障礙功能。舉例來說,如果使用者正在放大畫面,您可以選擇隱藏裝飾性 position: fixed 項目,讓使用者不必看到這些項目。不過,請注意,請勿隱藏使用者想仔細查看的內容。

您可以考慮在使用者放大時發布至數據分析服務。這有助於您找出使用者在預設縮放等級下,難以瀏覽的網頁。

visualViewport.addEventListener('resize', () => {
    if (visualViewport.scale > 1) {
    // Post data to analytics service
    }
});

大功告成!visualViewport 是一個不錯的小型 API,可解決相容性問題。