Mit dem Service Worker gaben wir den Versuch auf, Probleme offline zu lösen, und gaben Entwicklern die notwendigen Schritte, um Probleme selbst zu lösen. Es gibt Ihnen Kontrolle über das Caching und die Verarbeitung von Anfragen. Das bedeutet, dass Sie Ihre eigenen Muster erstellen können. Sehen wir uns einige mögliche Muster isoliert an, aber in der Praxis werden Sie abhängig von URL und Kontext wahrscheinlich viele von ihnen zusammen verwenden.
Eine funktionierende Demo einiger dieser Muster finden Sie unter Trained-to-thrill und in diesem Video, in dem die Auswirkungen auf die Leistung veranschaulicht werden.
Die Cache-Maschine – wann Ressourcen gespeichert werden sollen
Mit Service Worker können Sie Anfragen unabhängig vom Caching verarbeiten. Ich zeige sie Ihnen daher separat. Erster Punkt: Caching. Wann sollte es getan werden?
Bei der Installation – als Abhängigkeit
Der Service Worker sendet Ihnen ein install
-Ereignis. Damit können Sie Dinge vorbereiten, die bereits für die Verarbeitung anderer Ereignisse bereit sein müssen. Eine vorherige Version Ihres Service Workers wird weiterhin ausgeführt und Seiten werden bereitgestellt. Ihre Aktionen sollten den Vorgang also nicht beeinträchtigen.
Ideal für: CSS, Bilder, Schriftarten, JS, Vorlagen usw. im Grunde alles, was für diese "Version" Ihrer Website als statisch zu betrachten wäre.
Dies sind Dinge, die dazu führen, dass Ihre Website überhaupt nicht mehr funktioniert, wenn sie nicht abgerufen werden könnten, also Dinge, die eine äquivalente plattformspezifische App Teil des ersten Downloads sein würde.
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
legt die Dauer und den Erfolg der Installation fest. Wenn das Promise abgelehnt wird, gilt die Installation als fehlgeschlagen und dieser Service Worker wird abgebrochen. Wenn eine ältere Version ausgeführt wird, bleibt sie intakt. caches.open()
und cache.addAll()
geben Versprechungen zurück.
Wenn eine der Ressourcen nicht abgerufen werden kann, wird der cache.addAll()
-Aufruf abgelehnt.
Beim Trainieren verwende ich dies, um statische Inhalte im Cache zu speichern.
Bei der Installation – nicht als Abhängigkeit
Dieser Vorgang ist ähnlich wie oben beschrieben, verzögert jedoch 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, beispielsweise 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
();
}),
);
});
Im obigen Beispiel wird das cache.addAll
-Versprechen für die Level 11 bis 20 nicht auf event.waitUntil
übergeben. Selbst wenn es fehlschlägt, bleibt das Spiel also offline verfügbar. Natürlich müssen Sie auf das mögliche Fehlen dieser Ebenen achten und versuchen, sie im Cache zu speichern, falls sie fehlen.
Der Service Worker wird möglicherweise beendet, während die Level 11 bis 20 heruntergeladen werden, da er die Verarbeitung von Ereignissen abgeschlossen hat und nicht im Cache gespeichert wird. Zukünftig wird die Web Periodic Background Synchronization API solche Fälle und größere Downloads wie Filme verarbeiten können. Diese API wird derzeit nur in Chromium-Forks unterstützt.
Bei Aktivierung
Ideal für: Bereinigung und Migration
Sobald ein neuer Service Worker installiert wurde und eine vorherige Version nicht verwendet wird, wird der neue aktiviert und Sie erhalten ein activate
-Ereignis. Da die alte Version nicht mehr im Weg ist, empfiehlt es sich, Schemamigrationen in IndexedDB durchzuführen 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 andere Ereignisse wie fetch
in eine Warteschlange gestellt, sodass eine lange Aktivierung möglicherweise das Laden von Seiten blockieren könnte. Halten Sie Ihre Aktivierung so schlank wie möglich und verwenden Sie sie nur für Aktionen, die nicht möglich waren, während die alte Version aktiv war.
Für train-to-thrill verwende ich diese Option, um alte Caches zu entfernen.
Bei Nutzerinteraktion
Ideal für: Wenn nicht die gesamte Website offline verfügbar gemacht werden kann und Sie dem Nutzer die Möglichkeit gegeben haben, die Inhalte auszuwählen, die offline verfügbar sein sollen. z.B. ein Video auf YouTube, ein Artikel auf Wikipedia oder eine bestimmte Galerie auf Facebook.
Weisen Sie dem Nutzer eine Schaltfläche „Später lesen“ oder „Für Offlinenutzung speichern“ zu. Wenn darauf geklickt wird, rufen Sie das, was Sie brauchen, aus dem Netzwerk ab und speichern es in den Cache.
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 Caches API ist sowohl von Seiten als auch von Service Workern verfügbar. Das bedeutet, dass Sie dem Cache direkt von der Seite aus Inhalte hinzufügen können.
Bei Netzwerkantwort
Ideal geeignet für:Ressourcen wie der Posteingang eines Nutzers oder Artikelinhalte regelmäßig aktualisieren. Diese Methode eignet sich auch für nicht wesentliche Inhalte wie Avatare, aber es ist Vorsicht geboten.
Wenn eine Anfrage mit nichts im Cache übereinstimmt, rufen Sie sie aus dem Netzwerk ab, senden Sie sie an die Seite und fügen Sie sie gleichzeitig in den Cache ein.
Wenn Sie dies für eine Reihe von URLs tun, z. B. für Avatare, müssen Sie darauf achten, die Speicherung Ihres Ursprungs zu verhindern. Wenn der Nutzer Speicherplatz zurückgewinnen muss, möchten Sie nicht der Hauptkandidat sein. Entfernen Sie Elemente im 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 Arbeitsspeichernutzung zu ermöglichen, können Sie den Text einer Antwort/Anfrage nur einmal lesen. Der obige Code verwendet .clone()
, um zusätzliche Kopien zu erstellen, die separat gelesen werden können.
Für train-to-thrill verwende ich diese Option, um Flicker-Bilder im Cache zu speichern.
Veraltet bei Neuvalidierung
Ideal für:Ressourcen, bei denen die neueste Version nicht unbedingt erforderlich ist Avatare fallen in diese Kategorie.
Wenn eine im Cache gespeicherte Version verfügbar ist, verwenden Sie diese, aber rufen Sie 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;
});
}),
);
});
Dies ähnelt dem HTTP-Fehler stale-while-revalid.
Bei Push-Nachricht
Die Push API ist eine weitere Funktion, die auf dem Service Worker basiert. Dadurch kann der Service Worker als Antwort auf eine Nachricht vom Nachrichtendienst des Betriebssystems aktiviert werden. Dies geschieht auch dann, wenn der Nutzer keinen Tab auf Ihrer Website geöffnet hat. Nur der Service Worker wird geweckt. Sie fordern die Berechtigung dazu von einer Seite aus an und der Nutzer wird dazu aufgefordert.
Ideal für Inhalte mit Bezug zu einer Benachrichtigung, z. B. Chatnachrichten, Eilmeldungen oder E-Mails. Auch selten wechselnde Inhalte, die von der sofortigen Synchronisierung profitieren, z. B. eine To-do-Liste oder eine Kalenderänderung.
Das häufigste Endergebnis ist eine Benachrichtigung, die beim Antippen eine relevante Seite öffnet/hervorhebt, für die jedoch die Aktualisierung des Cache vor diesem Zeitpunkt extremely wichtig ist. Natürlich ist der Nutzer zum Zeitpunkt des Empfangs der Push-Nachricht online, aber wenn er schließlich mit der Benachrichtigung interagiert, ist er möglicherweise nicht online. Daher ist es wichtig, diesen Inhalt offline verfügbar zu machen.
Mit diesem Code wird der Cache 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 Hintergrundsynchronisierung
Die Hintergrundsynchronisierung ist eine weitere Funktion, die auf dem Service Worker basiert. Sie können die Synchronisierung von Hintergrunddaten einmalig oder in einem (sehr heuristischen) Intervall anfordern. Das passiert auch dann, wenn der Nutzer keinen Tab für deine Website geöffnet hat. Nur der Service Worker wird geweckt. Sie fordern die Berechtigung dazu von einer Seite aus an und der Nutzer wird aufgefordert.
Ideal für nicht dringende Updates, insbesondere solche, die so regelmäßig erfolgen, dass Nutzern zu häufig Push-Nachrichten gesendet werden. Dazu gehören z. B. Zeitpläne für soziale Medien 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 ein bestimmter freier Speicherplatz zur Verfügung gestellt, damit Sie das tun können, was er möchte. Dieser kostenlose Speicherplatz wird vom gesamten Ursprungsspeicher geteilt: (lokaler) Speicher, IndexedDB, Dateisystemzugriff und natürlich Caches.
Der Betrag ist nicht spezifiziert. Das hängt vom Gerät und von den Speicherbedingungen ab. So können Sie herausfinden, wie viel Sie erhalten haben:
navigator.storageQuota.queryInfo('temporary').then(function (info) {
console.log(info.quota);
// Result: <quota in bytes>
console.log(info.usage);
// Result: <used data in bytes>
});
Wie bei allen Browserspeichern kann der Browser Ihre Daten jedoch verwerfen, wenn das Gerät unter Speicherauslastung leidet. Leider kann der Browser nicht zwischen den Filmen, die Sie um jeden Preis behalten möchten, und dem Spiel unterscheiden, das Sie nicht wirklich interessiert.
Verwenden Sie die Schnittstelle StorageManager, um dieses Problem zu umgehen:
// 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.
});
Natürlich muss der Nutzer die Berechtigung erteilen. Verwenden Sie dazu die Berechtigungen API.
Es ist wichtig, den Nutzer in diesen Ablauf einzubeziehen, da er nun die Kontrolle über das Löschen hat. Wenn ihr Gerät unter Speicherauslastung steht und das Löschen nicht unbedingt erforderlicher Daten das Problem nicht löst, kann der Nutzer entscheiden, welche Elemente er behalten und entfernen möchte.
Dazu müssen Betriebssysteme „langlebige“ Ursprünge bei der Aufschlüsselung der Speichernutzung als plattformspezifische Apps behandeln, anstatt den Browser als einzelnes Element zu melden.
Vorschläge bereitstellen – Anfragen beantworten
Es spielt keine Rolle, wie viel Caching Sie durchführen, der Service Worker verwendet den Cache nur, wenn Sie ihm wann und wie mitteilen. Hier einige Muster für die Verarbeitung von Anfragen:
Nur Cache
Ideal für Inhalte, für die Sie eine bestimmte „Version“ Ihrer Website als statisch betrachten würden. Sie sollten diese beim Installationsereignis im Cache gespeichert haben, damit Sie darauf vertrauen 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 selbst bearbeiten müssen, deckt Cache, Fallback auf das Netzwerk dies ab.
Nur Netzwerk
Ideal für Elemente ohne Offline-Äquivalent wie Analyse-Pings oder Nicht-GET-Anfragen.
self.addEventListener('fetch', function (event) {
event.respondWith(fetch(event.request));
// or simply don't call event.respondWith, which
// will result in default browser behavior
});
Obwohl Sie diesen Fall nicht oft selbst bearbeiten müssen, deckt Cache, Fallback auf das Netzwerk dies ab.
Cache, Fallback auf Netzwerk
Ideal für: Offline-Entwicklung In diesen Fällen werden die meisten Anträge auf diese Weise verarbeitet. 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 „Nur Netzwerk“-Verhalten für alle Elemente, die nicht im Cache gespeichert sind (was alle Nicht-GET-Anfragen umfasst, da sie nicht im Cache gespeichert werden können).
Cache und Netzwerkrennen
Ideal für: kleine Assets, bei denen Sie auf Geräten mit langsamem Laufwerkzugriff auf die Leistung achten müssen
Mit einigen Kombinationen aus älteren Festplatten, Virenscannern und schnelleren Internetverbindungen kann das Abrufen von Ressourcen aus dem Netzwerk schneller sein als das Abrufen von Festplatten. Wenn der Nutzer die Inhalte auf seinem Gerät hat, kann das jedoch eine Verschwendung von Daten sein. Bedenke das.
// 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)]));
});
Netzwerk-Fallback auf Cache
Ideal für:Ressourcen, die häufig aktualisiert werden und nicht die Version der Website enthalten. z.B. Artikel, Avatare, Zeitpläne für soziale Medien und Bestenlisten für Spiele.
Das bedeutet, dass Onlinenutzern die aktuellsten Inhalte angezeigt werden, während Nutzer, die offline sind, eine ältere im Cache gespeicherte Version erhalten. Wenn die Netzwerkanfrage erfolgreich ist, sollten Sie den Cache-Eintrag aktualisieren.
Diese Methode weist jedoch Fehler auf. Wenn der Nutzer eine zeitweise oder langsame Verbindung hat, muss er warten, bis das Netzwerk ausgefallen ist, bevor er bereits akzeptable Inhalte auf seinem Gerät erhält. Das kann sehr lange dauern und die Nutzerfreundlichkeit ist frustrierend. Sehen Sie sich das nächste Muster, Cache und dann Netzwerk, an, um eine bessere Lösung zu finden.
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, Zeitleisten für soziale Medien, Spiele und Bestenlisten.
Dafür muss die Seite zwei Anfragen senden: eine an den Cache und eine an das Netzwerk. Die Idee dahinter ist, zuerst die im Cache gespeicherten Daten anzuzeigen und dann die Seite zu aktualisieren, wenn die Netzwerkdaten eintreffen.
Manchmal reicht es aus, die aktuellen Daten einfach zu ersetzen, wenn neue Daten eintreffen (z. B. in einer Bestenliste). Das kann aber durch größere Inhalte gestört werden. Verschwinden Sie nicht etwas, das Nutzende lesen oder mit dem sie interagieren.
Twitter fügt den neuen Inhalt über dem alten Inhalt hinzu und passt die Scrollposition so an, dass der Nutzer nicht unterbrochen wird. Das ist möglich, weil Twitter größtenteils eine lineare Reihenfolge der Inhalte beibehält. Ich habe dieses Muster kopiert, um mit train-to-thrill Inhalte so schnell wie möglich auf den Bildschirm zu bringen und aktuelle Inhalte anzuzeigen, sobald sie eintreffen.
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 im Netzwerk einen Cache 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 train-to-thrill habe ich XHR anstelle von Abruf verwendet und den Header Accept missbraucht, um dem Service Worker mitzuteilen, von wo das Ergebnis abgerufen werden soll (Seitencode, Service Worker-Code).
Allgemeines Fallback
Wenn etwas nicht aus dem Cache und/oder Netzwerk bereitgestellt werden kann, sollten Sie ein generisches Fallback angeben.
Ideal für:sekundäre Bilder wie Avatare, fehlgeschlagene POST-Anfragen und die 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.
}),
);
});
Bei dem Element, auf das Sie ein Fallback verwenden, handelt es sich wahrscheinlich um eine Installationsabhängigkeit.
Wenn auf Ihrer Seite eine E-Mail gepostet wird, speichert der Service Worker die E-Mail möglicherweise in einem IndexedDB-Postausgang und antwortet der Seite, dass der Sendevorgang fehlgeschlagen ist, die Daten aber beibehalten wurden.
Vorlagen auf Service Worker-Seite
Ideal für:Seiten, deren Serverantworten nicht im Cache gespeichert werden können.
Das Rendern von Seiten auf dem Server beschleunigt den Vorgang. Dies kann jedoch bedeuten, dass Statusdaten einbezogen werden, die in einem Cache nicht sinnvoll sind, z. B. "Angemeldet als...". Wenn Ihre Seite von einem Service Worker gesteuert wird, können Sie stattdessen 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',
},
});
}),
);
});
Ergebnis
Sie sind nicht auf eine dieser Methoden beschränkt. Tatsächlich werden Sie wahrscheinlich viele davon abhängig von der Anfrage-URL verwenden. Für trained-to-thrill wird beispielsweise Folgendes verwendet:
- cache on install (bei Installation im Cache) für die statische Benutzeroberfläche und das Verhalten
- bei Netzwerkantwort im Cache speichern, für Bilder und Daten von Facebook
- Aus dem Cache abrufen, auf Netzwerk zurückgreifen, bei den meisten Anfragen
- Aus Cache und dann Netzwerk abrufen, für die Suchergebnisse von Facebook
Sehen Sie sich einfach die Anfrage an und entscheiden Sie, was zu tun ist:
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);
}),
);
});
...du bekommst das Bild.
Guthaben
...für die hübschen Symbole:
- Code von Livelyrobot
- Kalender von Scott Lewis
- Netzwerk von Ben Rizzo
- SD von Thomas Le Bas
- CPU von iconmind.com
- Papierkorb von trasnik
- Benachrichtigung von @daosme
- Layout von Mister Pixel
- Cloud von P.J. Onori
Und danke an Jeff Posnick, der viele Heulfehler erkannt hat, bevor ich auf „Veröffentlichen“ klicke.
Weitere Informationen
- Einführung in Service Workers
- Ist Service Worker bereit? Sie können den Implementierungsstatus in den wichtigsten Browsern verfolgen.
- Einführung in JavaScript Promises – Leitfaden zu Promises