Mit Service Workern haben wir Entwicklern eine Möglichkeit gegeben, Netzwerkverbindungen zu optimieren. Sie haben die Kontrolle über das Caching und die Verarbeitung von Anfragen. Das bedeutet, dass Sie Ihre eigenen Muster erstellen können. Sehen Sie sich einige mögliche Muster isoliert an. In der Praxis werden Sie sie jedoch wahrscheinlich je nach URL und Kontext gemeinsam verwenden.
Eine funktionierende Demo einiger dieser Muster finden Sie unter Trained-to-thrill.
Wann sollten Ressourcen gespeichert werden?
Mit Service Workern können Sie Anfragen unabhängig vom Caching verarbeiten. Daher zeige ich sie separat. Zuerst müssen Sie festlegen, wann Sie den Cache verwenden sollten.
Bei der Installation als Abhängigkeit
Die Service Worker API stellt Ihnen ein install-Ereignis zur Verfügung. Damit können Sie Dinge vorbereiten, die vor der Verarbeitung anderer Ereignisse bereit sein müssen. Während install werden weiterhin frühere Versionen Ihres Service Workers ausgeführt und Seiten bereitgestellt. Was auch immer Sie zu diesem Zeitpunkt tun, sollte den vorhandenen Service Worker nicht stören.
Ideal für: CSS, Bilder, Schriftarten, JS, Vorlagen oder alles andere, was Sie für diese Version Ihrer Website als statisch betrachten.
Rufen Sie die Elemente ab, die Ihre Website vollständig funktionsunfähig machen würden, wenn sie nicht abgerufen werden könnten. Das sind Elemente, die in einer entsprechenden plattformspezifischen App Teil des ersten Downloads wären.
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 verwendet ein Promise, um die Dauer und den Erfolg der Installation zu definieren. Wenn das Promise abgelehnt wird, gilt die Installation als fehlgeschlagen und dieser Service Worker wird verworfen (wenn eine ältere Version ausgeführt wird, bleibt sie intakt). caches.open() und cache.addAll() geben Versprechen zurück.
Wenn das Abrufen einer der Ressourcen fehlschlägt, wird der cache.addAll()-Aufruf abgelehnt.
Auf trained-to-thrill verwende ich das, um statische Assets zu cachen.
Bei der Installation, nicht als Abhängigkeit
Dies ähnelt der Installation als Abhängigkeit, verzögert aber nicht den Abschluss der Installation und führt nicht dazu, dass die Installation fehlschlägt, wenn das Caching fehlschlägt.
Ideal für: Größere Ressourcen, die nicht sofort benötigt werden, z. B. Assets für spätere Level eines Spiels.
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
();
}),
);
});
In diesem Beispiel wird das cache.addAll-Versprechen für die Level 11 bis 20 nicht an event.waitUntil zurückgegeben. Selbst wenn es also fehlschlägt, ist das Spiel weiterhin offline verfügbar. Natürlich müssen Sie die mögliche Abwesenheit dieser Ebenen berücksichtigen und versuchen, sie noch einmal zu cachen, wenn sie fehlen.
Der Service Worker wird möglicherweise beendet, während die Ebenen 11 bis 20 heruntergeladen werden, da er die Verarbeitung von Ereignissen abgeschlossen hat. Das bedeutet, dass sie nicht im Cache gespeichert werden. Die Web Periodic Background Synchronization API kann solche Fälle und größere Downloads wie Filme verarbeiten.
Bei Aktivierung
Ideal für: Bereinigung und Migration.
Sobald ein neuer Service Worker installiert wurde und keine vorherige Version verwendet wird, wird der neue Service Worker aktiviert und Sie erhalten ein activate-Ereignis. Da die vorherige Version nicht mehr im Weg ist, ist jetzt ein guter Zeitpunkt, um Schemamigrationen in IndexedDB zu verarbeiten und nicht verwendete Caches zu löschen.
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);
}),
);
}),
);
});
Während der Aktivierung werden Ereignisse wie fetch in eine Warteschlange gestellt. Eine lange Aktivierung kann daher das Laden von Seiten blockieren. Halten Sie die Aktivierung so schlank wie möglich und verwenden Sie sie nur für Dinge, die Sie nicht tun konnten, als die vorherige Version aktiv war.
Auf trained-to-thrill verwende ich das, um alte Caches zu entfernen.
Bei Nutzerinteraktion
Ideal für: Wenn die gesamte Website nicht offline verfügbar gemacht werden kann und Sie dem Nutzer die Möglichkeit geben möchten, die Inhalte auszuwählen, die er offline verfügbar machen möchte. z.B. ein Video auf YouTube, ein Artikel auf Wikipedia oder eine bestimmte Galerie auf Flickr.
Bieten Sie dem Nutzer eine Schaltfläche „Später lesen“ oder „Offline speichern“ an. Wenn darauf geklickt wird, werden die benötigten Daten aus dem Netzwerk abgerufen und im Cache gespeichert.
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);
});
});
});
Die Cache API ist auf Seiten und in Service-Workern verfügbar. Das bedeutet, dass Sie den Cache direkt über die Seite erweitern können.
Netzwerkantwort
Ideal für: häufige Aktualisierungen von Ressourcen wie dem Posteingang eines Nutzers oder Artikelinhalten. Auch für nicht essenzielle Inhalte wie Avatare nützlich, aber Vorsicht ist geboten.
Wenn eine Anfrage nicht mit dem Cache übereinstimmt, wird sie aus dem Netzwerk abgerufen, an die Seite gesendet und gleichzeitig dem Cache hinzugefügt.
Wenn Sie dies für eine Reihe von URLs tun, z. B. für Avatare, müssen Sie darauf achten, dass der Speicher Ihres Ursprungs nicht überlastet wird. Wenn der Nutzer Speicherplatz freigeben muss, sollten Sie nicht der Hauptkandidat sein. Entfernen Sie Elemente aus dem Cache, die Sie nicht mehr benötigen.
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;
})
);
});
}),
);
});
Um eine effiziente Speichernutzung zu ermöglichen, können Sie den Body einer Antwort/Anfrage nur einmal lesen. Im Codebeispiel wird .clone() verwendet, um zusätzliche Kopien zu erstellen, die separat gelesen werden können.
Auf trained-to-thrill verwende ich das, um Flickr-Bilder zu cachen.
„Stale-while-revalidate“
Ideal für: Ressourcen, die häufig aktualisiert werden und bei denen die allerneueste Version nicht unbedingt erforderlich ist. Avatare können in diese Kategorie fallen.
Wenn eine Version im Cache verfügbar ist, verwenden Sie diese, rufen Sie aber ein Update für das nächste Mal ab.
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;
});
}),
);
});
Das ist dem stale-while-revalidate von HTTP sehr ähnlich.
Bei Push-Benachrichtigung
Die Push API ist eine weitere Funktion, die auf Service Workern basiert. Dadurch kann der Service Worker als Reaktion auf eine Nachricht vom Messaging-Dienst des Betriebssystems aktiviert werden. Das passiert auch, wenn der Nutzer keinen Tab mit Ihrer Website geöffnet hat. Nur der Service Worker wird aktiviert. Sie fordern die Berechtigung auf einer Seite an und der Nutzer wird aufgefordert.
Ideal für: Inhalte, die sich auf eine Benachrichtigung beziehen, z. B. eine Chatnachricht, eine E‑Mail oder eine Eilmeldung. Auch Inhalte, die sich selten ändern, aber von einer sofortigen Synchronisierung profitieren, z. B. eine Aktualisierung einer Aufgabenliste oder eine Kalenderänderung.
Das übliche Endergebnis ist eine Benachrichtigung, die beim Tippen eine relevante Seite öffnet und fokussiert. Hierfür ist das vorherige Aktualisieren von Caches extrem wichtig. Der Nutzer ist zum Zeitpunkt des Empfangs der Push-Benachrichtigung online, möglicherweise aber nicht, wenn er schließlich mit der Benachrichtigung interagiert. Daher ist es wichtig, dass diese Inhalte offline verfügbar sind.
Mit diesem Code werden Caches aktualisiert, bevor eine Benachrichtigung angezeigt wird:
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/');
}
});
Bei der Hintergrundsynchronisierung
Synchronisierung im Hintergrund ist eine weitere Funktion, die auf Service Workern basiert. Damit können Sie die Synchronisierung von Hintergrunddaten einmalig oder in einem (extrem heuristischen) Intervall anfordern. Das passiert auch, wenn der Nutzer keinen Tab mit Ihrer Website geöffnet hat. Nur der Service Worker wird aktiviert. Sie fordern die Berechtigung dazu auf einer Seite an und der Nutzer wird aufgefordert, sie zu erteilen.
Ideal für: nicht dringende Updates, insbesondere solche, die so regelmäßig erfolgen, dass eine Push-Benachrichtigung pro Update für Nutzer zu häufig wäre, z. B. soziale Timelines oder Nachrichtenartikel.
self.addEventListener('sync', function (event) {
if (event.id == 'update-leaderboard') {
event.waitUntil(
caches.open('mygame-dynamic').then(function (cache) {
return cache.add('/leaderboard.json');
}),
);
}
});
Cache-Persistenz
Ihrem Ursprung wird eine bestimmte Menge an freiem Speicherplatz zugewiesen, die er nach Belieben nutzen kann. Dieser kostenlose Speicherplatz wird von allen Ursprungsspeichern gemeinsam genutzt: (lokaler) Speicher, IndexedDB, File System Access und natürlich Caches.
Der Betrag, den Sie erhalten, ist nicht festgelegt. Die Dauer hängt vom Gerät und den Lagerbedingungen ab. Sie können mit dem folgenden Befehl herausfinden, wie viel Sie haben:
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.`);
}
Wie bei allen Browserspeichern kann der Browser Ihre Daten jedoch löschen, wenn der Speicher des Geräts knapp wird. Leider kann der Browser nicht zwischen den Filmen, die du unbedingt behalten möchtest, und dem Spiel, das dir nicht so wichtig ist, unterscheiden.
Um dieses Problem zu umgehen, verwenden Sie die StorageManager-Schnittstelle:
// 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.
});
Der Nutzer muss natürlich die Berechtigung erteilen. Verwenden Sie dazu die Permissions API.
Es ist wichtig, dass der Nutzer Teil dieses Ablaufs ist, da er nun die Kontrolle über das Löschen hat. Wenn auf dem Gerät des Nutzers der Speicherplatz knapp wird und das Löschen nicht benötigter Daten das Problem nicht behebt, kann der Nutzer selbst entscheiden, welche Elemente er behalten und welche er entfernen möchte.
Dazu müssen Betriebssysteme „dauerhafte“ Ursprünge in ihren Aufschlüsselungen der Speichernutzung als gleichwertig mit plattformspezifischen Apps behandeln, anstatt den Browser als einzelnes Element zu melden.
Vorschläge bereitstellen
Es spielt keine Rolle, wie viel Caching Sie durchführen. Der Service Worker verwendet den Cache nur, wenn Sie ihm sagen, wann und wie. Hier sind einige Muster für die Verarbeitung von Anfragen:
Nur Cache
Ideal für: alles, was Sie für eine bestimmte „Version“ Ihrer Website als statisch betrachten. Sie sollten diese im Installationsereignis zwischengespeichert haben, damit Sie sich darauf verlassen können, dass sie vorhanden sind.
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));
});
…obwohl Sie diesen Fall nicht oft speziell behandeln müssen, da er durch Cache, Fallback auf Netzwerk abgedeckt wird.
Nur Netzwerk
Ideal für: Dinge, die kein Offline-Äquivalent haben, z. B. Analyse-Pings, Nicht-GET-Anfragen.
self.addEventListener('fetch', function (event) {
event.respondWith(fetch(event.request));
// or don't call event.respondWith, which
// will result in default browser behavior
});
…obwohl Sie diesen Fall nicht oft speziell behandeln müssen, da er durch Cache, Fallback auf Netzwerk abgedeckt wird.
Cache, Fallback auf Netzwerk
Ideal für: Offline-First-Anwendungen. In solchen Fällen gehen Sie bei den meisten Anfragen so vor. Andere Muster sind Ausnahmen, die auf der eingehenden Anfrage basieren.
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
return response || fetch(event.request);
}),
);
});
So erhalten Sie das Verhalten „Nur Cache“ für Elemente im Cache und das Verhalten „Nur Netzwerk“ für alle nicht im Cache befindlichen Elemente (einschließlich aller Nicht-GET-Anfragen, da sie nicht im Cache gespeichert werden können).
Cache- und Netzwerk-Race
Ideal für: kleine Assets, bei denen Sie die Leistung auf Geräten mit langsamem Festplattenzugriff optimieren möchten.
Bei einigen Kombinationen aus älteren Festplatten, Virenscannern und schnelleren Internetverbindungen kann es schneller sein, Ressourcen aus dem Netzwerk abzurufen, als auf die Festplatte zuzugreifen. Wenn der Nutzer die Inhalte jedoch auf seinem Gerät hat, kann der Zugriff auf das Netzwerk eine Verschwendung von Daten sein.
// 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)]));
});
Netzwerk greift auf Cache zurück
Ideal für: schnelle Korrekturen für Ressourcen, die häufig außerhalb der „Version“ der Website aktualisiert werden. z.B. Artikel, Avatare, Timelines in sozialen Medien und Bestenlisten in Spielen.
Das bedeutet, dass Online-Nutzer die aktuellsten Inhalte erhalten, während Offline-Nutzer eine ältere, im Cache gespeicherte Version sehen. Wenn die Netzwerkanfrage erfolgreich ist, möchten Sie den Cacheeintrag wahrscheinlich aktualisieren.
Diese Methode hat jedoch Schwächen. Wenn der Nutzer eine instabile oder langsame Verbindung hat, muss er warten, bis das Netzwerk ausfällt, bevor er die bereits auf seinem Gerät vorhandenen Inhalte abrufen kann. Das kann sehr lange dauern und ist für Nutzer frustrierend. Eine bessere Lösung finden Sie im nächsten Muster Cache then network.
self.addEventListener('fetch', function (event) {
event.respondWith(
fetch(event.request).catch(function () {
return caches.match(event.request);
}),
);
});
Cache und dann Netzwerk
Ideal für: Inhalte, die häufig aktualisiert werden. z. B. Artikel, Social-Media-Timelines und Spiele-Bestenlisten.
Dazu muss die Seite zwei Anfragen stellen: eine an den Cache und eine an das Netzwerk. Die Idee ist, zuerst die Daten im Cache zu laden und die Seite dann zu aktualisieren, wenn die Netzwerkdaten eintreffen.
Manchmal können Sie die aktuellen Daten einfach ersetzen, wenn neue Daten eingehen, z. B. bei einer Spielebestenliste. Bei größeren Inhalten kann das jedoch störend sein. Grundsätzlich sollten Sie nichts „verschwinden“ lassen, was der Nutzer gerade liest oder mit dem er interagiert.
Twitter fügt die neuen Inhalte über den alten Inhalten ein und passt die Scrollposition so an, dass der Nutzer nicht unterbrochen wird. Das ist möglich, weil Twitter Inhalte in einer weitgehend linearen Reihenfolge anzeigt. Ich habe dieses Muster für trained-to-thrill kopiert, um Inhalte so schnell wie möglich auf dem Bildschirm anzuzeigen und gleichzeitig aktuelle Inhalte zu präsentieren, sobald sie verfügbar sind.
Code auf der Seite:
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);
Code im Service Worker:
Sie sollten immer das Netzwerk aufrufen und den Cache nach und nach aktualisieren.
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;
});
}),
);
});
In trained-to-thrill habe ich dieses Problem umgangen, indem ich XHR anstelle von „fetch“ verwendet und den Accept-Header missbraucht habe, um dem Service Worker mitzuteilen, wo er das Ergebnis abrufen soll (Seitencode, Service Worker-Code).
Generischer Fallback
Wenn Sie nichts aus dem Cache oder Netzwerk bereitstellen können, stellen Sie einen generischen Fallback bereit.
Ideal für: Sekundäre Bilder wie Avatare, fehlgeschlagene POST-Anfragen und eine Seite „Offline nicht verfügbar“.
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.
}),
);
});
Das Element, auf das Sie zurückgreifen, ist wahrscheinlich eine Installationsabhängigkeit.
Wenn auf Ihrer Seite eine E‑Mail gepostet wird, kann Ihr Service Worker die E‑Mail in einem IndexedDB-Postausgang speichern und der Seite mitteilen, dass das Senden fehlgeschlagen ist, die Daten aber erfolgreich beibehalten wurden.
Templating auf Service Worker-Seite
Ideal für: Seiten, deren Serverantwort nicht im Cache gespeichert werden kann.
Das Rendern von Seiten auf dem Server ist schneller, kann aber dazu führen, dass Zustandsdaten einbezogen werden, die in einem Cache möglicherweise nicht sinnvoll sind, z. B. der Anmeldestatus. Wenn Ihre Seite von einem Service Worker gesteuert wird, können Sie auch JSON-Daten zusammen mit einer Vorlage anfordern und diese rendern.
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',
},
});
}),
);
});
Zusammenfassung
Sie sind nicht auf eine dieser Methoden beschränkt. Tatsächlich werden Sie je nach Anfrage-URL wahrscheinlich viele davon verwenden. trained-to-thrill verwendet beispielsweise:
- Cache on install (Bei der Installation im Cache speichern) für die statische Benutzeroberfläche und das Verhalten
- Cache on network response für die Flickr-Bilder und -Daten
- Die meisten Anfragen werden aus dem Cache abgerufen und bei Bedarf wird auf das Netzwerk zurückgegriffen.
- Aus Cache abrufen, dann aus Netzwerk für die Flickr-Suchergebnisse
Sehen Sie sich die Anfrage an und entscheiden Sie, was Sie tun möchten:
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);
}),
);
});
Weitere Informationen
- Service Worker und die Cache Storage API
- JavaScript Promises—an Introduction: Leitfaden zu Promises
Gutschriften
Für die schönen Symbole:
- Code von buzzyrobot
- Google Kalender von Scott Lewis
- Netzwerk von Ben Rizzo
- SD von Thomas Le Bas
- CPU von iconsmind.com
- Trash von trasnik
- Benachrichtigung von @daosme
- Layout von Mister Pixel
- Cloud von P.J. Onori
Vielen Dank an Jeff Posnick, der viele Fehler entdeckt hat, bevor ich auf „Veröffentlichen“ geklickt habe.