Dzięki Service Workers udostępniliśmy programistom sposób na rozwiązanie problemu z połączeniem sieciowym. Masz kontrolę nad buforowaniem i sposobem obsługi żądań. Oznacza to, że możesz tworzyć własne wzory. Przyjrzyj się kilku możliwym wzorcom w izolacji, ale w praktyce prawdopodobnie będziesz ich używać jednocześnie, w zależności od adresu URL i kontekstu.
Przykłady niektórych z tych wzorców znajdziesz w Trained-to-thrill.
Kiedy przechowywać zasoby
Skrypty service worker umożliwiają obsługę żądań niezależnie od buforowania, dlatego pokażę je osobno. Najpierw ustal, kiedy warto używać pamięci podręcznej.
Podczas instalacji jako zależność
Interfejs Service Worker API udostępnia zdarzenie install. Możesz go użyć, aby przygotować elementy, które muszą być gotowe przed obsługą innych zdarzeń. Podczas install poprzednie wersje skryptu service worker działają i obsługują strony. Wszelkie działania podejmowane w tym czasie nie powinny zakłócać działania dotychczasowego service workera.
Idealne rozwiązanie w przypadku: plików CSS, obrazów, czcionek, JS, szablonów i wszystkiego innego, co w danej wersji witryny uważasz za statyczne.
Pobieraj elementy, które w przypadku niepowodzenia pobierania spowodują, że witryna przestanie działać, oraz elementy, które odpowiednia aplikacja na konkretną platformę uwzględniłaby w początkowym 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 przyjmuje obietnicę określenia długości i skuteczności instalacji. Jeśli obietnica zostanie odrzucona, instalacja zostanie uznana za nieudaną, a ten skrypt service worker zostanie porzucony (jeśli działa starsza wersja, pozostanie ona nienaruszona). caches.open() i cache.addAll() obietnice zwrotu.
Jeśli nie uda się pobrać żadnego z zasobów, wywołanie cache.addAll() zostanie odrzucone.
Na stronie trained-to-thrill używam tego do buforowania statycznych komponentów.
Podczas instalacji, a nie jako zależność
Jest to podobne do instalowania jako zależności, ale nie opóźnia zakończenia instalacji i nie powoduje jej niepowodzenia w przypadku niepowodzenia buforowania.
Idealne rozwiązanie w przypadku: większych zasobów, które nie są potrzebne od razu, np. elementów do późniejszych poziomów 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
();
}),
);
});
Ten przykład nie przekazuje z powrotem do cache.addAll obietnicy dotyczącej poziomów 11–20, więc nawet jeśli się nie powiedzie, gra będzie nadal dostępna offline.event.waitUntil Oczywiście musisz uwzględnić możliwość braku tych poziomów i ponownie spróbować je zapisać w pamięci podręcznej, jeśli ich nie ma.
Proces service worker może zostać zakończony podczas pobierania poziomów 11–20, ponieważ zakończył obsługę zdarzeń, co oznacza, że nie zostaną one zapisane w pamięci podręcznej. Web Periodic Background Synchronization API może obsługiwać takie przypadki, a także większe pobierania, np. filmów.
Po aktywacji
Idealny do: czyszczenia i migracji.
Gdy nowy skrypt service worker zostanie zainstalowany, a poprzednia wersja nie będzie używana, nowy skrypt zostanie aktywowany i otrzymasz zdarzenie activate. Ponieważ poprzednia wersja nie jest już potrzebna, to dobry moment na przeprowadzenie migracji schematu w IndexedDB i usunięcie nieużywanych pamięci podręcznych.
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 zdarzenia takie jak fetch są umieszczane w kolejce, więc długotrwała aktywacja może blokować wczytywanie stron. Zadbaj o to, aby aktywacja była jak najprostsza, i używaj jej tylko do czynności, których nie można było wykonać, gdy aktywna była poprzednia wersja.
Na stronie trained-to-thrill używam tego do usuwania starych pamięci podręcznych.
Przy interakcji użytkownika
Idealne rozwiązanie: gdy nie można udostępnić całej witryny w trybie offline i zdecydowano, że użytkownik będzie mógł wybrać treści, które mają być dostępne offline. np. film w YouTube, artykuł w Wikipedii lub konkretna galeria na Flickrze.
Udostępnij użytkownikowi przycisk „Przeczytaj później” lub „Zapisz do odczytu offline”. Gdy użytkownik kliknie link, pobierz potrzebne informacje z sieci i zapisz je 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 Cache API jest dostępny na stronach i w skryptach service worker, co oznacza, że możesz dodawać elementy do pamięci podręcznej bezpośrednio ze strony.
Odpowiedź sieci
Idealne rozwiązanie w przypadku: częstego aktualizowania zasobów, takich jak skrzynka odbiorcza użytkownika lub zawartość artykułu. Przydatne również w przypadku treści nieistotnych, takich jak awatary, ale należy zachować ostrożność.
Jeśli żądanie nie pasuje do niczego w pamięci podręcznej, pobierz je z sieci, wyślij na stronę i jednocześnie dodaj do pamięci podręcznej.
Jeśli zrobisz to w przypadku zakresu adresów URL, np. awatarów, musisz uważać, aby nie przepełnić miejsca na dane w Twojej domenie. Jeśli użytkownik musi odzyskać miejsce na dysku, nie chcesz być głównym kandydatem. Usuń z pamięci podręcznej 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 zapewnić efektywne wykorzystanie pamięci, możesz odczytać treść odpowiedzi lub żądania tylko raz. W przykładzie kodu użyto .clone() do utworzenia dodatkowych kopii, które można odczytywać oddzielnie.
Na blogu trained-to-thrill używam tej funkcji do buforowania obrazów z Flickr.
Stale-while-revalidate
Idealne rozwiązanie w przypadku: często aktualizowanych zasobów, w których przypadku posiadanie najnowszej wersji nie jest niezbędne. Do tej kategorii mogą należeć awatary.
Jeśli dostępna jest wersja w pamięci podręcznej, użyj jej, ale pobierz aktualizację na następny raz.
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;
});
}),
);
});
Działa to bardzo podobnie do stale-while-revalidate w HTTP.
W wiadomości push
Push API to kolejna funkcja oparta na mechanizmie Service Worker. Dzięki temu usługa Service Worker może zostać wybudzona w odpowiedzi na wiadomość z usługi przesyłania wiadomości systemu operacyjnego. Dzieje się tak nawet wtedy, gdy użytkownik nie ma otwartej karty z Twoją witryną. Tylko proces roboczy usługi jest wybudzany. Prośbę o pozwolenie na to możesz przesłać ze strony, a użytkownik zobaczy odpowiedni komunikat.
Idealne do: treści związanych z powiadomieniem, np. wiadomości na czacie, najnowszych informacji lub e-maila. Rzadko zmieniające się treści, które korzystają z natychmiastowej synchronizacji, np. aktualizacja listy zadań do wykonania lub zmiana w kalendarzu.
Typowym efektem końcowym jest powiadomienie, które po kliknięciu otwiera i wyświetla odpowiednią stronę. W tym przypadku bardzo ważne jest wcześniejsze zaktualizowanie pamięci podręcznej. Użytkownik jest online w momencie otrzymania wiadomości push, ale może nie być online, gdy wchodzi w interakcję z powiadomieniem. Dlatego ważne jest, aby udostępniać te treści w trybie 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 to kolejna funkcja oparta na instancji roboczej usługi. Umożliwia jednorazowe lub okresowe (z użyciem bardzo heurystycznego interwału) wysyłanie żądań synchronizacji danych w tle. Dzieje się tak nawet wtedy, gdy użytkownik nie ma otwartej karty z Twoją witryną. Aktywowany jest tylko skrypt service worker. Prośbę o zezwolenie możesz wysłać ze strony, a użytkownik zobaczy odpowiedni komunikat.
Idealne rozwiązanie w przypadku: niepilnych aktualizacji, zwłaszcza tych, które pojawiają się tak często, że wysyłanie powiadomień push po każdej z nich byłoby zbyt uciążliwe dla użytkowników, np. w przypadku osi czasu w mediach społecznościowych lub artykułów prasowych.
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
Pochodzenie otrzymuje określoną ilość wolnego miejsca, które może wykorzystać w dowolny sposób. To bezpłatne miejsce jest współdzielone przez wszystkie źródła pamięci: pamięć(lokalną), IndexedDB, dostęp do systemu plików i oczywiście pamięć podręczną.
Kwota, którą otrzymasz, nie jest określona. Zależy to od urządzenia i warunków przechowywania. Możesz sprawdzić, ile masz środków, za pomocą tych metod:
if (navigator.storage && navigator.storage.estimate) {
const quota = await navigator.storage.estimate();
// quota.usage -> Number of bytes used.
// quota.quota -> Maximum number of bytes available.
const percentageUsed = (quota.usage / quota.quota) * 100;
console.log(`You've used ${percentageUsed}% of the available storage.`);
const remaining = quota.quota - quota.usage;
console.log(`You can write up to ${remaining} more bytes.`);
}
Jednak podobnie jak w przypadku wszystkich pamięci przeglądarki, przeglądarka może usunąć Twoje dane, jeśli na urządzeniu zabraknie miejsca. Niestety przeglądarka nie jest w stanie odróżnić filmów, które chcesz zachować za wszelką cenę, od gry, na której Ci nie zależy.
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 wyrazić na to zgodę. W tym celu użyj interfejsu Permissions API.
Zaangażowanie użytkownika w ten proces jest ważne, ponieważ dzięki temu może on kontrolować usuwanie danych. Jeśli na urządzeniu zaczyna brakować miejsca, a wyczyszczenie nieistotnych danych nie rozwiązuje problemu, użytkownik może zdecydować, które elementy zachować, a które usunąć.
Aby to działało, systemy operacyjne muszą traktować „trwałe” źródła jako równoważne aplikacjom na konkretne platformy w podziale wykorzystania pamięci, zamiast raportować przeglądarkę jako pojedynczy element.
Wyświetlanie sugestii
Bez względu na to, ile pamięci podręcznej używasz, service worker korzysta z niej tylko wtedy, gdy powiesz mu, kiedy i jak ma to robić. Oto kilka wzorców obsługi żądań:
Tylko pamięć podręczna
Idealne rozwiązanie w przypadku: wszystkiego, co uważasz za statyczne w danej „wersji” witryny. Powinny być one zapisane w zdarzeniu instalacji, więc możesz mieć pewność, że tam będą.
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ż nie musisz często obsługiwać tego przypadku, obejmuje go pamięć podręczna z powrotem do sieci.
Tylko sieć
Idealne w przypadku: elementów, które nie mają odpowiednika offline, np. pingów analitycznych czy żądań innych niż GET.
self.addEventListener('fetch', function (event) {
event.respondWith(fetch(event.request));
// or don't call event.respondWith, which
// will result in default browser behavior
});
…chociaż nie musisz często obsługiwać tego przypadku, obejmuje go pamięć podręczna z powrotem do sieci.
Pamięć podręczna, powrót do sieci
Idealne rozwiązanie do: tworzenia aplikacji działających w trybie offline. W takich przypadkach w większości przypadków będziesz postępować w ten sposób. Inne wzorce są wyjątkami opartymi na żądaniu przychodzącym.
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
return response || fetch(event.request);
}),
);
});
Dzięki temu w przypadku elementów w pamięci podręcznej uzyskasz zachowanie „tylko pamięć podręczna”, a w przypadku elementów nieznajdujących się w pamięci podręcznej – zachowanie „tylko sieć” (dotyczy to wszystkich żądań innych niż GET, ponieważ nie można ich przechowywać w pamięci podręcznej).
Wyścig pamięci podręcznej i sieci
Idealne w przypadku: małych zasobów, w których zależy Ci na wydajności na urządzeniach z wolnym dostępem do dysku.
W przypadku niektórych kombinacji starszych dysków twardych, skanerów antywirusowych i szybszych połączeń internetowych pobieranie zasobów z sieci może być szybsze niż uzyskiwanie dostępu do dysku. Jednak w sytuacji, gdy użytkownik ma treści na urządzeniu, korzystanie z sieci może być marnotrawstwem danych, więc pamiętaj o tym.
// Promise.race rejects when a promise rejects before fulfilling.
// To make a 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ć wraca do pamięci podręcznej
Idealne rozwiązanie w przypadku: zasobów, które są często aktualizowane poza „wersją” witryny. np. artykuły, awatary, osie czasu w mediach społecznościowych i tabele wyników w grach.
Oznacza to, że użytkownicy online otrzymują najbardziej aktualne treści, a użytkownicy offline – starszą wersję z pamięci podręcznej. Jeśli żądanie sieciowe zakończy się powodzeniem, prawdopodobnie zechcesz zaktualizować wpis w pamięci podręcznej.
Ta metoda ma jednak wady. Jeśli użytkownik ma niestabilne lub wolne połączenie, musi poczekać, aż sieć przestanie działać, zanim otrzyma w pełni akceptowalne treści, które są już na jego urządzeniu. Może to zająć bardzo dużo czasu i być frustrujące dla użytkowników. Lepsze rozwiązanie znajdziesz w następnym wzorcu, Pamięć podręczna, a potem sieć.
self.addEventListener('fetch', function (event) {
event.respondWith(
fetch(event.request).catch(function () {
return caches.match(event.request);
}),
);
});
Pamięć podręczna, a potem sieć
Idealny do: treści, które są często aktualizowane. np. artykuły, osie czasu w mediach społecznościowych i gry. tabele wyników.
Wymaga to wysłania przez stronę 2 żądań: jednego do pamięci podręcznej, a drugiego do sieci. Chodzi o to, aby najpierw wyświetlić dane z pamięci podręcznej, a potem zaktualizować stronę, gdy dotrą dane z sieci.
Czasami możesz po prostu zastąpić bieżące dane, gdy pojawią się nowe (np. w tabeli wyników gry), ale w przypadku większych treści może to być uciążliwe. Nie ukrywaj elementów, które użytkownik może czytać lub z którymi może wchodzić w interakcję.
Twitter dodaje nowe treści nad starymi i dostosowuje pozycję przewijania, aby nie przerywać użytkownikowi przeglądania. Jest to możliwe, ponieważ Twitter zachowuje w przypadku treści w większości liniową kolejność. Skopiowałem ten wzorzec dla trained-to-thrill, aby jak najszybciej wyświetlać treści na ekranie, a jednocześnie pokazywać aktualne treści od razu po ich otrzymaniu.
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 usłudze Service Worker:
Zawsze przechodź do sieci i aktualizuj pamięć podręczną na bieżąco.
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 trained-to-thrill poradziłem sobie z tym problemem, używając XHR zamiast fetch i wykorzystując nagłówek Accept, aby poinformować skrypt service worker, skąd ma pobrać wynik (kod strony, kod skryptu service worker).
Ogólny przebieg
Jeśli nie uda Ci się wyświetlić czegoś z pamięci podręcznej lub sieci, udostępnij ogólną wersję zastępczą.
Idealne do: obrazów dodatkowych, takich jak awatary, nieudane żądania POST i strona „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, do którego następuje powrót, jest prawdopodobnie zależnością instalacyjną.
Jeśli strona wysyła e-maila, service worker może zapisać go w skrzynce wychodzącej IndexedDB i odpowiedzieć, że wysyłanie się nie powiodło, ale dane zostały zachowane.
Tworzenie szablonów po stronie skryptu service worker
Idealne rozwiązanie w przypadku: stron, których odpowiedzi serwera nie można przechowywać w pamięci podręcznej.
Szybsze renderowanie stron na serwerze może jednak oznaczać uwzględnianie danych stanu, które nie mają sensu w pamięci podręcznej, np. stanu zalogowania. Jeśli Twoja strona jest kontrolowana przez skrypt service worker, możesz poprosić o dane JSON wraz z szablonem i zamiast tego je renderować.
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',
},
});
}),
);
});
Łączenie elementów
Nie musisz ograniczać się do jednej z tych metod. W zależności od adresu URL żądania prawdopodobnie użyjesz wielu z nich. Na przykład trained-to-thrill używa:
- Pamięć podręczna podczas instalacji w przypadku statycznego interfejsu i zachowania.
- Cache on network response (pamięć podręczna w odpowiedzi sieci) w przypadku obrazów i danych z Flickr.
- Pobieranie z pamięci podręcznej z przełączeniem na sieć w przypadku większości żądań
- Pobieranie z pamięci podręcznej, a potem z sieci w przypadku wyników wyszukiwania w Flickr
Wystarczy, że sprawdzisz prośbę i zdecydujesz, 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);
}),
);
});
Więcej informacji
- Service worker i interfejs Cache Storage API
- JavaScript Promises—an Introduction: przewodnik po obietnicach
Środki
W przypadku pięknych ikon:
- Code (Kod) od buzzyrobot
- Kalendarz autorstwa Scotta Lewisa
- Sieć Ben Rizzo
- SD, Thomas Le Bas
- CPU od iconsmind.com
- Trash autorstwa trasnik
- Powiadomienie od @daosme
- Layout od Mister Pixel
- Cloud P.J. Onori
Dziękuję też Jeffowi Posnickowi za wyłapanie wielu błędów, zanim kliknąłem „Opublikuj”.