Ich habe mich in letzter Zeit viel mit ServiceWorker beschäftigt. Als Chris mich bat, einen Artikel darüber zu schreiben, hätte ich mich nicht mehr freuen können. ServiceWorker ist die wirkungsvollste moderne Webtechnologie seit Ajax. Es ist eine API, die im Browser lebt und zwischen Ihren Webseiten und Ihren Anwendungsservern sitzt. Nach der Installation und Aktivierung kann ein ServiceWorker programmatisch bestimmen, wie auf Anfragen nach Ressourcen aus Ihrem Ursprung reagiert werden soll, auch wenn der Browser offline ist. ServiceWorker kann zur Unterstützung des sogenannten „Offline First“-Webs verwendet werden.
ServiceWorker ist eine progressive Technologie, und in diesem Artikel zeige ich Ihnen, wie Sie eine Website so aufbereiten, dass sie für Benutzer mit modernen Browsern offline verfügbar ist, während Benutzer mit nicht unterstützten Browsern unbeeinflusst bleiben.
Hier ist ein stummgeschaltetes, 26 Sekunden langes Video eines unterstützenden Browsers (Chrome), der offline geht und die finale Demo-Website, die trotzdem funktioniert
Wenn Sie sich nur den Code ansehen möchten, gibt es ein Simple Offline Site Repository, das wir dafür erstellt haben. Sie können das Ganze als CodePen Project ansehen, und es ist sogar eine vollwertige Demo-Website.
Browser-Unterstützung
Heute hat ServiceWorker Browserunterstützung in Google Chrome, Opera und in Firefox hinter einem Konfigurationsschalter. Microsoft wird wahrscheinlich bald daran arbeiten. Von Apples Safari gibt es noch keine offizielle Stellungnahme.
Jake Archibald hat eine Seite, die die Unterstützung aller ServiceWorker-bezogenen Technologien verfolgt.
Diese Daten zur Browserunterstützung stammen von Caniuse, wo es weitere Details gibt. Eine Zahl bedeutet, dass der Browser die Funktion ab dieser Version unterstützt.
Desktop
| Chrome | Firefox | IE | Edge | Safari |
|---|---|---|---|---|
| 45 | 44 | Nein | 17 | 11.1 |
Mobil / Tablet
| Android Chrome | Android Firefox | Android | iOS Safari |
|---|---|---|---|
| 127 | 127 | 127 | 11.3-11.4 |
Angesichts der Tatsache, dass Sie diese Dinge im Stil der progressiven Verbesserung implementieren können (sie beeinträchtigen nicht unterstützte Browser), ist dies eine großartige Gelegenheit, sich einen Vorsprung zu verschaffen. Diejenigen, die unterstützt werden, werden es sehr zu schätzen wissen.
Bevor wir beginnen, möchte ich Sie auf ein paar Dinge hinweisen, die Sie berücksichtigen sollten.
Nur sichere Verbindungen
Sie sollten wissen, dass es bei ServiceWorker einige strenge Voraussetzungen gibt. In erster Linie **muss Ihre Website über eine sichere Verbindung bereitgestellt werden**. Wenn Sie Ihre Website noch über HTTP bereitstellen, ist dies vielleicht ein guter Anlass, HTTPS zu implementieren.

Sie könnten einen CDN-Proxy wie CloudFlare verwenden, um den Traffic sicher zu bedienen. Denken Sie daran, gemischte Inhaltwarnungen zu finden und zu beheben, da einige Browser Ihre Kunden möglicherweise vor Ihrer unsicheren Website warnen.
- Ich habe ein Tutorial geschrieben, das Ihnen helfen könnte, CloudFlare für Ihre Website einzurichten
- Sie möchten vielleicht LetsEncrypt.org nutzen, um ein kostenloses TLS-Zertifikat zu erhalten
Obwohl die Spezifikation für HTTP/2 verschlüsselte Verbindungen nicht an sich erzwingt, beabsichtigen Browser, HTTP/2 und ähnliche Technologien *nur* über HTTPS zu implementieren. Die ServiceWorker-Spezifikation hingegen empfiehlt die Browserimplementierung über HTTPS. Browser haben auch angedeutet, über unverschlüsselte Verbindungen bereitgestellte Websites als unsicher zu markieren. Suchmaschinen bestrafen unverschlüsselte Ergebnisse.
„Nur HTTPS“ ist die Art, wie Browser sagen: *„Das ist wichtig, das sollten Sie tun.“*
Eine Promise-basierte API
Die Zukunft von Webbrowser-API-Implementierungen ist stark Promise-lastig. Die fetch-API zum Beispiel streut süßen Promise-basierten Zucker über XMLHttpRequest. ServiceWorker nutzt fetch gelegentlich, aber es gibt auch Worker-Registrierung, Caching und Nachrichtenübermittlung, die alle Promise-basiert sind.
- Ich habe ein Tutorial geschrieben, das Ihnen helfen könnte, mit Promises zu beginnen
- Es gibt auch eine ES6-Übersicht in Stichpunkten auf meinem Blog
- Es gibt auch dieses Werkzeug, das ich geschrieben habe und das Ihnen hilft, Promises zu visualisieren, wenn Sie eher ein visueller Lerner sind

Ob Sie nun ein Fan von Promises sind oder nicht, sie werden bleiben, also gewöhnen Sie sich besser daran.
Registrieren Ihres ersten ServiceWorkers
Ich habe mit Chris an der denkbar einfachsten praktischen Demonstration der Verwendung von ServiceWorker gearbeitet. Er implementierte eine einfache Website (statisches HTML, CSS, JavaScript und Bilder) und bat mich, Offline-Unterstützung hinzuzufügen. Ich hatte das Gefühl, das wäre eine großartige Gelegenheit, zu zeigen, wie einfach und unaufdringlich es ist, einer bestehenden Website Offline-Funktionen hinzuzufügen.
Wenn Sie zum Ende springen möchten, sehen Sie sich diesen Commit zur Demo-Website auf GitHub an.
Der erste Schritt ist die Registrierung des ServiceWorkers. Anstatt blind die Registrierung zu versuchen, stellen wir mittels Feature-Detection fest, ob ServiceWorker verfügbar ist.
if ('serviceWorker' in navigator) {
}
Das folgende Codebeispiel zeigt, wie wir einen ServiceWorker installieren würden. Die JavaScript-Ressource, die an .register übergeben wird, wird im Kontext eines ServiceWorkers ausgeführt. Beachten Sie, wie die Registrierung ein Promise zurückgibt, damit Sie verfolgen können, ob die ServiceWorker-Registrierung erfolgreich war oder nicht. Ich habe den Log-Anweisungen CLIENT: vorangestellt, um es mir visuell zu erleichtern, zu erkennen, ob eine Log-Anweisung von einer Webseite oder dem ServiceWorker-Skript stammt.
// ServiceWorker is a progressive technology. Ignore unsupported browsers
if ('serviceWorker' in navigator) {
console.log('CLIENT: service worker registration in progress.');
navigator.serviceWorker.register('/service-worker.js').then(function() {
console.log('CLIENT: service worker registration complete.');
}, function() {
console.log('CLIENT: service worker registration failure.');
});
} else {
console.log('CLIENT: service worker is not supported.');
}
Der Endpunkt zur service-worker.js-Datei ist sehr wichtig. Wenn das Skript beispielsweise von /js/service-worker.js geladen würde, könnte der ServiceWorker nur Anfragen im /js/-Kontext abfangen, wäre aber blind für Ressourcen wie /other. Dies ist typischerweise ein Problem, da Sie Ihre JavaScript-Dateien normalerweise in einem Verzeichnis wie /js/, /public/, /assets/ oder ähnlich gruppieren, während Sie in den meisten Fällen das ServiceWorker-Skript vom Stammverzeichnis der Domäne aus bedienen möchten.
Das war tatsächlich die einzig notwendige Änderung an Ihrem Webanwendungscode, vorausgesetzt, Sie hatten bereits HTTPS implementiert. An diesem Punkt werden unterstützende Browser eine Anfrage für /service-worker.js ausgeben und versuchen, den Worker zu installieren.
Wie sollte die service-worker.js-Datei dann strukturiert sein?
Einen ServiceWorker zusammenstellen
ServiceWorker ist ereignisgesteuert und **Ihr Code sollte darauf abzielen, zustandslos zu sein**. Das liegt daran, dass ein ServiceWorker, wenn er nicht verwendet wird, heruntergefahren wird und seinen gesamten Zustand verliert. Sie haben keine Kontrolle darüber, daher ist es am besten, langfristige Abhängigkeiten vom In-Memory-Zustand zu vermeiden.
Unten habe ich die bemerkenswertesten Ereignisse aufgelistet, die Sie in einem ServiceWorker behandeln müssen.
- Das
install-Ereignis wird ausgelöst, wenn ein ServiceWorker zum ersten Mal heruntergeladen wird. Dies ist Ihre Chance, den ServiceWorker-Cache mit den grundlegenden Ressourcen zu befüllen, die auch dann verfügbar sein sollten, wenn Benutzer offline sind. - Das
fetch-Ereignis wird ausgelöst, wenn eine Anfrage aus dem Geltungsbereich Ihres ServiceWorkers stammt, und Sie haben die Möglichkeit, die Anfrage abzufangen und sofort zu beantworten, ohne das Netzwerk zu kontaktieren. - Das
activate-Ereignis wird nach einer erfolgreichen Installation ausgelöst. Sie können es verwenden, um ältere Versionen des Workers auszumustern. Wir werden ein einfaches Beispiel betrachten, bei dem wir veraltete Cache-Einträge gelöscht haben.
Gehen wir jedes Ereignis durch und betrachten Beispiele, wie sie behandelt werden könnten.
Installation Ihres ServiceWorkers
Eine Versionsnummer ist nützlich, wenn Sie die Worker-Logik aktualisieren, und ermöglicht es Ihnen, veraltete Cache-Einträge während des Aktivierungsschritts zu entfernen, wie wir später sehen werden. Wir werden die folgende Versionsnummer als Präfix verwenden, wenn wir Cache-Speicher erstellen.
var version = 'v1::';
Sie können addEventListener verwenden, um einen Event-Handler für das install-Ereignis zu registrieren. Die Verwendung von event.waitUntil blockiert den Installationsprozess für das bereitgestellte p-Promise. Wenn das Promise abgelehnt wird, weil beispielsweise eine der Ressourcen nicht heruntergeladen werden konnte, wird der ServiceWorker nicht installiert. Hier können Sie das Promise nutzen, das von caches.open(name) zurückgegeben wird, und es dann in cache.addAll(resources) umwandeln, was Antworten für die angegebenen Ressourcen herunterlädt und speichert.
self.addEventListener("install", function(event) {
console.log('WORKER: install event in progress.');
event.waitUntil(
/* The caches built-in is a promise-based API that helps you cache responses,
as well as finding and deleting them.
*/
caches
/* You can open a cache by name, and this method returns a promise. We use
a versioned cache name here so that we can remove old cache entries in
one fell swoop later, when phasing out an older service worker.
*/
.open(version + 'fundamentals')
.then(function(cache) {
/* After the cache is opened, we can fill it with the offline fundamentals.
The method below will add all resources we've indicated to the cache,
after making HTTP requests for each of them.
*/
return cache.addAll([
'/',
'/css/global.css',
'/js/global.js'
]);
})
.then(function() {
console.log('WORKER: install completed');
})
);
});
Sobald der Installationsschritt erfolgreich war, wird das activate-Ereignis ausgelöst. Dies hilft uns, ältere ServiceWorker auszumustern, und wir werden uns das später ansehen. Konzentrieren wir uns jetzt auf das fetch-Ereignis, das etwas interessanter ist.
Abfangen von Fetch-Anfragen
Das fetch-Ereignis wird ausgelöst, wenn eine von diesem ServiceWorker kontrollierte Seite eine Ressource anfordert. Dies beschränkt sich nicht nur auf fetch oder sogar XMLHttpRequest. Es umfasst sogar die Anfrage nach der HTML-Seite beim ersten Laden, sowie JS- und CSS-Ressourcen, Schriftarten, Bilder usw. Beachten Sie auch, dass Anfragen an andere Ursprünge ebenfalls vom fetch-Handler des ServiceWorkers abgefangen werden. Anfragen an i.imgur.com – dem CDN einer beliebten Bild-Hosting-Seite – würden beispielsweise ebenfalls von unserem ServiceWorker abgefangen, solange die Anfrage von einem der vom Worker kontrollierten Clients (z.B. Browser-Tabs) stammt.
Genau wie bei install können wir das fetch-Ereignis blockieren, indem wir ein Promise an event.respondWith(p) übergeben, und wenn das Promise erfüllt ist, wird der Worker damit statt der Standardaktion (Netzwerkanfrage) antworten. Wir können caches.match verwenden, um nach gecachten Antworten zu suchen und diese zurückzugeben, anstatt das Netzwerk zu kontaktieren.
Wie in den Kommentaren beschrieben, verwenden wir hier ein "eventuell frisches" Caching-Muster, bei dem wir die im Cache gespeicherten Daten zurückgeben, aber immer versuchen, eine Ressource erneut aus dem Netzwerk abzurufen, um den Cache aktuell zu halten. Wenn die dem Benutzer bereitgestellte Antwort veraltet ist, erhält er beim nächsten Anfordern der Ressource eine frische Antwort. Wenn die Netzwerkanfrage fehlschlägt, wird versucht, die Antwort durch eine hartkodierte Response wiederherzustellen.
self.addEventListener("fetch", function(event) {
console.log('WORKER: fetch event in progress.');
/* We should only cache GET requests, and deal with the rest of method in the
client-side, by handling failed POST,PUT,PATCH,etc. requests.
*/
if (event.request.method !== 'GET') {
/* If we don't block the event as shown below, then the request will go to
the network as usual.
*/
console.log('WORKER: fetch event ignored.', event.request.method, event.request.url);
return;
}
/* Similar to event.waitUntil in that it blocks the fetch event on a promise.
Fulfillment result will be used as the response, and rejection will end in a
HTTP response indicating failure.
*/
event.respondWith(
caches
/* This method returns a promise that resolves to a cache entry matching
the request. Once the promise is settled, we can then provide a response
to the fetch request.
*/
.match(event.request)
.then(function(cached) {
/* Even if the response is in our cache, we go to the network as well.
This pattern is known for producing "eventually fresh" responses,
where we return cached responses immediately, and meanwhile pull
a network response and store that in the cache.
Read more:
https://ponyfoo.com/articles/progressive-networking-serviceworker
*/
var networked = fetch(event.request)
// We handle the network request with success and failure scenarios.
.then(fetchedFromNetwork, unableToResolve)
// We should catch errors on the fetchedFromNetwork handler as well.
.catch(unableToResolve);
/* We return the cached response immediately if there is one, and fall
back to waiting on the network as usual.
*/
console.log('WORKER: fetch event', cached ? '(cached)' : '(network)', event.request.url);
return cached || networked;
function fetchedFromNetwork(response) {
/* We copy the response before replying to the network request.
This is the response that will be stored on the ServiceWorker cache.
*/
var cacheCopy = response.clone();
console.log('WORKER: fetch response from network.', event.request.url);
caches
// We open a cache to store the response for this request.
.open(version + 'pages')
.then(function add(cache) {
/* We store the response for this request. It'll later become
available to caches.match(event.request) calls, when looking
for cached responses.
*/
cache.put(event.request, cacheCopy);
})
.then(function() {
console.log('WORKER: fetch response stored in cache.', event.request.url);
});
// Return the response so that the promise is settled in fulfillment.
return response;
}
/* When this method is called, it means we were unable to produce a response
from either the cache or the network. This is our opportunity to produce
a meaningful response even when all else fails. It's the last chance, so
you probably want to display a "Service Unavailable" view or a generic
error response.
*/
function unableToResolve () {
/* There's a couple of things we can do here.
- Test the Accept header and then return one of the `offlineFundamentals`
e.g: `return caches.match('/some/cached/image.png')`
- You should also consider the origin. It's easier to decide what
"unavailable" means for requests against your origins than for requests
against a third party, such as an ad provider
- Generate a Response programmaticaly, as shown below, and return that
*/
console.log('WORKER: fetch request failed in both cache and network.');
/* Here we're creating a response programmatically. The first parameter is the
response body, and the second one defines the options for the response.
*/
return new Response('<h1>Service Unavailable</h1>', {
status: 503,
statusText: 'Service Unavailable',
headers: new Headers({
'Content-Type': 'text/html'
})
});
}
})
);
});
Es gibt mehrere weitere Strategien, einige davon diskutiere ich in einem Artikel über ServiceWorker-Strategien auf meinem Blog.
Wie versprochen, werfen wir einen Blick auf den Code, mit dem Sie ältere Versionen Ihres ServiceWorker-Skripts ausmustern können.
Ausmustern älterer ServiceWorker-Versionen
Das activate-Ereignis wird ausgelöst, nachdem ein ServiceWorker erfolgreich installiert wurde. Es ist am nützlichsten, wenn eine ältere Version eines ServiceWorkers ausgemustert wird, da Sie zu diesem Zeitpunkt wissen, dass der neue Worker korrekt installiert wurde. In diesem Beispiel löschen wir alte Caches, die nicht mit der version des Workers übereinstimmen, den wir gerade installiert haben.
self.addEventListener("activate", function(event) {
/* Just like with the install event, event.waitUntil blocks activate on a promise.
Activation will fail unless the promise is fulfilled.
*/
console.log('WORKER: activate event in progress.');
event.waitUntil(
caches
/* This method returns a promise which will resolve to an array of available
cache keys.
*/
.keys()
.then(function (keys) {
// We return a promise that settles when all outdated caches are deleted.
return Promise.all(
keys
.filter(function (key) {
// Filter by keys that don't start with the latest version prefix.
return !key.startsWith(version);
})
.map(function (key) {
/* Return a promise that's fulfilled
when each outdated cache is deleted.
*/
return caches.delete(key);
})
);
})
.then(function() {
console.log('WORKER: activate completed.');
})
);
});
Zur Erinnerung: Es gibt ein Simple Offline Site Repository, das wir dafür erstellt haben. Sie können das Ganze als CodePen Project ansehen, und es ist sogar eine vollwertige Demo-Website.
Leicht am Thema vorbei, aber der Code in diesem Artikel ist sehr lang und aufgrund der schmalen Darstellung in den bereitgestellten Code-Ansichten unmöglich zu lesen.
Was die Technologie selbst betrifft? Absolut fantastisch. Die Möglichkeit, praktisch einen clientseitigen Proxy-Server zu schreiben, ist unglaublich nützlich.
Ich habe beim Lesen desselben gedacht. Ich habe das Stylish Browser-Plugin verwendet, um die Seitenleiste von Chris auszublenden –
@namespace url(http://www.w3.org/1999/xhtml);
@-moz-document domain(“css-tricks.com”) {
.entry-unrelated { display: none !important; }
.blog-posts.grid-2-3 { width: 100% !important; }
}
@Neal Das ist die Art von „CAN DO“-Einstellung, die ich liebe!
Hallo Chris,
Was ist mit einem Umschalter, der uns ermöglicht, den Hauptinhalt auf 100% zu vergrößern, um größere Codeblöcke wie diesen anzuzeigen? Dev-Tooling ist in Ordnung, aber es als Teil der Site-Funktion zu haben, wäre großartig.
CSS-Tricks kann Codeblöcke beim Hover mit ein paar CSS-Stilen erweiterbar machen: https://www.youtube.com/watch?v=w79Uze8sTrU
@Šime: width: -moz-max-content! cool! Viel besser als eine magische Zahl. Außerdem denke ich, dass es nur dann breiter würde, *wenn* es breiter werden müsste.
Ich kann den ServiceWorker unter Chrome 47.0.2526.49 Beta (64-bit) nicht dazu bringen, die Offline-Website bereitzustellen, wenn ich die Netzwerkdrosselung auf Offline setze. Der Worker ist registriert und scheint laut Inspektion zu laufen, aber Chrome gibt nur die Meldung „Unable to connect to the Internet“ aus.
Das ist, was ich bekomme
Aber ich habe manchmal eher inkonsistente Ergebnisse gesehen und unterschiedliche Ergebnisse beim hartem Neuladen im Vergleich zum normalen Neuladen.
Hallo Chris,
Ich kann es auch nicht zum Laufen bringen. Ich habe dasselbe Problem wie Johnny. Ich habe es auf Chrome 46.0.2490.86 (64-bit) und Chrome 48.0.2563.0 Canary (64-bit) ausprobiert.
Ich bin mir nicht sicher, welche weiteren Details ich geben soll, aber lassen Sie es mich wissen und ich gebe gerne mehr Informationen. :)
Danke!
Hey, nur zur Info, das Tutorial hier funktioniert, aber die Demo-Website nicht. :)
Polymer macht diesen Anwendungsfall mit dem Element unglaublich einfach. Es hat mich etwa 5 Minuten gekostet, eine 100% offline einfache Website zu haben.
das
<platinum-sw-register>Element*Das ist ungefähr die Zeit, die es dauert, ServiceWorker ohne Bibliotheken zu implementieren, und Sie können es überall implementieren, sobald Sie die Grundlagen gelernt haben (nicht nur auf Websites, die Polymer verwenden).
Safaris Mangel an „Enthusiasmus“ ist das Einzige, was mich davon abhält, weiter mit Service Workers zu experimentieren.
Aus Apples Sicht ist es sinnvoll, sich nicht selbst ins Bein zu schießen, indem man einfach zu bedienende offline-fähige Web-Apps zulässt, möglicherweise aus anderen Gründen, von denen ich sicher bin. Es ist einfach schlecht für Webentwickler.
Wenn man bedenkt, dass sogar der Internet Explorer die Unterstützung von Service Workers in Erwägung zieht, könnte es an der Zeit sein, ohne Safari voranzukommen.