1. 程式人生 > >HTML5-Service Worker實現離線頁面訪問

HTML5-Service Worker實現離線頁面訪問

如果提到HTML5的新API,WebSocketWeb Workers大家應該比較熟悉。WebSocket是用於簡述請求數量的新協議,Web Workers是用於實現瀏覽器的多執行緒。而今天要介紹的Service Worker是用於頁面離線快取,提供類似App的服務。注意,這和瀏覽器快取不是一回事。

下面所有程式碼請檢視github下載完整版本

1. Service Worker 介紹

試想,當你正在訪問一個人的部落格目錄,當你找到你感興趣的部落格時候,想點選進入檢視完整部落格,這時候斷網了,你將會看到如下頁面:
這裡寫圖片描述
忽略上面的網址,這是我在自己瀏覽器裡使用chrome -> 開發者工具 -> New Work -> offline 模擬斷網,訪問我本地伺服器上網頁的結果。你看到頁面應該類似。這時候僅僅依靠瀏覽器快取是無法解決問題的。於是HTML5提出了 Service Worker,它是一段執行在瀏覽器後臺程序裡的指令碼,它獨立於當前頁面,提供了那些不需要與web頁面互動的功能在網頁背後悄悄執行的能力。在將來,基於它可以實現訊息推送,靜默更新以及地理圍欄等服務,但是目前它首先要具備的功能是攔截和處理網路請求,包括可程式設計的響應快取管理。所以,使用它可以斷網情況下輕鬆實現攔截使用者請求,用已經快取的頁面代替伺服器響應,簡稱離線快取。
注意:Service Worker由於許可權很高,只支援https協議或者localhost

2. Service Worker使用

2.1 生命週期

要讓一個service worker在你的網站上生效,你需要先在你的網頁中註冊它。註冊一個service worker之後,瀏覽器會在後臺默默啟動一個service worker的安裝過程。

在安裝過程中,瀏覽器會載入並快取一些靜態資源。如果所有的檔案被快取成功,service worker就安裝成功了。如果有任何檔案載入或快取失敗,那麼安裝過程就會失敗,service worker就不能被啟用(也即沒能安裝成功)。如果發生這樣的問題,別擔心,它會在下次再嘗試安裝。

當安裝完成後,service worker的下一步是啟用,在這一階段,你還可以升級一個service worker的版本,具體內容我們會在後面講到。

在啟用之後,service worker將接管所有在自己管轄域範圍內的頁面,但是如果一個頁面是剛剛註冊了service worker,那麼它這一次不會被接管,到下一次載入頁面的時候,service worker才會生效。

當service worker接管了頁面之後,它可能有兩種狀態:要麼被終止以節省記憶體,要麼會處理fetch和message事件,這兩個事件分別產生於一個網路請求出現或者頁面上傳送了一個訊息。

總結起來Service Worker的生命週期有如下幾個關鍵步驟(就是常常需要監聽並制定回撥函式的事件):
1. 安裝
2. 啟用,啟用成功之後,開啟chrome://inspect/#service-workers可以檢視到當前執行的service worker
3. 監聽fetch和message事件,下面兩種事件會進行簡要描述
4. 銷燬,是否銷燬由瀏覽器決定,如果一個service worker長期不使用或者機器記憶體有限,則可能會銷燬這個worker
下面具體介紹這幾個事件。

2.2 生命週期中常需監聽的幾個事件

fetch事件

在頁面發起http請求時,service worker可以通過fetch事件攔截請求,並且給出自己的響應。
w3c提供了一個新的fetch api,用於取代XMLHttpRequest,與XMLHttpRequest最大不同有兩點:

  1. fetch()方法返回的是Promise物件,通過then方法進行連續呼叫,減少巢狀。ES6的Promise在成為標準之後,會越來越方便開發人員。

  2. 提供了Request、Response物件,如果做過後端開發,對Request、Response應該比較熟悉。前端要發起請求可以通過url發起,也可以使用Request物件發起,而且Request可以複用。但是Response用在哪裡呢?在service worker出現之前,前端確實不會自己給自己發訊息,但是有了service worker,就可以在攔截請求之後根據需要發回自己的響應,對頁面而言,這個普通的請求結果並沒有區別,這是Response的一處應用。

message事件

頁面和serviceWorker之間可以通過posetMessage()方法傳送訊息,傳送的訊息可以通過message事件接收到。

這是一個雙向的過程,頁面可以發訊息給service worker,service worker也可以傳送訊息給頁面,由於這個特性,可以將service worker作為中間紐帶,使得一個域名或者子域名下的多個頁面可以自由通訊。

install事件

當頁面載入時觸發該事件。常用於快取離線頁面,當斷開網路時,在該事件中快取的頁面將被返回給使用者。

acrive事件

當Service Worker被觸發時觸發該事件。程式碼更新後,通常需要在activate的callback中執行一個管理cache的操作。因為你會需要清除掉之前舊的資料。我們在activate而不是install的時候執行這個操作是因為如果我們在install的時候立馬執行它,那麼依然在執行的舊版本的資料就壞了。

3. Service Worker例項

再次提醒:下面所有程式碼請檢視github下載完整版本

例子很簡單,當我在連線網路時訪問頁面,結果如下:
這裡寫圖片描述
控制檯如下:
這裡寫圖片描述
這裡寫圖片描述
解釋下這裡的 scope,是指可以攔截請求的域。
當我在chrome裡使用chrome -> 開發者工具 -> New Work -> offline 模擬斷網,重新整理頁面:
這裡寫圖片描述
同一個網址,返回了不同頁面。說明Service Worker成功攔截了原始請求(如果不攔截,會像上面那樣出現頁面無法訪問的提示)。

主要程式碼及註釋如下:
index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>test service worker - online page</title>
</head>
<body>
    <div style="text-align:center; margin-top:40px; font-family:monospace;">
     <img src="./online.svg" width="80" />
     <p>您已經連線網路...</p>
   </div>
    <script>
    // 註冊 service worker
    if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('./service-worker.js').then(function(registration) {
        // 註冊成功
        console.log('ServiceWorker registration successful with scope: ', registration.scope);
    }).catch(function(err) {
        // 註冊失敗
        console.log('ServiceWorker registration failed: ', err);
       });
    }
    </script>
</body>
</html>

service-worker.js

'use strict';

var cacheVersion = 0;
var currentCache = {
  offline: 'offline-cache' + cacheVersion
};
const offlineUrl = 'offline.html';

this.addEventListener('install', event => {
  event.waitUntil(
    caches.open(currentCache.offline).then(function(cache) {
      return cache.addAll([
          './offline.svg',
          offlineUrl
      ]);
    })
  );
});

this.addEventListener('fetch', event => {

  if (event.request.mode === 'navigate' || (event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html'))) {
        event.respondWith(
          fetch(event.request.url).catch(error => {
              // Return the offline page
              return caches.match(offlineUrl);
        })
     );
  }
  else{
        event.respondWith(caches.match(event.request)
                        .then(function (response) {
                        return response || fetch(event.request);
                    })
            );
      }
});