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.
Die Cache-Maschine – wann Ressourcen gespeichert werden sollen
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 Ihnen 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 es 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.
Wenn ein neuer Service Worker installiert wurde und keine vorherige Version verwendet wird, wird die neue aktiviert und Sie erhalten das Ereignis activate
. Da die alte Version nicht mehr verfügbar ist, ist ein guter Zeitpunkt, um Schemamigrationen in IndexedDB zu verarbeiten und auch 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.
Bei 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 (z. B. Avatare) tun, müssen Sie darauf achten, dass Sie den Speicher Ihres Ursprungs nicht überladen. 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 kann der Text einer Antwort/Anfrage nur einmal gelesen werden. 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 gehören zu dieser 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;
});
}),
);
});
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/');
}
});
Bei Hintergrundsynchronisierung
Die Hintergrundsynchronisierung ist ein weiteres Feature, das auf Service Worker 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 Aktualisierungen, insbesondere solche, die so regelmäßig erfolgen, dass eine Push-Nachricht pro Update für Nutzer zu häufig wäre, z. B. für Social-Media-Zeitleisten 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 angegeben. Dieser Wert kann je nach Gerät und Speicherbedingungen variieren. 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 er jetzt davon ausgehen kann, dass er die Löschung selbst steuern kann. Wenn der Speicherplatz auf dem Gerät 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.
Dazu müssen Betriebssysteme in der Aufschlüsselung der Speichernutzung „langlebige“ Ursprünge als gleichwertig mit 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, für die es keine Offline-Entsprechung gibt, z. B. 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 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 gehst du so vor: 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);
}),
);
});
Dadurch erhalten Sie das „Nur Cache“-Verhalten für Elemente im Cache und das „Nur Netzwerk“-Verhalten für alles, was nicht im Cache gespeichert ist (einschließlich aller Nicht-GET-Anfragen, da sie nicht im Cache gespeichert werden können).
Cache- und Netzwerkausführungsstörungen
Ideal für: Kleine Assets, bei denen auf Geräten mit langsamem Laufwerkzugriff eine hohe Leistung erforderlich ist.
Mit einigen Kombinationen aus älteren Festplatten, Virenscannern und schnelleren Internetverbindungen können Ressourcen aus dem Netzwerk schneller abgerufen werden als auf der Festplatte. 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 den 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, Social-Media-Zeitachsen, Spiele oder Bestenlisten.
Dazu muss die Seite zwei Anfragen stellen, eine an den Cache und eine an das Netzwerk. Der Grundgedanke ist, zuerst die im Cache gespeicherten Daten anzuzeigen und dann die Seite zu aktualisieren, wenn bzw. 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, 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 die Inhalte größtenteils in einer linearen Reihenfolge anzeigt. Ich habe dieses Muster für train-to-thrill kopiert, um Inhalte so schnell wie möglich auf den Bildschirm zu bringen und gleichzeitig 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 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 etwas nicht aus dem Cache und/oder Netzwerk bereitgestellt wird, können Sie ein allgemeines Fallback verwenden.
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, bei denen die 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 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 (Cache bei Netzwerkantwort) 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
- Kalender 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
Vielen Dank an Jeff Posnick, dass er viele Heulende Fehler entdeckt hat, bevor ich auf "Veröffentlichen" klicke.
Weitere Informationen
- Service Worker – Einführung
- Ist Service Worker bereit? – Verfolgen Sie den Implementierungsstatus in den wichtigsten Browsern.
- JavaScript Promises – eine Einführung – Leitfaden zu Versprechen