Mit Service Workern haben wir aufgegeben, das Offline-Problem zu lösen, und Entwicklern die nötigen Bausteine an die Hand gegeben, damit sie es selbst lösen können. Sie haben die 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 einzeln an. In der Praxis werden Sie jedoch wahrscheinlich viele davon je nach URL und Kontext gemeinsam verwenden.
Eine funktionierende Demo einiger dieser Muster finden Sie unter Trained-to-thrill und in diesem Video, das die Leistungsauswirkungen zeigt.
Der Cache – wann Ressourcen gespeichert werden
Mit Service Worker können Sie Anfragen unabhängig vom Caching verarbeiten. Daher werde ich sie separat vorstellen. Zuerst: Wann sollte das Caching erfolgen?
Bei der Installation – als Abhängigkeit
Der Service Worker sendet ein install
-Ereignis. So kannst du Dinge vorbereiten, die bereit sein müssen, bevor du andere Ereignisse behandelst. Währenddessen wird die vorherige Version Ihres Service Workers weiterhin ausgeführt und sendet Seiten aus. Ihre Änderungen dürfen dies nicht beeinträchtigen.
Ideal für: CSS, Bilder, Schriftarten, JS, Vorlagen – im Grunde alles, was Sie für diese „Version“ Ihrer Website als statisch betrachten.
Wenn diese Elemente nicht abgerufen werden, funktioniert Ihre Website nicht. Sie sind Teil des ursprünglichen Downloads einer entsprechenden plattformspezifischen App.
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
nimmt ein Versprechen an, um die Dauer und den Erfolg der Installation zu definieren. Wenn das Versprechen abgelehnt wird, wird die Installation als fehlgeschlagen betrachtet und dieser Dienst-Worker wird aufgegeben. Wenn eine ältere Version ausgeführt wird, bleibt sie intakt. caches.open()
und cache.addAll()
-Rückgabeversprechen.
Wenn eine der Ressourcen nicht abgerufen werden kann, wird der cache.addAll()
-Aufruf abgelehnt.
Auf trained-to-thrill verwende ich das, um statische Assets im Cache zu speichern.
Bei der Installation – nicht als Abhängigkeit
Diese Option ist mit der oben beschriebenen vergleichbar, führt aber nicht zu einer Verzögerung der Installation und auch nicht zum Abbruch der Installation, 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
();
}),
);
});
Im obigen Beispiel wird das cache.addAll
-Versprechen für die Level 11–20 nicht an event.waitUntil
zurückgegeben. Selbst wenn der Vorgang fehlschlägt, ist das Spiel also weiterhin offline verfügbar. Natürlich müssen Sie für das mögliche Fehlen dieser Ebenen sorgen und versuchen, sie noch einmal zu cachen, falls sie fehlen.
Der Dienst-Worker wird möglicherweise beendet, während die Level 11–20 heruntergeladen werden, da die Verarbeitung von Ereignissen abgeschlossen ist. Das bedeutet, dass sie nicht im Cache gespeichert werden. In Zukunft wird die Web Periodic Background Sync API solche Fälle und größere Downloads wie Filme verarbeiten. Diese API wird derzeit nur in Chromium-Forks unterstützt.
Bei Aktivierung
Ideal für:Bereinigung und Migration.
Sobald ein neuer Dienst-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 verwendet wird, ist jetzt ein guter Zeitpunkt, Schemamigrationen in IndexedDB vorzunehmen 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. Eine lange Aktivierung kann daher das Laden der Seite blockieren. Begrenzen Sie die Aktivierung so weit wie möglich und verwenden Sie sie nur für Dinge, die Sie nicht tun konnten, während die alte 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 gestellt werden kann und Sie den Nutzern erlauben möchten, 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 Flickr.
Bieten Sie Nutzern die Möglichkeit, Inhalte mit der Schaltfläche „Später lesen“ oder „Für die Offlinenutzung speichern“ zu speichern. Wenn darauf geklickt wird, wird das benötigte Element aus dem Netzwerk abgerufen und in den 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 Caches API ist sowohl auf Seiten als auch auf Service-Workern verfügbar. Sie können also direkt über die Seite zum Cache beitragen.
Auf Netzwerkantwort
Ideal für: häufig aktualisierte Ressourcen wie den Posteingang eines Nutzers oder Artikelinhalte. Auch für nicht wesentliche Inhalte wie Avatare geeignet, aber Vorsicht ist geboten.
Wenn eine Anfrage nicht mit einem Eintrag im 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 Sie den Speicherplatz Ihres Ursprungs nicht überlasten. 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;
})
);
});
}),
);
});
Für eine effiziente Speichernutzung können Sie den Textkörper einer Antwort/Anfrage nur einmal lesen. Im Code oben 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 im Cache zu speichern.
Stale-while-revalidate
Ideal für: häufig aktualisierte Ressourcen, bei denen die neueste Version nicht unbedingt erforderlich ist. Avatare können in diese Kategorie fallen.
Wenn eine Version im Cache verfügbar ist, verwenden Sie diese, holen Sie sich aber für das nächste Mal ein Update.
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-Mechanismus von HTTP sehr ähnlich.
Bei Push-Nachricht
Die Push API ist eine weitere Funktion, die auf Service Workern basiert. So kann der Dienst-Worker als Reaktion auf eine Nachricht vom Messaging-Dienst des Betriebssystems geweckt werden. Das passiert auch, wenn der Nutzer keinen Tab mit Ihrer Website geöffnet hat. Nur der Service Worker wird geweckt. Sie fordern auf einer Seite die Berechtigung dazu an und der Nutzer wird aufgefordert, diese zu gewähren.
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 und von einer sofortigen Synchronisierung profitieren, z. B. eine Aktualisierung einer To-do-Liste oder eine Kalenderänderung.
Häufig ist das Endergebnis eine Benachrichtigung, die beim Tippen eine relevante Seite öffnet oder den Fokus darauf legt. Es ist jedoch äußerst wichtig, die Caches vor dem Tippen zu aktualisieren. Der Nutzer ist zwar zum Zeitpunkt des Empfangs der Push-Nachricht online, aber möglicherweise nicht, wenn er schließlich mit der Benachrichtigung interagiert. Daher ist es wichtig, diese Inhalte auch offline verfügbar zu machen.
Mit diesem Code werden die 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/');
}
});
Hintergrundsynchronisierung aktiviert
Die Synchronisierung im Hintergrund ist eine weitere Funktion, die auf Service Workern basiert. Sie können die Hintergrunddatensynchronisierung 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 geweckt. Sie fordern auf einer Seite die Berechtigung dazu an und der Nutzer wird aufgefordert.
Ideal für: Nicht dringende Updates, insbesondere solche, die so regelmäßig auftreten, dass eine Push-Nachricht pro Update für Nutzer zu häufig wäre, z. B. Zeitachsen in sozialen 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 zugewiesen, den Sie nach Belieben nutzen können. Dieser kostenlose Speicherplatz wird zwischen allen Speicherorten geteilt: (lokaler) Speicher, IndexedDB, Dateisystemzugriff und natürlich Caches.
Der Betrag, den Sie erhalten, ist nicht festgelegt. Sie variiert je nach Gerät und Lagerbedingungen. So kannst du herausfinden, wie viel du hast:
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 Speicherplatz des Geräts knapp wird. Leider kann der Browser nicht zwischen den Filmen unterscheiden, die Sie um jeden Preis behalten möchten, und dem Spiel, das Ihnen nicht wirklich wichtig ist.
Verwenden Sie dazu die StorageManager-Oberfläche:
// 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 seine Einwilligung geben. Verwenden Sie dazu die Permissions API.
Es ist wichtig, dass der Nutzer Teil dieses Ablaufs ist, da wir davon ausgehen können, dass er jetzt die Kontrolle über das Löschen hat. Wenn der Speicherplatz des Geräts knapp wird und das Löschen nicht benötigter Daten das Problem nicht löst, kann der Nutzer entscheiden, welche Elemente er behalten und welche er entfernen möchte.
Damit dies funktioniert, müssen Betriebssysteme „dauerhafte“ Ursprünge in ihren Aufschlüsselungen der Speichernutzung als gleichwertig zu plattformspezifischen Apps behandeln, anstatt den Browser als einzelnes Element zu melden.
Vorschläge für die Auslieferung – auf Anfragen antworten
Es spielt keine Rolle, wie viel Sie zwischenspeichern. Der Service Worker verwendet den Cache nur, wenn Sie ihm mitteilen, wann und wie. Hier sind einige Muster für die Bearbeitung 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 im Cache gespeichert haben, damit Sie sicher sein 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, wird er unter Cache, Rückfall auf Netzwerk behandelt.
Nur Netzwerk
Ideal für:Dinge, die kein Offline-Äquivalent haben, z. B. Analytics-Pings, 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 speziell behandeln müssen, wird er unter Cache, Rückfall auf Netzwerk behandelt.
Cache, Netzwerk als Rückfall
Ideal für: Offline-first-Entwicklung In solchen Fällen gehen Sie so mit der Mehrheit der Anfragen um. Andere Muster sind Ausnahmen, die sich aus der Anfrage ergeben.
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
return response || fetch(event.request);
}),
);
});
So wird das Verhalten „Nur Cache“ für Elemente im Cache und das Verhalten „Nur Netzwerk“ für Elemente angewendet, die nicht im Cache gespeichert sind. Dazu gehören alle Anfragen, die keine GET-Anfragen sind, da sie nicht im Cache gespeichert werden können.
Cache- und Netzwerkauslastung
Ideal für:kleine Assets, bei denen auf Geräten mit langsamem Laufwerkzugriff eine hohe Leistung erforderlich ist.
Bei einigen Kombinationen aus älteren Festplatten, Virenscannern und schnelleren Internetverbindungen kann es schneller gehen, Ressourcen aus dem Netzwerk abzurufen als vom Laufwerk. Wenn der Nutzer die Inhalte jedoch bereits auf seinem Gerät hat, kann ein Zugriff auf das Netzwerk zu einer Datenverschwendung führen.
// 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 greift auf Cache zurück
Ideal für: Schnelle Lösung für Ressourcen, die unabhängig von der „Version“ der Website häufig aktualisiert werden. Beispiele: Artikel, Avatare, Zeitachsen in sozialen Medien und Bestenlisten in Spielen.
So erhalten Onlinenutzer die aktuellsten Inhalte, während Offlinenutzer eine ältere Version aus dem Cache sehen. Wenn die Netzwerkanfrage erfolgreich war, sollten Sie den Cacheeintrag aktualisieren.
Diese Methode hat jedoch Mängel. Wenn der Nutzer eine unterbrochene oder langsame Verbindung hat, muss er warten, bis das Netzwerk ausfällt, bevor er die akzeptablen Inhalte erhält, die bereits auf seinem Gerät vorhanden sind. Das kann extrem lange dauern und ist frustrierend für die Nutzer. Eine bessere Lösung finden Sie im nächsten Muster, Cache, dann Netzwerk.
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, Zeitachsen in sozialen Medien und Bestenlisten von Spielen.
Dazu muss die Seite zwei Anfragen stellen, eine an den Cache und eine an das Netzwerk. Die Idee ist, zuerst die im Cache gespeicherten Daten anzuzeigen und die Seite dann zu aktualisieren, wenn die Netzwerkdaten eintreffen.
Manchmal kannst du die aktuellen Daten einfach ersetzen, wenn neue Daten eintreffen (z. B. bei Bestenlisten in Spielen). Bei größeren Inhalten kann das jedoch zu Unterbrechungen führen. Lassen Sie nichts verschwinden, das 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 die Inhalte größtenteils in einer linearen Reihenfolge anzeigt. Ich habe dieses Muster für trained-to-thrill kopiert, um Inhalte so schnell wie möglich auf dem Bildschirm zu präsentieren und gleichzeitig aktuelle Inhalte zu zeigen, 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 zum Netzwerk gehen und den 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 trained-to-thrill habe ich das Problem umgangen, indem ich XHR anstelle von fetch verwendet und den Accept-Header missbraucht habe, um dem Service Worker mitzuteilen, woher er das Ergebnis abrufen soll (Seitencode, Service Worker-Code).
Generischer Fallback
Wenn keine Inhalte aus dem Cache und/oder Netzwerk ausgeliefert werden können, sollten Sie einen generischen Fallback bereitstellen.
Ideal für:sekundäre Bilder wie Avatare, fehlgeschlagene POST-Anfragen und die Seite „Im Offlinemodus 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 ggf. im IndexedDB-Postausgang speichern und die Seite darüber informieren, dass der Versand fehlgeschlagen ist, die Daten aber erfolgreich gespeichert wurden.
Vorlagen auf Service Worker-Seite
Ideal für: Seiten, deren Serverantwort nicht im Cache gespeichert werden kann.
Das Rendern von Seiten auf dem Server ist schnell, kann aber dazu führen, dass Statusdaten eingeschlossen werden, die in einem Cache möglicherweise keinen Sinn ergeben, 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 stattdessen 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. Je nach Anfrage-URL werden Sie wahrscheinlich viele davon verwenden. Für trained-to-thrill werden beispielsweise folgende Elemente verwendet:
- cache on install für die statische Benutzeroberfläche und das Verhalten
- cache on network response für die Flickr-Bilder und -Daten
- Aus dem Cache abrufen und bei Bedarf auf das Netzwerk zurückgreifen, für die meisten Anfragen
- Aus dem Cache und dann aus dem Netzwerk abrufen 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);
}),
);
});
…Sie verstehen.
Gutschriften
…für die schönen Symbole:
- Code von buzzyrobot
- Calendar von Scott Lewis
- Network 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
Und vielen Dank an Jeff Posnick, der viele gravierende Fehler gefunden hat, bevor ich auf „Veröffentlichen“ geklickt habe.
Weitere Informationen
- Einführung in Service Worker
- Is Service Worker ready?: Hier können Sie den Implementierungsstatus in den wichtigsten Browsern verfolgen.
- JavaScript Promises – eine Einführung – Leitfaden zu Versprechen