Web技巧(11)

前段時間在項目使用div元素模擬body時使用了will-change時觸發了一個奇特的問題,最終以刪除will-change而解決。那麼這一期中咱們先來聊聊will-changetransform和層有關的事情,而後再和你們一塊兒分享幾個在Web中的JavaScript相關的API,好比全屏API、MediaStream API、MediaRecorder API、scrollIntoView API 和 分享 API等。感興趣的同窗請繼續往下閱讀。javascript

Web中的渲染層和如何控制層

在一些瀏覽器的調試工具中提供一些調試工具,能夠查看當前面的三維視圖模式,在該模式中,頁面中的HTML嵌套結構,會以圖形化的方式,由外到內,從頁面底部一級一級凸顯出來。這種視圖可讓你很容易的看清楚頁面的嵌套結構。css

而在CSS中有關於渲染層的控制一樣要以藉助一些CSS的屬性來作相應的控制,好比transformz-index等。html

瀏覽器渲染一個Web頁面可能會經歷下面這樣的幾個過程:html5

  • 下載並解析HTML,生成一個DOM樹
  • 處理佈局文檔,生成一個佈局樹
  • 將佈局樹轉換爲渲染指令,生成一個繪製樹
  • 生成一個足夠容納整個文檔的畫布

簡單地瞭解一下有關於樣式計算,佈局和元素繪製等幾個方面。java

瀏覽器下載並解析HTML只會生成一個DOM樹,這個時候DOM還不足以獲知頁面的具體樣式,瀏覽器主進程還會基於CSS選擇器解析CSS獲取每個節點的最終的計算樣式值。即便不提供任何CSS,瀏覽器對每一個元素也會有一個默認的樣式:css3

若是想要讓頁面有完整的樣式效果,除了獲取每一個節點的具體樣式,還須要獲知每個節點在頁面上的位置,佈局實際上是找到全部元素的幾何關係的進程。其具體過程大體以下:git

經過遍歷DOM及相關的元素的計算樣式,主線程會構建出包含每一個元素的座標信息及盒子大小的佈局樹。佈局樹和DOM樹相似,可是其中只包含頁面可見的元素,若是一個元素設置了display: none,這個元素不會出如今佈局樹上,僞元素雖然在DOM樹上不可見,可是在佈局樹上是可見的。github

即便知道了不一樣元素的位置及樣式信息,咱們還須要知道不一樣元素的繪製前後順序才能正確繪製出整個頁面。在繪製階段,主線程會遍歷佈局樹以建立繪製記錄。繪製記錄能夠看作是記錄各元素繪製前後順序的筆記。web

頁面的渲染還會經歷一個層的合成。正如文章開頭所示,頁面分割了不一樣的層,並相互嵌套。而複合是將頁面分割爲不一樣的層,並單獨柵格化,隨後組合爲幀的技術。不一樣層的組合由compositor線程(合成器線程)完成。shell

主線程會遍歷佈局樹來建立層樹(Layer tree),添加了will-change屬性的元素,會被看作單獨的一層。

有關於瀏覽器渲染原理方面更多的內容能夠閱讀:

你可能會想給每個元素都添加will-change,不過組合過多的層也許會在每一幀都柵格化頁面中的某些小部分更慢。那麼怎麼合理的使用will-change或者說怎麼更合理的使用層呢?

@dassurma在他的博客中對這方面作過相應的闡述:

除非你要更改transform,不然不要使用will-change: transform。使用will-change: opacitybackface-visibility:hidden的反作用不會那麼使人困擾。

來看看文章中一些有意思的東西,也值得咱們去關注和掌握的東西。

在CSS中會常用animationtransition來實現一些動效效果。在使用這兩個屬性時,瀏覽器會自動將動畫元素放到一個層上。它將主畫布保留到下一幀,並將額外的工做保持在儘量低的位置。咱們能夠在瀏覽器的調試工具中的Rendering選項卡中啓用Layers bordersLayers選項卡能夠看到位於單獨圖層上的元素周圍的橙色邊框和當前頁面上全部層的實時交互視圖:

理想狀況下呢,都但願瀏覽器知道什麼是合適的,而後去作什麼。遺憾的是,事實並不是如此。例如,當你使用requestAnimationFrame()在逐幀的基礎上處理動畫元素。這是很難的,不可能讓瀏覽告訴元素將有一個新值轉換每一個幀。除非你本身將動畫元素放到它本身的層中,不然就會有性能問題,由於瀏覽器將從新繪製整個文檔的每一幀。

在過去,大都會設置transform: translateZ(0)來開啓GPU的渲染。由於它將使用GPU計算透視(perspective)失真(即便它最終沒有失真)。若是使用translateX()translateY(),則不須要透視圖失真,瀏覽器將使用指定的偏移量將元素繪製到主畫布中。

早期這樣使用,在Chrome和Safari瀏覽器會讓元素閃爍,因此被建議在樣式中設置backface-visibility: hidden,甚至直到今天還這麼的使用。

自2016看開始,瀏覽器對will-change屬性的支持度愈來愈高,而該屬性告訴瀏覽某個CSS屬性將會改變。若是你在元素上設置will-change: transform,會告訴瀏覽器transform屬性將在不久的未來發生更改。所以,瀏覽器能夠推測地應用優化來適應這些將來的更改。在轉換的狀況下,這意味着它將強制元素到它本身的層上。

在某些場景之下,使用will-change:transform會對性能有提升,好比:

能夠避免元素重繪。

雖然某些場景對性能有所提升,但也會有相關的反作用。

先來看backface-visibility:hidden帶來的反作用 —— 隱藏元素的背面。一般這面不是面向用戶的,可是當你在3D空間中旋轉你的元素時,它會發生。以下圖所示:

對於will-change: transform,前面提到過,該屬性會告訴瀏覽器未來會發生什麼。因爲這個語義,規範規定設置will-change:<something>必須具備與該<something>屬性的任何非初始值相同的反作用。

這彷佛頗有道理,但當你使用position: fixedposition: absolute時,就會出錯:

若是爲transform設置一個值,將會建立一個新的包含塊。任何具備position: fixedposition: absolute的子元素都會相對於這個新的包含塊。這個在一些容器中使用position: fixed的元素將不會再相對於視窗定位,會相對於容器(具備新包含塊)定位。

transform: translateZ(0)will-change: transform具備相同的反作用,可是也可 人會干擾使用transform的其餘樣式,由於這些屬性根據級聯相互覆蓋。

再看一下will-change: opacity,從行爲上看上去和前面示例中演示的效果同樣,但這並不意味着它沒有副做做。設置will-change:opacity會建立一個新的疊加上下文。意味着它能夠影響元素渲染的順序。若是有重疊的元素,它能夠更改哪一個元素位於頂部。即便出現這種狀況,z-index也能幫助你恢復你想要的層疊順序。

也就是說,使用will-change的時候千萬要注意,這也爲何在某些場景下使用will-change會帶來反作用。正如上面所述,除了will-change以外,transform: translateZ(0)backface-visibility:hiddenwill-change: opacity多少會帶來一些反作用。直到目前爲止,使用will-change: opacitybackface-visibility: hidden能夠將一個元素強制放到它本身的層上(由於它的反作用彷佛最不可能產生問題)。另外,只有當你真正要改變transform時,才應用使用will-change: transform

擴展閱讀:

Web Share API

Chrome 61開始爲Android系統引入了Web Share API以來彷佛並無引發太多的關注。但在移動端的分享功能卻又是不可或缺乏的一部分。Web Share API本質上它提供了一種方法,能夠Web頁面或Web應用程序中提供分享的能力。該API的引入容許開發人員利用用戶設備的本地內容共享功能嚮應用程序或網站添加共享功能。

咱們能夠先使用navigator.share來作判斷,檢測用戶的瀏覽器是否支持Web Share API:

if (navigator.share) {
    // Web Share API is supported
} else {
    // Fallback
}
複製代碼

支持Web Share API的瀏覽器能夠調用navigator.share()方法並傳遞如下這些字段:

  • url:分享的URL字符串
  • title:分享的標題,一般是document.title
  • text:分享的描述內容

來看一個簡單的示例:

shareButton.addEventListener('click', event => {
    if (navigator.share) {
        navigator.share({
            title: '來自W3cplus的分享',
            url: 'https://www.w3cplus.com'
        }).then(() => {
            console.log('Thanks for sharing!');
        })
        .catch(console.error);
    } else {
        // fallback
    }
});
複製代碼

對於不支持Web Share API的瀏覽器,咱們能夠作了個降級方案,好比在Web頁面彈出一個Modal框:

shareButton.addEventListener('click', event => {
    if (navigator.share) {
        navigator.share({
            title: 'Web技巧11',
            url: 'https://www.w3cplus.com'
        }).then(() => {
            console.log('Thanks for sharing!');
        })
        .catch(console.error);
    } else {
        shareDialog.classList.add('is-open');
    }
});
複製代碼

有關於Web Share API更多的內容能夠閱讀:

MediaRecorder API

在Web頁面或Web應用程序上,咱們能夠從用戶的相機、麥克風等來捕獲媒體流。咱們可使用這些媒體流在WebRTC上進行實時的視頻聊天。經過MediaRecorder API還能夠直接在Web瀏覽器中記錄和保存用戶的音頻或視頻。

要使用MediaRecorder API就先須要一個MediaStream。能夠從<video><audio>元素中獲取一個,也能夠經過調用getUserMedia來捕獲用戶的相機和麥克風。一旦你有一個流,就能夠用它初始化MediaRecorder,也就能夠開始錄製了。

在記錄期間,MediaRecorder對象將發出dataavailable事件,並將記錄的數據做爲事件的一部分。咱們將偵聽這些事件並整理數組中的數據塊。一旦記錄完成,咱們將把塊數組從新綁定到一個Blob對象中。咱們能夠經過調用MediaRecorder對象上的startstop來控制錄製的開始和結束。

getUserMedia

和Web Share API相似,能夠經過下面的方式來檢測瀏覽器是否支持MediaRecorder:

if ('MediaRecorder' in window) {
    // everything is good, let's go ahead
} else {
    renderError ('Sorry, your browser doesn't support the MediaRecorder API, so this demo will not work')
}
複製代碼

對於renderError方法,咱們將用錯誤消息替換某個指定的元素,好比<main>來顯示相應的錯誤信息。能夠在事件偵聽器以後添加此方法:

function renderError(message) {
    const main = document.querySelector('main');
    main.innerHTML = `<div class="error"><p>${message}</p></div>`;
}
複製代碼

若是瀏覽器支持MediaRecorder,那咱們就能夠訪問麥克風來記錄。爲此,咱們將使用getUserMedia這個API。另外,咱們不會要求直接訪問麥克風,由於這對任何用戶來講都是一個糟糕的體驗。咱們可讓用戶主動點擊用戶上面的某個按鈕來訪問麥克風,而後再向用戶發起詢問,肯定是否開啓麥克風來記錄。

if ('MediaRecorder' in window) {
    getMic.addEventListener('click', async () => {
        getMic.setAttribute('hidden', 'hidden');
        try {
            const stream = await navigator.mediaDevices.getUserMedia({
            audio: true,
            video: false
        });
        console.log(stream);
        } catch {
            renderError(
            'You denied access to the microphone so this demo will not work.'
        );
    }
  });
} else {
    renderError ('Sorry, your browser doesn't support the MediaRecorder API, so this demo will not work')
}
複製代碼

還能夠調用navigator.mediaDevices.getUserMedia返回一個promise,若是用戶容許訪問該媒體,則該promise將成功解析。由於咱們使用的是現代JavaScript,因此可使用async/wait使這個promise看起來是同步的。咱們聲明的click事件是一個異步函數,而後當調用getUserMedia時,咱們等待結果,而後繼續。

固然,用戶也有可能會拒絕訪問麥克風,咱們能夠在try/catch語句中封裝調用來處理這個問題。若是用戶拒絕訪問麥克風將致使catch塊中代碼執行,那麼會調用renderError函數。

記錄

通過上面的處理,咱們可使用麥克風了,能夠準備錄音了。但咱們還須要將錄下來的這些東西存放起來。

首先,咱們將使用的是MIME類型,即audio/webm,另外還須要建立一個變量,好比chunks(它是一個數組),用來存儲錄下來的內容。

MediaRecorder使用從用戶麥克風捕獲的媒體流和選對象初始化,將傳遞前面定義的MIME類型:

if ('MediaRecorder' in window) {
    getMic.addEventListener('click', async () => {
        getMic.setAttribute('hidden', 'hidden');
        try {
            const stream = await navigator.mediaDevices.getUserMedia({
            audio: true,
            video: false
        });
        const mimeType = 'audio/webm';
        let chunks = [];
        const recorder = new MediaRecorder(stream, { type: mimeType });
        } catch {
            renderError(
                'You denied access to the microphone so this demo will not work.'
            );
        }
    });
} else {
    renderError ('Sorry, your browser doesn't support the MediaRecorder API, so this demo will not work')
}
複製代碼

如今咱們已經建立了MediaRecorder,咱們須要爲它設置一些事件監聽器。記錄器出於許多不一樣的緣由發出事件。這些都和記錄器自己的交互有關,所以能夠在記錄器開始記錄、暫停、繼續和中止時偵聽事件。最重要的事件是dataavailable事件,它在記錄器積極記錄時按期發出。這些事件包含一個記錄塊,咱們將把它推到剛剛建立的chunks數組上。

if ('MediaRecorder' in window) {
    getMic.addEventListener('click', async () => {
        getMic.setAttribute('hidden', 'hidden');
        try {
            const stream = await navigator.mediaDevices.getUserMedia({
            audio: true,
            video: false
        });
        const mimeType = 'audio/webm';
        let chunks = [];
        const recorder = new MediaRecorder(stream, { type: mimeType });
        recorder.addEventListener('dataavailable', event => {
            if (typeof event.data === 'undefined') return;
            if (event.data.size === 0) return;
                chunks.push(event.data);
            });
        recorder.addEventListener('stop', () => {
            const recording = new Blob(chunks, {
                type: mimeType
            });
            renderRecording(recording, list);
            chunks = [];
        });
        } catch {
            renderError(
                'You denied access to the microphone so this demo will not work.'
            );
        }
    });
} else {
    renderError ('Sorry, your browser doesn't support the MediaRecorder API, so this demo will not work')
}
複製代碼

另外,爲了方便用戶能夠把錄下來的音保存起來,還能夠建立一個函數:

function renderRecording(blob, list) {
    const blobUrl = URL.createObjectURL(blob);
    const li = document.createElement('li');
    const audio = document.createElement('audio');
    const anchor = document.createElement('a');
    anchor.setAttribute('href', blobUrl);
    const now = new Date();
    anchor.setAttribute(
    'download',
    `recording-${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDay().toString().padStart(2, '0')}--${now.getHours().toString().padStart(2, '0')}-${now.getMinutes().toString().padStart(2, '0')}-${now.getSeconds().toString().padStart(2, '0')}.webm`
    );
    anchor.innerText = 'Download';
    audio.setAttribute('src', blobUrl);
    audio.setAttribute('controls', 'controls');
    li.appendChild(audio);
    li.appendChild(anchor);
    list.appendChild(li);
}
複製代碼

最終示例代碼以下:

<!-- HTML -->
<div class="controls">
    <button type="button" id="mic">Get Microphone</button>
    <button type="button" id="record" hidden>Record</button>
</div>
<ul id="recordings"></ul>

// JavaScript
window.addEventListener('DOMContentLoaded', () => {
    const getMic = document.getElementById('mic');
    const recordButton = document.getElementById('record');
    const list = document.getElementById('recordings');

    if ('MediaRecorder' in window) {
        getMic.addEventListener('click', async () => {
            getMic.setAttribute('hidden', 'hidden');
            try {
                const stream = await navigator.mediaDevices.getUserMedia({
                    audio: true,
                    video: false
                });

                const mimeType = 'audio/webm';
                let chunks = [];
                const recorder = new MediaRecorder(stream, { type: mimeType });

                recorder.addEventListener('dataavailable', event => {
                    if (typeof event.data === 'undefined') return;
                    if (event.data.size === 0) return;
                    chunks.push(event.data);
                });

                recorder.addEventListener('stop', () => {
                    const recording = new Blob(chunks, {
                        type: mimeType
                    });
                    renderRecording(recording, list);
                    chunks = [];
                });

                recordButton.removeAttribute('hidden');

                recordButton.addEventListener('click', () => {
                    if (recorder.state === 'inactive') {
                        recorder.start();
                        recordButton.innerText = 'Stop';
                    } else {
                        recorder.stop();
                        recordButton.innerText = 'Record';
                    }
                });
            } catch {
                renderError(
                    'You denied access to the microphone so this demo will not work.'
                );
            }
        });
    } else {
        renderError(
            "Sorry, your browser doesn't support the MediaRecorder API, so this demo will not work."
        );
    }
});

function renderError(message) {
    const main = document.querySelector('main');
    main.innerHTML = `<div class="error"><p>${message}</p></div>`;
}

function renderRecording(blob, list) {
    const blobUrl = URL.createObjectURL(blob);
    const li = document.createElement('li');
    const audio = document.createElement('audio');
    const anchor = document.createElement('a');
    anchor.setAttribute('href', blobUrl);
    const now = new Date();
    anchor.setAttribute(
      'download',
      `recording-${now.getFullYear()}-${(now.getMonth() + 1)
        .toString()
        .padStart(2, '0')}-${now
        .getDay()
        .toString()
        .padStart(2, '0')}--${now
        .getHours()
        .toString()
        .padStart(2, '0')}-${now
        .getMinutes()
        .toString()
        .padStart(2, '0')}-${now
        .getSeconds()
        .toString()
        .padStart(2, '0')}.webm`
    );
    anchor.innerText = 'Download';
    audio.setAttribute('src', blobUrl);
    audio.setAttribute('controls', 'controls');
    li.appendChild(audio);
    li.appendChild(anchor);
    list.appendChild(li);
}
複製代碼

示例代碼來自@Phil nash《An introduction to the MediaRecorder API》一文。

擴展閱讀:

MediaStream API

MediaStream API 也能夠用來幫助你建立來自用戶輸入設備的數據流,好比視頻(攝像機)或音頻(麥克風)。該API有兩個核心方法:.getUserMedia()navigator.mediaDevices

請注意,此對象僅在HTTPS-secured上下文中才可用。

async function getMedia() {
    const constraints = {
        // ...
    };
    let stream;

    try {
        stream = await navigator.mediaDevices.getUserMedia(constraints);
        // use the stream
    } catch (err) {
        // handle the error - user's rejection or no media available
    }
}

getMedia();
複製代碼

該方法接受所謂的constraints對象,並返回一個promise,該promise解析爲一個新的MediaStream實例。這樣的接口是當前流媒體的表示。它由零個或多個獨立的MediaStreamTrack組成,每一個MediaStreamTrack表示音頻或視頻流,而音頻軌道由左右通道組成(用於立體聲和其餘東西)。若是須要進一步控制,這些軌道還提供了一些特殊的方法和事件。

constraints對象是較爲重要的部分。它用於配置.getUserMedia()的請求和生成的流。此對象能夠具備布爾值或對象值的兩個屬性:audiovideo

const constraints = {
    video: true,
    audio: true
};
複製代碼

經過將它們都設置爲true,咱們請求訪問用戶的默認視頻和音頻的輸入設備,並應用默認設置。要知道,爲了讓.getUserMedia()能正常工做,必須至少設置這兩個屬性中的一個。

若是但願進一步配置媒體設備的設置,須要傳遞一個對象。這裏提供的屬性列表很是長,而且根據應用於視頻或音頻的曲目類型不一樣而有所不一樣。你能夠在這裏看到完整的列表,並使用.getSupportedConstraints()方法檢查可用的列表。

假設此次咱們想更具體一些,爲視頻軌道指定一些額外的配置。

async function getConstraints() {
    const supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
    const video = {};

    if (supportedConstraints.width) {
        video.width = 1920;
    }
    if (supportedConstraints.height) {
        video.height = 1080;
    }
    if (supportedConstraints.deviceId) {
        const devices = await navigator.mediaDevices.enumerateDevices();
        const device = devices.find(device => {
            return device.kind == "videoinput";
        });
        video.deviceId = device.deviceId;
    }

    return { video };
}
複製代碼

另外咱們可使用.enumerateDevices()方法檢查可用的輸入設備,同時設置視頻的分辨率。它返回一個promise,該promise解析爲一個MediaDeviceInfo對象數組。來看一個簡單的小示例:

<!-- HTML -->
<video autoplay></video>
<audio autoplay></audio>

// JavaScript
async function getConstraints() {
    const supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
    const video = {};

    if (supportedConstraints.width) {
        video.width = 1920;
    }
    if (supportedConstraints.height) {
        video.height = 1080;
    }
    if (supportedConstraints.deviceId) {
        const devices = await navigator.mediaDevices.enumerateDevices();
        const device = devices.find(device => {
            return device.kind == "videoinput";
        });
        video.deviceId = device.deviceId;
    }

    return { video, audio: true };
}

async function getMedia() {
    const constraints = await getConstraints();
    const video = document.querySelector("video");
    const audio = document.querySelector("audio");
    let stream = null;

    try {
        stream = await navigator.mediaDevices.getUserMedia(constraints);
        console.log(stream, video.srcObject)
        video.srcObject = stream;
        audio.srcObject = stream;
        // use the stream
    } catch (err) {
        // handle the error - user's rejection or no media available
    }
}
getMedia();
複製代碼

擴展閱讀:

Fullscreen API

有的時候須要將頁面全屏顯示。CSS中有一些僞元素,能夠實現該效果:

:-webkit-full-screen body,
:-moz-full-screen body,
:-ms-fullscreen body {
    /* properties */
    width: 100vw;
    height: 100vh;
}

:full-screen body {
    /*pre-spec */
    /* properties */
    width: 100vw;
    height: 100vh;
}

:fullscreen body {
    /* spec */
    /* properties */
    width: 100vw;
    height: 100vh;
}

/* deeper elements */

:-webkit-full-screen body {
    width: 100vw;
    height: 100vh;
}

/* styling the backdrop*/

::backdrop,
::-ms-backdrop {
    /* Custom styles */
}
複製代碼

而蘋果去年秋天在iPad Safari上推出了對全屏API的支持。這意味着開發人員如今能夠在iPad上爲用戶建立徹底沉浸式的Web應用程序。它可靠地消除了屏幕上的全部干擾,幫助用戶專一於手頭的任務。就像一個本地應用程序同樣,能夠全屏顯示。

下面的代碼就是使用原生JavaScript來實現全屏效果的。首先建立了一個名爲_toggleFullScreen的函數,讓你在全屏和url模式之間切換:

const _toggleFullScreen = function _toggleFullScreen() {
    if (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement) {
        if (document.cancelFullScreen) {
            document.cancelFullScreen();
        } else {
            if (document.mozCancelFullScreen) {
                document.mozCancelFullScreen();
            } else {
                if (document.webkitCancelFullScreen) {
                    document.webkitCancelFullScreen();
                }
            }
        }
    } else {
        const _element = document.documentElement;
        if (_element.requestFullscreen) {
            _element.requestFullscreen();
        } else {
            if (_element.mozRequestFullScreen) {
                _element.mozRequestFullScreen();
            } else {
                if (_element.webkitRequestFullscreen) {
                    _element.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
                }
            }
        }
    }
};
複製代碼

這個_toggleFullScreen函數處理全部瀏覽器的前綴和特性。如今咱們只須要確認用戶使用的設備、瀏覽器和iOS版本,這樣就能夠啓用全屏功能了。

  • 使用使用的是iPad
  • 使用的是Safari瀏覽器
  • iOS版本是12以及更高的版本

不過咱們能夠採用window.navigator.userAgent來作檢測:

const userAgent = window.navigator.userAgent;

const iPadSafari =
    !!userAgent.match(/iPad/i) &&  		// Detect iPad first.
    !!userAgent.match(/WebKit/i) && 	// Filter browsers with webkit engine only
    !userAgent.match(/CriOS/i) &&		// Eliminate Chrome & Brave
    !userAgent.match(/OPiOS/i) &&		// Rule out Opera
    !userAgent.match(/FxiOS/i) &&		// Rule out Firefox
    !userAgent.match(/FocusiOS/i);		// Eliminate Firefox Focus as well!

    const element = document.getElementById('fullScreenButton');

    function iOS() {
        if (userAgent.match(/ipad|iphone|ipod/i)) {
            const iOS = {};
            iOS.majorReleaseNumber = +userAgent.match(/OS (\d)?\d_\d(_\d)?/i)[0].split('_')[0].replace('OS ', '');
            return iOS;
        }
    }

    if (element !== null) {
        if (userAgent.match(/iPhone/i) || userAgent.match(/iPod/i)) {
            element.className += ' hidden';
        } else if (userAgent.match(/iPad/i) && iOS().majorReleaseNumber < 12) {
            element.className += ' hidden';
        } else if (userAgent.match(/iPad/i) && !iPadSafari) {
            element.className += ' hidden';
        } else {
            element.addEventListener('click', _toggleFullScreen, false);
        }
    }
複製代碼

示例代碼來自於@Marvin Danig的《How to go fullscreen on iPad Safari》一文。

來看個示例:

擴展閱讀:

scrollIntoView API

在《滾動的特性》和《改變用戶體驗的滾動新特性》中咱們都提到了CSS的overscroll-behavior能夠控制一個容器或頁面body容器滾動時發生的默認行爲。可使用這個屬性取消滾動連接、禁用、自定義下拉刷新,禁用在iOS上的回彈效果等。並且使用overscroll-behavior不會對頁面有性能影響。

在JavaScript中提供了一個scrollIntoView API,能夠指示瀏覽器將一個元素添加到視窗端口。經過在scrollIntoViewOption對象上添加行爲屬性,能夠指示scrollIntoView API使用滾動部分具備動畫效果。

element.scrollIntoView({ behavior: 'smooth' });
複製代碼

好比使用JavaScript來自動檢測對錨點的點擊,這樣瀏覽器就會跳到錨點目標。這種跳轉可能會讓用戶迷失方向(由於它會一閃就跳過去了),因此讓這個動畫有一個過程化將會大大改善用戶體驗。

// Everytime someone clicks on something
document.body.addEventListener('click', e => {

    const href = e.target.href;
    
    // no href attribute, no need to continue then
    if (!href) return;
    
    const id = href.split('#').pop();
    const target = document.getElementById(id);
    
    // no target to scroll to, bail out
    if (!target) return;
    
    // prevent the default quick jump to the target
    e.preventDefault();
    
    // set hash to window location so history is kept correctly
    history.pushState({}, document.title, href);
    
    // smooooooth scroll to the target!
    target.scrollIntoView({
        behavior: 'smooth',
        block: 'start'
    });

});
複製代碼

擴展閱讀:

Web Animations API

Web Animations API已經出來好久了,並且很是的棒。除了能支持日常動畫以外,還可使用PromiserAF和CSS的transition來從新建立它們,會讓動畫效果更接近人體工程學。

使用element.animation能夠很是輕易的讓任何元素根據動畫序列幀播放,相似CSS的@keyframes動畫:

element.animate([
    {transform: 'translateX(0px)', backgroundColor: 'red'},
    {transform: 'translateX(100px)', backgroundColor: 'blue'},
    {transform: 'translateX(50px)', backgroundColor: 'green'},
    {transform: 'translateX(0px)', backgroundColor: 'red'},
    //...
], {
    duration: 3000,
    iterations: 3,
    delay: 0
}).finish.then(_ => console.log('I’m done animating!'));
複製代碼

然而,平常開發要像上面那樣使用動畫系列聲明的方式來定義動畫,須要使用Web Animation Polyfill,它包含了咱們實際所須要的更多功能:

Object.assign(element.style,
    {
        transition: 'transform 1s, background-color 1s',
        backgroundColor: 'red',
        transform: 'translateX(0px)',
    }
);

requestAnimationFramePromise()
    .then(_ => animate(element,
        {transform: 'translateX(100px)', backgroundColor: 'blue'}))
    .then(_ => animate(element,
        {transform: 'translateX(50px)', backgroundColor: 'green'}))
    .then(_ => animate(element,
        {transform: 'translateX(0px)', backgroundColor: 'red'}))
    .then(_ => console.log('I’m done animating!'));
複製代碼

當你使用CSS Transition讓一個元素具備一個動畫效果,其實他有一個transitioned事件:

function transitionEndPromise(element) { 
    return new Promise(resolve => { 
        element.addEventListener('transitionend', function f() { 
            element.removeEventListener('transitionend', f); 
            resolve(); 
        }); 
    }); 
}
複製代碼

這樣使用後不會註冊咱們的偵聽器,也不會泄露內存!有了這個,我可使用Promises替代回調,等待動畫的結束。

咱們也能夠對requestAnimationFrame進行包裝,讓其變得更簡單:

function requestAnimationFramePromise() {
    return new Promise(resolve => requestAnimationFrame(resolve));
}
複製代碼

有了這個,咱們可使用Promise而不是回調來等待下一幀。

咱們能夠將它合併到咱們本身的element.animate()中:

function animate(element, stylz) {
    Object.assign(element.style, stylz);
    return transitionEndPromise(element)
        .then(_ => requestAnimationFramePromise());
}
複製代碼

這是對animationtransition很是輕量的抽象處理,會給不少開發人員帶來不少的便利。

若是您在兩個元素上使用這個技術,而其中一個元素是另外一個元素的祖先,那麼從繼承者的傳遞結束事件將使前面的動畫鏈向前推動。幸運的是,經過檢查事件,能夠很容易的解決這樣的現象,好比event.target

function transitionEndPromise(element) {
    return new Promise(resolve => {
        element.addEventListener('transitionend', function f(event) {
            if (event.target !== element) return;
            element.removeEventListener('transitionend', f);
            resolve();
        });
    });
}
複製代碼

上面的的示例代碼來自於@Surma的《DIY Web Animations: Promises + rAF + Transitions》一文

擴展閱讀:

小結

這一期中咱們先來聊聊will-change、transform和層有關的事情,而後再和你們一塊兒分享幾個在Web中的JavaScript相關的API,好比全屏API、MediaStream API、MediaRecorder API、scrollIntoView API 和 分享 API等。

相關文章
相關標籤/搜索