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ść
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ść
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
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
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ą
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
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
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 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
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ć
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
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
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
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ć
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
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
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ę:
- buforowanie pamięci podręcznej po instalacji dla statycznego interfejsu użytkownika i działania.
- Buforuj w odpowiedzi sieci – w przypadku obrazów i danych z serwisu Flickr.
- pobieranie z pamięci podręcznej, wracanie do sieci, w przypadku większości żądań
- pobieraj z pamięci podręcznej, a następnie z sieci, dla wyników wyszukiwania z serwisu Flickr
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:
- Kod autorstwa buzzyrobot
- Kalendarz Scott Lewis
- Sieć wykonawcy Ben Rizzo
- SD, Thomas Le Bas
- Procesor od ikonmind.com
- Kosz na trasach
- Powiadomienie od @daosme
- Układ: Mister Pixel
- Cloud – P.J. Onori
Dziękuję też Jeffowi Posnicku za wychwycenie wielu błędów, zanim kliknę „Opublikuj”.
Więcej informacji
- Skrypty service worker — wprowadzenie
- Czy skrypt Service Worker jest gotowy? – pozwala śledzić stan implementacji w głównych przeglądarkach;
- Obietnice JavaScript – wprowadzenie – przewodnik po obietnicach