Książka kucharska offline

Jake Archibald
Jake Archibald

Dzięki Service Worker zrezygnowaliśmy z rozwiązywania problemów offline i daliśmy deweloperom dostęp do potrzebnych elementów. Daje Ci kontrolę nad buforowaniem i sposobem obsługi żądań. Oznacza to, że możesz tworzyć własne wzorce. Przyjrzyjmy się kilku możliwym schematom izolacji, ale w praktyce w zależności od adresu URL i kontekstu będziesz korzystać z nich równolegle.

Praktyczną prezentację niektórych z tych wzorców znajdziesz w tym filmie i tym filmie o wpływie na wyniki.

Pamięć podręczna – kiedy przechowywać zasoby

Skrypt service worker pozwala obsługiwać żądania niezależnie od buforowania, więc zademonstruję je oddzielnie. Po pierwsze, pamięć podręczna. Kiedy należy to zrobić?

Podczas instalacji – jako zależność

Podczas instalacji – jako zależność.
Przy instalacji – jako zależność.

Skrypt service worker udostępnia zdarzenie install. Możesz go użyć do przygotowania rzeczy, które muszą być gotowe, zanim zajmiesz się innymi zdarzeniami. Mimo że taka sytuacja ma miejsce w przypadku wcześniejszych wersji mechanizmu Service Worker, wciąż działa i wyświetla strony, więc czynności, które tu wykonujesz, nie mogą zakłócić działania.

Idealny do: CSS, obrazów, czcionek, kodu JS, szablonów... w zasadzie wszystkich treści, które uważasz za statyczne dla tej „wersji” witryny.

Są to sytuacje, w których witryna zupełnie nie będzie działać, jeśli nie uda się ich pobrać, a odpowiednia aplikacja na konkretnej platformie będzie miała miejsce przy pierwszym pobieraniu.

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 na podstawie obietnicy określa długość i skuteczność instalacji. Jeśli obietnica zostanie odrzucona, instalacja zostanie uznana za niepowodzenie, a ten skrypt service worker zostanie porzucony (jeśli starsza wersja jest uruchomiona, pozostanie bez zmian). Obietnice zwrotu caches.open() i cache.addAll(). Jeśli nie uda się pobrać któregoś z zasobów, wywołanie cache.addAll() zostanie odrzucone.

W przypadku filmu wytrenowanego do zaciekawienia używam go do buforowania zasobów statycznych.

Podczas instalacji – nie jako zależność

Podczas instalacji – nie jako zależność.
Po instalacji – nie jako zależność.

To działanie jest podobne do opisanych powyżej, ale nie opóźni dokończenia instalacji i nie spowoduje błędu instalacji w przypadku niepowodzenia buforowania.

Idealny w przypadku: większych zasobów, których nie potrzebujesz od razu, takich jak zasoby używane na późniejszych poziomach gry.

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 and levels 1–10
        ();
    }),
  );
});

Powyższy przykład nie spełnia obietnicy cache.addAll w przypadku poziomów 11–20 z powrotem do użytkownika event.waitUntil. Nawet jeśli się nie uda, gra będzie nadal dostępna offline. Oczywiście musisz uwzględnić możliwe braki w tych poziomach i ponownie je zapisać w pamięci podręcznej, jeśli ich nie znajdziesz.

Skrypt service worker może zostać wyłączony podczas pobierania poziomów 11–20, ponieważ zakończył obsługę zdarzeń, co oznacza, że nie będą one przechowywane w pamięci podręcznej. W przyszłości Web Periodic Background Synchronization API będzie obsługiwać takie przypadki i większe pliki do pobrania, takie jak filmy. Ten interfejs API jest obecnie obsługiwany tylko w rozwidlach Chromium.

W przypadku aktywacji

Wł. aktywacji.
Przy aktywacji.

Idealny do: czyszczenia i migracji.

Gdy nowy skrypt Service Worker zostanie zainstalowany i poprzednia wersja nie jest używana, aktywuje się nowa i otrzymasz zdarzenie activate. Stara wersja nie jest już potrzebna, więc warto zająć się migracją schematu w IndexedDB i usunąć nieużywane pamięci podręczne.

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);
          }),
      );
    }),
  );
});

Podczas aktywacji inne zdarzenia, takie jak fetch, są umieszczane w kolejce, więc długa aktywacja może spowodować zablokowanie wczytania strony. Pamiętaj, aby maksymalnie ograniczyć zakres aktywacji i korzystać z niej tylko w przypadku rzeczy, które nie mogły zostać wykonane, gdy stara wersja była aktywna.

W przypadku trenowania do czerpania przyjemności używam go do usuwania starych pamięci podręcznych.

Po interakcji użytkownika

Przy interakcji użytkownika.
Przy interakcji użytkownika.

Idealny w sytuacjach: gdy nie można przełączyć całej witryny w tryb offline i zezwolisz użytkownikowi na wybranie treści, które mają być dostępne offline. Może to być np. film w YouTube, artykuł w Wikipedii, konkretna galeria w serwisie Flickr.

Udostępnij użytkownikowi przycisk „Przeczytaj później” lub „Zapisz do zapisywania offline”. Po kliknięciu pobierz z sieci to, czego potrzebujesz, i umieść w pamięci podręcznej.

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);
      });
  });
});

Interfejs caches API jest dostępny zarówno na stronach, jak i w skryptach service worker, co oznacza, że możesz go dodawać do pamięci podręcznej bezpośrednio z poziomu strony.

Odpowiedź po połączeniu z siecią

Odpowiedź sieciowa.
Odpowiedź w sieci.

Idealny w przypadku: częstego aktualizowania zasobów, takich jak skrzynka odbiorcza użytkownika czy treść artykułów. Ta opcja jest też przydatna w przypadku mniej ważnych treści, takich jak awatary, ale wymaga uwagi.

Jeśli żądanie nie odpowiada niczemu w pamięci podręcznej, pobierz je z sieci, wyślij na stronę i jednocześnie dodaj do pamięci podręcznej.

Jeśli robisz to w przypadku różnych adresów URL, na przykład awatarów, musisz uważać, aby nie nadużywać miejsca na dane w witrynie źródłowej. Jeśli użytkownik musi zwolnić miejsce na dysku, nie warto brać udziału w grze jako kandydat. Upewnij się, że z pamięci podręcznej zostały usunięte elementy, których już nie potrzebujesz.

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;
          })
        );
      });
    }),
  );
});

Aby umożliwić wydajne wykorzystanie pamięci, treść odpowiedzi lub żądania możesz odczytać tylko raz. Powyższy kod korzysta z usługi .clone() do tworzenia dodatkowych kopii, które można odczytywać oddzielnie.

W ramach wytrenowanego do zabiegu używam go do buforowania obrazów z serwisu Flickr.

Nieaktualny podczas ponownej weryfikacji

Nieaktualny podczas ponownej weryfikacji.
Nieaktywny podczas ponownej weryfikacji.

Idealny w przypadku: częstego aktualizowania zasobów, gdy najnowsza wersja nie jest konieczna. Do tej kategorii mogą należeć awatary.

Jeśli dostępna jest wersja z pamięci podręcznej, użyj jej, ale pobierz aktualizację na przyszłość.

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;
      });
    }),
  );
});

Jest to bardzo podobne do metody stale-pending-revalidate (nieaktualny w czasie ponownej weryfikacji) w HTTP.

W wiadomości push

W wiadomości push.
W wiadomości push.

Push API to kolejna funkcja oparta na mechanizmie Service Worker. Dzięki temu mechanizm Service Worker może być aktywowany w odpowiedzi na komunikat z usługi do przesyłania wiadomości systemu operacyjnego. Dzieje się tak nawet wtedy, gdy użytkownik nie ma otwartej karty w Twojej witrynie. Wybudzany jest tylko skrypt service worker. Gdy poprosisz o wykonanie tej czynności na stronie, użytkownik zostanie o tym powiadomiony.

Idealny w przypadku: treści związanych z powiadomieniem, np. wiadomości na czacie, aktualności lub e-maili. Również rzadkie zmiany treści, które są objęte natychmiastową synchronizacją, np. aktualizacja listy zadań lub zmiana kalendarza.

Najczęstszym wynikiem jest powiadomienie, które po kliknięciu otwiera lub ustawia wybraną stronę na odpowiedniej stronie, ale dla tego extremely ważne jest wcześniejsze zaktualizowanie pamięci podręcznej. Użytkownik jest oczywiście online w momencie otrzymania wiadomości push, ale nie musi jeszcze wejść w interakcję z powiadomieniem, dlatego ważne jest udostępnienie treści offline.

Ten kod aktualizuje pamięć podręczną przed wyświetleniem powiadomienia:

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/');
  }
});

Synchronizacja w tle

Synchronizacja w tle
Przy synchronizacji w tle.

Synchronizacja w tle to kolejna funkcja oparta na mechanizmie Service Worker. Pozwala zażądać synchronizacji danych w tle jednorazowo lub w odstępach heurystycznych. Dzieje się tak nawet wtedy, gdy użytkownik nie ma otwartej karty w witrynie. Wybudzany jest tylko skrypt service worker. Gdy poprosisz o zezwolenie na zrobienie tego na stronie, użytkownik zobaczy taką prośbę.

Idealne w przypadku: mniej pilnych informacji, zwłaszcza takich, które odbywają się tak regularnie, że wysyłanie wiadomości push w ramach aktualizacji byłoby zbyt częste dla użytkowników. Przykładem może być harmonogram w mediach społecznościowych lub artykuły z wiadomościami.

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

Trwałość pamięci podręcznej

Twój punkt początkowy otrzymuje pewną ilość wolnego miejsca na to, z czym chce korzystać. Wolne miejsce jest współużytkowane przez wszystkie pamięci źródłowe: (lokalne) pamięć masową, IndexedDB, dostęp do systemu plików i oczywiście pamięci podręczne.

Kwota nie jest określona. Będzie ona różnić się w zależności od urządzenia i warunków przechowywania. Możesz dowiedzieć się, ile udało Ci się uzyskać dzięki:

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

Jednak tak jak w przypadku całej pamięci przeglądarki, jeśli na urządzeniu brakuje pamięci, przeglądarka może wyrzucić Twoje dane. Przeglądarka nie jest w stanie odróżnić filmów, które chcesz zachować za wszelką cenę, od tych, które Cię nie interesują.

Aby obejść ten problem, użyj interfejsu StorageManager:

// From a page:
navigator.storage.persist()
.then(function(persisted) {
  if (persisted) {
    // Hurrah, your data is here to stay!
  } else {
   // So sad, your data may get chucked. Sorry.
});

Oczywiście użytkownik musi udzielić zgody. W tym celu użyj interfejsu Permissions API.

Warto włączyć użytkowników do tego procesu, ponieważ mamy kontrolę nad usunięciem danych. Jeśli na jego urządzeniu zacznie brakować miejsca na dane, a wyczyszczenie mniej ważnych danych nie rozwiąże problemu, użytkownik może ocenić, które elementy należy zachować, a które usunąć.

Aby tak się stało, systemy operacyjne muszą traktować „trwałe” źródła jako odpowiednik aplikacji na danej platformie w zestawieniach wykorzystania miejsca na dane, a nie zgłaszać przeglądarkę jako pojedynczy element.

Sugestie dotyczące wyświetlania – odpowiadanie na żądania

Nie ma znaczenia, jak bardzo buforujesz dane – mechanizm Service Worker nie będzie z niej korzystać, chyba że powiesz, kiedy i jak. Oto kilka wzorców obsługi żądań:

Tylko pamięć podręczna

Tylko pamięć podręczna.
Tylko pamięć podręczna.

Idealny do: wszystkiego, co uważasz za statyczne w konkretnej „wersji” witryny. Należy je zapisać w pamięci podręcznej podczas zdarzenia instalacji, więc na pewno się tam znajdą.

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));
});

...chociaż często nie musisz zajmować się tym przypadkiem, pozwala to zrobić w przypadku pamięci podręcznej, powracając do sieci.

Tylko sieć

Tylko sieć.
Tylko sieć.

Idealny do: funkcji, które nie mają odpowiednika offline, takich jak pingi analityczne czy żądania inne niż GET.

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

...chociaż często nie musisz zajmować się tym przypadkiem, pozwala to zrobić w przypadku pamięci podręcznej, powracając do sieci.

Pamięć podręczna, z powrotem do sieci

Pamięć podręczna, przełączam się z powrotem na sieć.
Pamięć podręczna, z powrotem do sieci.

Idealne rozwiązanie: tworzenie głównie offline. W taki sposób zajmij się większością próśb. Inne wzorce będą wyjątkami na podstawie przychodzącego żądania.

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

Daje to działanie „tylko pamięć podręczna” w przypadku elementów w pamięci podręcznej, a działanie „tylko sieć” w przypadku obiektów nieprzechowywanych w pamięci podręcznej (co obejmuje wszystkie żądania inne niż GET, ponieważ nie mogą być przechowywane w pamięci podręcznej).

Pamięć podręczna i wyścig sieci

Pamięć podręczna i wyścig sieci.
Pamięć podręczna i wyścig w sieci.

Idealny w przypadku: małych zasobów, którym zależy na wydajności na urządzeniach z powolnym dostępem do dysku.

W przypadku niektórych kombinacji starszych dysków twardych, skanerów wirusów i szybszego połączenia z internetem pobieranie zasobów z sieci może być szybsze niż przechowywanie zasobów na dysku. Weź jednak pod uwagę, że wchodzenie do sieci, gdy użytkownik ma treści na swoim urządzeniu, wiąże się z marnowaniem danych.

// 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)]));
});

Sieć powraca do pamięci podręcznej

Sieć powraca do pamięci podręcznej.
Sieć powraca do pamięci podręcznej.

Idealny dla: szybkich poprawek zasobów, które często się aktualizują, poza „wersją” witryny. Dotyczy to np. artykułów, awatarów, osi czasu w mediach społecznościowych i tablic wyników w grach.

Oznacza to, że udostępniasz użytkownikom online najbardziej aktualne treści, ale użytkownicy offline korzystają ze starszej wersji przechowywanej w pamięci podręcznej. Jeśli żądanie sieciowe zostanie zrealizowane, najprawdopodobniej zaktualizuj wpis w pamięci podręcznej.

Ta metoda ma jednak wady. Jeśli użytkownik ma przerywane lub powolne połączenie, musi poczekać, aż sieć ulegnie awarii, zanim będzie mógł pobrać idealnie akceptowaną treść na swoje urządzenie. Może to zająć bardzo dużo czasu i jest bardzo frustrujące dla użytkowników. Aby znaleźć lepsze rozwiązanie, zapoznaj się z następnym schematem: Pamięć podręczna, a następnie sieć.

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

Pamięć podręczna, a następnie sieć

Pamięć podręczna, a następnie sieć.
Pamięć podręczna, a następnie sieć.

Idealny w przypadku: treści, które często się aktualizują. Dotyczy to np. artykułów, osi czasu w mediach społecznościowych, gier, tabel wyników.

W takiej sytuacji strona musi wysłać dwa żądania – jedno do pamięci podręcznej, a drugie do sieci. Chodzi o to, aby najpierw wyświetlać dane z pamięci podręcznej, a następnie aktualizować stronę, gdy napływają dane sieciowe.

Czasami możesz po prostu zastąpić bieżące dane po pojawieniu się nowych danych (np. tabeli wyników w grze), ale w przypadku większych treści może to być uciążliwe. Zasadniczo nie „znikaj” z informacji, które użytkownik czyta lub z którymi wchodzi w interakcję.

Twitter dodaje nowe treści nad starymi treściami i dostosowuje pozycję przewijania, by użytkownik nie miał przerw w jej działaniu. Jest to możliwe, ponieważ Twitter zazwyczaj zachowuje liniowy porządek treści. Skopiowałem ten wzorzec tak, aby materiały zapewniały satysfakcję, aby jak najszybciej pojawiały się na ekranie. Bieżące treści wyświetlają się zaraz po ich udostępnieniu.

Kod na stronie:

var networkDataReceived = false;

startSpinner();

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

// 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);

Kod w skrypcie service worker:

Zawsze powinno się przejść do sieci i na bieżąco aktualizować pamięć podręczną.

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;
      });
    }),
  );
});

W ramach trenowania do thrill udało mi się obejść ten problem, używając XHR zamiast pobierania i nadużywając nagłówka Accept, aby wskazać skryptowi Service Worker, skąd ma pobrać wynik (kod strony, kod skryptu service worker).

Ogólna kreacja zastępcza

Ogólna wartość zastępcza.
Ogólna wartość zastępcza.

Jeśli nie uda Ci się udostępnić czegoś z pamięci podręcznej lub sieci, możesz podać ogólną metodę zastępczą.

Idealny w przypadku: obrazów dodatkowych, takich jak awatary, nieudane żądania POST i strony „Niedostępne w trybie 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 and headers.
        // Eg, a fallback silhouette image for avatars.
      }),
  );
});

Element, którego użyjesz w zastępstwie, może być zależnością instalacji.

Jeśli Twoja strona publikuje e-maila, skrypt service worker może wrócić do przechowywania go w skrzynce nadawczej IndexedDB i odpowiedzieć, informując stronę o niepowodzeniu wysyłania, ale dane zostały zachowane.

Szablony po stronie skryptu service worker

Szablony po stronie elementu ServiceWorker.
Szablony po stronie elementu ServiceWorker.

Idealny w przypadku: stron, na których nie można przechowywać odpowiedzi serwera w pamięci podręcznej.

Renderowanie stron na serwerze przyspiesza działanie, ale może to oznaczać uwzględnianie w pamięci podręcznej danych o stanie, które mogą nie mieć sensu, np. „Zalogowano jako...”. Jeśli stroną steruje skrypt service worker, możesz zamiast tego zażądać danych JSON wraz z szablonem i je wyrenderować.

importScripts('templating-engine.js');

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

  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',
        },
      });
    }),
  );
});

Jak to wszystko połączyć

Nie musisz ograniczać się do żadnej z tych metod. Prawdopodobnie użyjesz wielu z nich w zależności od adresu URL żądania. Na przykład w przypadku elementu trenowanego do zaciekawiania stosuje się:

Zapoznaj się z prośbą i zdecyduj, co zrobić:

self.addEventListener('fetch', function (event) {
  // Parse the URL:
  var 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 (/\.webp$/.test(requestURL.pathname)) {
      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(
    caches.match(event.request).then(function (response) {
      return response || fetch(event.request);
    }),
  );
});

...zdobędziesz obraz.

Środki

...dla uroczych ikon:

Dziękuję też Jeffowi Posnicku za wychwycenie wielu błędów, zanim kliknę „Opublikuj”.

Więcej informacji