1. 程式人生 > >[譯]前端離線指南(下)

[譯]前端離線指南(下)

[譯]前端離線指南(上)

原文連結:The offline cookbook 作者:Jake Archibald

快取持久化

為您的站點提供一定量的可用空間來執行其所需的操作。該可用空間可在站點中所有儲存之間共享:LocalStorage、IndexedDB、Filesystem,當然也包含Caches。

您能獲取到的空間容量是不一定的,同時由於裝置和儲存條件的差異也會有所不同。您可以通過下面的程式碼來檢視您已獲得的空間容量:

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

然而,與所有瀏覽器儲存一樣,如果裝置面臨儲存壓力,瀏覽器就會隨時捨棄這些儲存內容。但遺憾的是,瀏覽器無法區分您珍藏的電影,和您沒啥興趣的遊戲之間有啥區別。

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

// 在頁面中執行
navigator.storage.requestPersistent().then((granted
) =>
{ if (granted) { // 啊哈,資料儲存在這裡呢 } }); 複製程式碼

當然,使用者必須要授予許可權。讓使用者參與進這個流程是很有必要的,因為我們可以預期使用者會控制刪除。如果使用者手中的裝置面臨儲存壓力,而且清除不重要的資料還沒能解決問題,那麼使用者就需要根據自己的判斷來決定刪除哪些專案以及保留哪些專案。

為了實現此目的,需要作業系統將“持久化”源等同於其儲存使用空間細分中的本機應用,而不是作為單個專案報告給瀏覽器。

快取建議-響應請求

無論您打算快取多少內容,除非您告訴ServiceWorker應當在何時以及如何去快取內容,ServiceWorker不會去主動使用快取。下面是幾種用於處理請求的策略。

僅快取(cache only)

適用於: 您認為在站點的“該版本”中屬於靜態內容的任何資源。您應當在install事件中就快取這些資源,以便您可以在處理請求的時候依靠它們。

self.addEventListener('fetch', (event) => {
  // 如果某個匹配到的資源在快取中找不到,
  // 則響應結果看起來就會像一個連線錯誤。
  event.respondWith(caches.match(event.request));
});
複製程式碼

...儘管您一般不需要通過特殊的方式來處理這種情況,但“快取,回退到網路”涵蓋了這種策略。

僅網路(network only)

適用於: 沒有相應的離線資源的物件,比如analytics pings,非GET請求。

self.addEventListener('fetch', (event) => {
  event.respondWith(fetch(event.request));
  // 或者簡單地不再呼叫event.respondWith,這樣就會
  // 導致預設的瀏覽器行為
});
複製程式碼

...儘管您一般不需要通過特殊的方式來處理這種情況,但“快取,回退到網路”涵蓋了這種策略。

快取優先,若無快取則回退到網路(Cache, falling back to network)

適用於: 如果您以快取優先的方式構建,那麼這種策略就是您處理大多數請求時所用的策略 根據傳入請求而定,其他策略會有例外。

self.addEventListener('fetch', (event) => {
  event.respondWith(async function() {
    const response = await caches.match(event.request);
    return response || fetch(event.request);
  }());
});
複製程式碼

其中,針對已快取的資源提供“Cache only”的行為,針對未快取的資源(包含所有非GET請求,因為它們根本無法被快取)提供“Network only”的行為。

快取與網路競爭

適用於: 儲存在讀取速度慢的硬碟中的小型資源。

在老舊硬碟、病毒掃描程式、和較快網速這幾種因素都存在的情況下,從網路中獲取資源可能比從硬碟中獲取的速度更快。不過,通過網路獲取已經在使用者裝置中儲存過的內容,是一種浪費流量的行為,所以請牢記這一點。

// Promise.race 對我們來說並不太好,因為若當其中一個promise在
// fulfilling之前reject了,那麼整個Promise.race就會返回reject。
// 我們來寫一個更好的race函式:
function promiseAny(promises) {
  return new Promise((resolve, reject) => {
    // 確保promises代表所有的promise物件。
    promises = promises.map(p => Promise.resolve(p));
    // 只要當其中一個promise物件呼叫了resolve,就讓此promise物件變成resolve的
    promises.forEach(p => p.then(resolve));
    // 如果傳入的所有promise都reject了,就讓此promise物件變成resject的
    promises.reduce((a, b) => a.catch(() => b))
      .catch(() => reject(Error("All failed")));
  });
};

self.addEventListener('fetch', (event) => {
  event.respondWith(
    promiseAny([
      caches.match(event.request),
      fetch(event.request)
    ])
  );
});
複製程式碼

通過網路獲取失敗回退到快取(Network falling back to cache)

適用於: 對頻繁更新的資源進行快速修復。例如:文章、頭像、社交媒體時間軸、遊戲排行榜等。

這就意味著您可以為線上使用者提供最新內容,但是離線使用者獲取到的是較老的快取版本。如果網路請求成功,您可能需要更新快取。

不過,這種方法存在缺陷。如果使用者的網路斷斷續續,或者網速超慢,則使用者可能會在從自己裝置中獲取更好的、可接受的內容之前,花很長一段時間去等待網路請求失敗。這樣的使用者體驗是非常糟糕的。請檢視下一個更好的解決方案:“快取然後訪問網路”。

self.addEventListener('fetch', (event) => {
  event.respondWith(async function() {
    try {
      return await fetch(event.request);
    } catch (err) {
      return caches.match(event.request);
    }
  }());
});
複製程式碼

快取然後訪問網路

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

這種策略需要頁面發起兩個請求,一個是請求快取,一個是請求網路。首先展示快取資料,然後當網路資料到達的時候,更新頁面。

有時候,您可以在獲取到新的資料的時候,只替換當前資料(比如:遊戲排行榜),但是具有較大的內容時將導致資料中斷。基本上講,不要在使用者可能正在閱讀或正在操作的內容突然“消失”。

Twitter在舊內容上新增新內容,並調整滾動的位置,以便讓使用者感知不到。這是可能的,因為 Twitter 通常會保持使內容最具線性特性的順序。 我為 trained-to-thrill 複製了此模式,以儘快獲取螢幕上的內容,但當它出現時仍會顯示最新內容。 頁面中的程式碼

async function update() {
  // 儘可能地發起網路請求
  const networkPromise = fetch('/data.json');

  startSpinner();

  const cachedResponse = await caches.match('/data.json');
  if (cachedResponse) await displayUpdate(cachedResponse);

  try {
    const networkResponse = await networkPromise;
    const cache = await caches.open('mysite-dynamic');
    cache.put('/data.json', networkResponse.clone());
    await displayUpdate(networkResponse);
  } catch (err) {
   
  }

  stopSpinner();

  const networkResponse = await networkPromise;

}

async function displayUpdate(response) {
  const data = await response.json();
  updatePage(data);
}
複製程式碼

常規回退

如果您未能從網路和快取中提供某些資源,您可能需要一個常規回退策略。

適用於: 次要的圖片,比如頭像,失敗的POST請求,“離線時不可用”的頁面。

self.addEventListener('fetch', (event) => {
  event.respondWith(async function() {
    // 嘗試從快取中匹配
    const cachedResponse = await caches.match(event.request);
    if (cachedResponse) return cachedResponse;

    try {
      // 回退到網路
      return await fetch(event.request);
    } catch (err) {
      // 如果都失敗了,啟用常規回退:
      return caches.match('/offline.html');
      // 不過,事實上您需要根據URL和Headers,準備多個不同回退方案
      // 例如:頭像的兜底圖
    }
  }());
});
複製程式碼

您回退到的專案可能是一個“安裝依賴項”(見《前端離線指南(上)》中的“安裝時——以依賴的形式”小節)。

ServiceWorker-side templating

適用於: 無法快取其伺服器響應的頁面。

在伺服器上渲染頁面可提高速度,但這意味著會包括在快取中沒有意義的狀態資料,例如,“Logged in as…”。如果您的頁面由 ServiceWorker 控制,您可能會轉而選擇請求 JSON 資料和一個模板,並進行渲染。

importScripts('templating-engine.js');

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

  event.responseWith(async function() {
    const [template, data] = await Promise.all([
      caches.match('/article-template.html').then(r => r.text()),
      caches.match(requestURL.path + '.json').then(r => r.json()),
    ]);

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

總結

您不必只選擇其中的一種方法,您可以根據請求URL選擇使用多種方法。比如,在trained-to-thrill中使用了:

只需要根據請求,就能決定要做什麼:

self.addEventListener('fetch', (event) => {
  // Parse the URL:
  const requestURL = new URL(event.request.url);

  // Handle requests to a particular host specifically
  if (requestURL.hostname == 'api.example.com') {
    event.respondWith(/* some combination of patterns */);
    return;
  }
  // Routing for local URLs
  if (requestURL.origin == location.origin) {
    // Handle article URLs
    if (/^\/article\//.test(requestURL.pathname)) {
      event.respondWith(/* some other combination of patterns */);
      return;
    }
    if (requestURL.pathname.endsWith('.webp')) {
      event.respondWith(/* some other combination of patterns */);
      return;
    }
    if (request.method == 'POST') {
      event.respondWith(/* some other combination of patterns */);
      return;
    }
    if (/cheese/.test(requestURL.pathname)) {
      event.respondWith(
        new Response("Flagrant cheese error", {
          status: 512
        })
      );
      return;
    }
  }

  // A sensible default pattern
  event.respondWith(async function() {
    const cachedResponse = await caches.match(event.request);
    return cachedResponse || fetch(event.request);
  }());
});
複製程式碼

鳴謝

感謝下列諸君為本文提供那些可愛的圖示:

同時感謝 Jeff Posnick 在我點選“釋出”按鈕之前,為我找到多處明顯錯誤。

擴充套件閱讀