ServiceWorker 離線及緩存策略

原文

www.lishuaishuai.com/pwa/1093.ht…javascript

前言

若是你追求極致的Web體驗,你必定在站點中使用過 PWA(Progressive Web App),也必定面臨過在編寫Service Worker代碼時的猶豫不決,由於Service Worker過重要了,一旦註冊在用戶的瀏覽器,全站的請求都會被 Service Worker 控制,一不留神,小問題也成了大問題了。接下來看如何用Service Worker處理離線問題。css

什麼時候進行緩存

On install - as a dependency

ServiceWorker 提供一個 install 事件。可使用該事件作一些準備,即處理其餘事件以前必須完成的操做。 在進行這些操做時,任何之前版本的 ServiceWorker 仍在運行和提供頁面,所以在此處進行的操做必定不能干擾它們。

適合於: CSS、圖像、字體、JS、模板等,基本上囊括了能夠視爲網站「版本」的靜態內容的任何對象。html

若是未能提取上述對象,將使網站徹底沒法運行,對應的本機應用會將這些對象包含在初始下載中。java

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('mysite-static-v3').then(function(cache) {
      return cache.addAll([
        '/css/whatever-v3.css',
        '/css/imgs/sprites-v6.png',
        '/css/fonts/whatever-v8.woff',
        '/js/all-min-v4.js'
        // etc
      ]);
    })
  );
});
複製代碼

event.waitUntil 選取一個 promise 以定義安裝時長和安裝是否成功。 若是 promise reject,則安裝被視爲失敗,並捨棄這個 ServiceWorker (若是一個較舊的版本正在運行,它將保持不變)。caches.opencache.addAll 將返回 promise。若是其中有任一資源獲取失敗,則 cache.addAll 執行rejectweb

On install - not as a dependency

與上述類似,但若是緩存失敗,既不會延遲安裝也不會致使安裝失敗。json

適合於: 不是即刻須要的大型資源,如用於遊戲較高級別的資源。promise

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('mygame-core-v1').then(function(cache) {
      cache.addAll(
        // levels 11-20
      );
      return cache.addAll(
        // core assets & levels 1-10
      );
    })
  );
});
複製代碼

咱們不會將級別 11-20 的 cache.addAll promise 傳遞迴 event.waitUntil,所以,即便它失敗,遊戲在離線狀態下仍然可用。固然,必須考慮到可能缺乏這些資源的狀況,而且若是缺乏,則從新嘗試緩存它們。瀏覽器

當級別 1-10 進行下載時,ServiceWorker 可能會終止,意味着它們將不會被緩存。 未來,咱們計劃添加一個後臺下載 API 以處理此類狀況和較大文件下載,如電影。緩存

On activate

適合於: 清理和遷移。服務器

在新的 ServiceWorker 已安裝而且未使用之前版本的狀況下,新 ServiceWorker 將激活,而且將得到一個 activate 事件。 因爲舊版本退出,此時很是適合處理 IndexedDB 中的架構遷移和刪除未使用的緩存。

self.addEventListener('activate', function(event) {
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.filter(function(cacheName) {
          // Return true if you want to remove this cache,
          // but remember that caches are shared across
          // the whole origin
        }).map(function(cacheName) {
          return caches.delete(cacheName);
        })
      );
    })
  );
});
複製代碼

在激活期間,fetch 等其餘事件會放置在一個隊列中,所以長時間激活可能會阻止頁面加載。 儘量讓您的激活簡潔,僅針對舊版本處於活動狀態時沒法執行的操做使用它。

On user interaction

適合於: 若是整個網站沒法離線工做,您能夠容許用戶選擇他們須要離線可用的內容。 例如,YouTube 上的某個視頻、維基百科上的某篇文章、Flickr 上的某個特定圖庫。

爲用戶提供一個「Read later」或「Save for offline」按鈕。在點擊該按鈕後,從網絡獲取您須要的內容並將其置於緩存中。

document.querySelector('.cache-article').addEventListener('click', function(event) {
  event.preventDefault();

  var id = this.dataset.articleId;
  caches.open('mysite-article-' + id).then(function(cache) {
    fetch('/get-article-urls?id=' + id).then(function(response) {
      // /get-article-urls returns a JSON-encoded array of
      // resource URLs that a given article depends on
      return response.json();
    }).then(function(urls) {
      cache.addAll(urls);
    });
  });
});
複製代碼

caches API可經過頁面以及service workers獲取,這意味着您不須要經過service workers向緩存添加內容。

On network response

適合於: 頻繁更新諸如用戶收件箱或文章內容等資源。 同時適用於不重要的資源,如頭像,但須要謹慎處理。

若是請求的資源與緩存中的任何資源均不匹配,則從網絡中獲取,將其發送到頁面同時添加到緩存中。

若是您針對一系列網址執行此操做,如頭像,那麼您須要謹慎,不要使源的存儲變得臃腫,若是用戶須要回收磁盤空間,您不會想成爲主要候選對象。請確保將緩存中再也不須要的項目刪除。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.open('mysite-dynamic').then(function(cache) {
      return cache.match(event.request).then(function (response) {
        return response || fetch(event.request).then(function(response) {
          cache.put(event.request, response.clone());
          return response;
        });
      });
    })
  );
});
複製代碼

爲留出充足的內存使用空間,每次您只能讀取一個響應/請求的正文。 在上面的代碼中,.clone() 用於建立可單獨讀取的額外副本。

Stale-while-revalidate

適合於: 頻繁更新最新版本並不是必需的資源。 頭像屬於此類別。

若是有可用的緩存版本,則使用該版本,但下次會獲取更新。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.open('mysite-dynamic').then(function(cache) {
      return cache.match(event.request).then(function(response) {
        var fetchPromise = fetch(event.request).then(function(networkResponse) {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        })
        return response || fetchPromise;
      })
    })
  );
});
複製代碼

這與 HTTP 的 stale-while-revalidate 很是類似。

On push message

Push API 是基於 ServiceWorker 構建的另外一個功能。 該 API 容許喚醒 ServiceWorker 以響應來自操做系統消息傳遞服務的消息。即便用戶沒有爲您的網站打開標籤,也會如此,僅喚醒 ServiceWorker。 您從頁面請求執行此操做的權限,用戶將收到提示。

適合於: 與通知相關的內容,如聊天消息、突發新聞或電子郵件。 同時可用於頻繁更改受益於當即同步的內容,如待辦事項更新或日曆更改。

常見的最終結果是出現一個通知,在點按該通知時,打開/聚焦一個相關頁面,但在進行此操做前必定要先更新緩存。

很明顯,用戶在收到推送通知是處於在線狀態,可是,當他們最終與通知交互時可能已經離線,所以,容許離線訪問此內容很是重要。Twitter 本機應用在大多數狀況下都是很是好的離線優先例子,但在這點上卻有點問題。

若是沒有網絡鏈接,Twitter 沒法提供與推送消息相關的內容。 不過,點按通知會移除通知,從而使用戶獲取的信息將比點按通知前少。 不要這樣作!

在顯示通知以前,如下代碼將更新緩存:

self.addEventListener('push', function(event) {
  if (event.data.text() == 'new-email') {
    event.waitUntil(
      caches.open('mysite-dynamic').then(function(cache) {
        return fetch('/inbox.json').then(function(response) {
          cache.put('/inbox.json', response.clone());
          return response.json();
        });
      }).then(function(emails) {
        registration.showNotification("New email", {
          body: "From " + emails[0].from.name
          tag: "new-email"
        });
      })
    );
  }
});

self.addEventListener('notificationclick', function(event) {
  if (event.notification.tag == 'new-email') {
    // Assume that all of the resources needed to render
    // /inbox/ have previously been cached, e.g. as part
    // of the install handler.
    new WindowClient('/inbox/');
  }
});
複製代碼

On background-sync

後臺同步是基於 ServiceWorker 構建的另外一個功能。它容許您一次性或按(很是具備啓發性的)間隔請求後臺數據同步。 即便用戶沒有爲您的網站打開標籤,也會如此,僅喚醒 ServiceWorker。您從頁面請求執行此操做的權限,用戶將收到提示。

適合於: 非緊急更新,特別那些按期進行的更新,每次更新都發送一個推送通知會顯得太頻繁,如社交時間表或新聞文章。

self.addEventListener('sync', function(event) {
  if (event.id == 'update-leaderboard') {
    event.waitUntil(
      caches.open('mygame-dynamic').then(function(cache) {
        return cache.add('/leaderboard.json');
      })
    );
  }
});
複製代碼

緩存持久化

爲您的源提供特定量的可用空間以執行它須要的操做。該可用空間可在全部源存儲之間共享。 LocalStorage、IndexedDB、Filesystem,固然還有 Caches。

您獲取的空間容量未指定,其因設備和存儲條件而異。 您能夠經過如下代碼瞭解您已得到多少空間容量:

navigator.storageQuota.queryInfo("temporary").then(function(info) {
  console.log(info.quota);
  // Result: <quota in bytes>
  console.log(info.usage);
  // Result: <used data in bytes>
});
複製代碼

不過,與全部瀏覽器存儲同樣,若是設備出現存儲壓力,瀏覽器將隨時捨棄這些空間。 遺憾的是,瀏覽器沒法區分您想要不惜任何代價保留的電影和您不太關心的遊戲之間有什麼不一樣。

爲解決此問題,建議使用 API requestPersistent

// From a page:
navigator.storage.requestPersistent().then(function(granted) {
  if (granted) {
    // Hurrah, your data is here to stay!
  }
});
複製代碼

固然,用戶必須授予權限。讓用戶參與此流程很是重要,由於如今咱們能夠預期用戶會控制刪除。若是用戶的設備出現存儲壓力,並且清除不重要的數據沒能解決問題,那麼用戶須要憑判斷力決定保留哪些項目以及移除哪些項目。

爲實現此目的,須要操做系統將「持久化」源等同於其存儲使用空間細分中的本機應用,而不是做爲單個項目報告給瀏覽器。

緩存策略

不管您緩存多少內容 ServiceWorker 都不會使用緩存,除非您指示它在什麼時候使用緩存以及如何使用。 如下是用於處理請求的幾個模式:

Cache only

適合於: 您認爲屬於該「版本」網站靜態內容的任何資源。您應在安裝事件中緩存這些資源,以便您能夠依靠它們。

self.addEventListener('fetch', function(event) {
  // If a match isn't found in the cache, the response
  // will look like a connection error
  event.respondWith(caches.match(event.request));
});
複製代碼

Network Only

適合於: 沒有相應離線資源的對象,如 analytics pings、non-GET 請求。

self.addEventListener('fetch', function(event) {
  event.respondWith(fetch(event.request));
  // or simply don't call event.respondWith, which
  // will result in default browser behaviour
});
複製代碼

Cache First

適合於: 若是您以離線優先的方式進行構建,這將是您處理大多數請求的方式。 根據傳入請求而定,其餘模式會有例外。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      return response || fetch(event.request);
    })
  );
});
複製代碼

其針對緩存中的資源爲您提供「僅緩存」行爲,而對於未緩存的資源則提供「僅網絡」行爲(其包含全部 non-GET 請求,由於它們沒法緩存)。

Cache & network race

適合於: 小型資源,可用於改善磁盤訪問緩慢的設備的性能。

在硬盤較舊、具備病毒掃描程序且互聯網鏈接很快這幾種情形相結合的狀況下,從網絡獲取資源比訪問磁盤更快。不過,若是在用戶設備上具備相關內容時訪問網絡會浪費流量,請記住這一點。

// Promise.race is no good to us because it rejects if
// a promise rejects before fulfilling.Let's make a proper
// race function:
function promiseAny(promises) {
  return new Promise((resolve, reject) => {
    // make sure promises are all promises
    promises = promises.map(p => Promise.resolve(p));
    // resolve this promise as soon as one resolves
    promises.forEach(p => p.then(resolve));
    // reject if all promises reject
    promises.reduce((a, b) => a.catch(() => b))
      .catch(() => reject(Error("All failed")));
  });
};

self.addEventListener('fetch', function(event) {
  event.respondWith(
    promiseAny([
      caches.match(event.request),
      fetch(event.request)
    ])
  );
});
複製代碼

Network First

適合於: 快速修復(在該「版本」的網站外部)頻繁更新的資源。 例如,文章、頭像、社交媒體時間表、遊戲排行榜。

這意味着您爲在線用戶提供最新內容,但離線用戶會得到較舊的緩存版本。 若是網絡請求成功,您可能須要更新緩存條目。

不過,此方法存在缺陷。若是用戶的網絡時斷時續或很慢,他們只有在網絡出現故障後才能得到已存在於設備上的徹底可接受的內容。這須要花很長的時間,而且會致使使人失望的用戶體驗。 請查看下一個模式,緩存而後訪問網絡,以得到更好的解決方案。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    fetch(event.request).catch(function() {
      return caches.match(event.request);
    })
  );
});
複製代碼

Cache then network

適合於: 頻繁更新的內容。例如,文章、社交媒體時間表、遊戲排行榜。

這須要頁面進行兩次請求,一次是請求緩存,另外一次是請求訪問網絡。 該想法是首先顯示緩存的數據,而後在網絡數據到達時更新頁面。

有時候,當新數據(例如,遊戲排行榜)到達時,您能夠只替換當前數據,可是具備較大的內容時將致使數據中斷。從根本上講,不要使用戶正在讀取或交互的內容「消失」。

Twitter 在舊內容上添加新內容,並調整滾動位置,以便用戶不會感受到間斷。 這是可能的,由於 Twitter 一般會保持使內容最具線性特性的順序。 我爲 trained-to-thrill 複製了此模式,以儘快獲取屏幕上的內容,但當它出現時仍會顯示最新內容。

頁面中的代碼:

var networkDataReceived = false;

startSpinner();

// fetch fresh data
var networkUpdate = fetch('/data.json').then(function(response) {
  return response.json();
}).then(function(data) {
  networkDataReceived = true;
  updatePage();
});

// fetch cached data
caches.match('/data.json').then(function(response) {
  if (!response) throw Error("No data");
  return response.json();
}).then(function(data) {
  // don't overwrite newer network data
  if (!networkDataReceived) {
    updatePage(data);
  }
}).catch(function() {
  // we didn't get cached data, the network is our last hope:
  return networkUpdate;
}).catch(showErrorMessage).then(stopSpinner);
複製代碼

ServiceWorker 中的代碼:

咱們始終訪問網絡並隨時更新緩存。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.open('mysite-dynamic').then(function(cache) {
      return fetch(event.request).then(function(response) {
        cache.put(event.request, response.clone());
        return response;
      });
    })
  );
});
複製代碼

Generic fallback

若是您未能從緩存和/或網絡提供一些資源,您可能須要提供一個常規回退。

適合於: 次要圖像,如頭像、失敗的 POST 請求、「Unavailable while offline」頁面。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    // Try the cache
    caches.match(event.request).then(function(response) {
      // Fall back to network
      return response || fetch(event.request);
    }).catch(function() {
      // If both fail, show a generic fallback:
      return caches.match('/offline.html');
      // However, in reality you'd have many different
      // fallbacks, depending on URL & headers.
      // Eg, a fallback silhouette image for avatars.
    })
  );
});
複製代碼

若是您的頁面正在發佈電子郵件,您的 ServiceWorker 可能回退以在 IDB 的發件箱中存儲電子郵件並進行響應,讓用戶知道發送失敗,但數據已成功保存。

ServiceWorker-side templating

適合於: 沒法緩存其服務器響應的頁面。

在服務器上渲染頁面可提升速度,但這意味着會包括在緩存中沒有意義的狀態數據,例如,「Logged in as…」。若是您的頁面由 ServiceWorker 控制,您可能會轉而選擇請求 JSON 數據和一個模板,並進行渲染。

importScripts('templating-engine.js');

self.addEventListener('fetch', function(event) {
  var requestURL = new URL(event.request);

  event.respondWith(
    Promise.all([
      caches.match('/article-template.html').then(function(response) {
        return response.text();
      }),
      caches.match(requestURL.path + '.json').then(function(response) {
        return response.json();
      })
    ]).then(function(responses) {
      var template = responses[0];
      var data = responses[1];

      return new Response(renderTemplate(template, data), {
        headers: {
          'Content-Type': 'text/html'
        }
      });
    })
  );
});
複製代碼

總結

沒必要選擇上述的某一個方法,能夠根據請求網址使用其中的多個方法。

參考文獻

developers.google.com/web/fundame… developers.google.com/web/tools/w…

相關文章
相關標籤/搜索