Guten Tag, Freunde!
Ich präsentiere Ihnen eine Übersetzung von Jake Archibalds ausgezeichnetem Artikel "Offline Cookbook", der verschiedenen Anwendungsfällen der ServiceWorker-API und der Cache-API gewidmet ist.
Es wird davon ausgegangen, dass Sie mit den Grundlagen dieser Technologien vertraut sind, da viel Code und wenige Wörter vorhanden sind.
Wenn Sie nicht vertraut sind, beginnen Sie mit MDN und kehren Sie dann zurück. Hier ist ein weiterer guter Artikel über Servicemitarbeiter speziell für Visuals.
Ohne weiteres Vorwort.
Wann sollten Ressourcen gespart werden?
Mit dem Worker können Sie Anforderungen unabhängig vom Cache verarbeiten, sodass wir sie separat betrachten.
Die erste Frage ist, wann Sie Ressourcen zwischenspeichern sollten.
Bei Installation als Abhängigkeit
Eines der Ereignisse, die auftreten, wenn ein Worker ausgeführt wird, ist das Installationsereignis. Dieses Ereignis kann verwendet werden, um sich auf die Behandlung anderer Ereignisse vorzubereiten. Wenn ein neuer Worker installiert wird, wird die Seite weiterhin von dem alten bereitgestellt, sodass die Behandlung des Installationsereignisses nicht zu einer Unterbrechung führen sollte.
Geeignet zum Zwischenspeichern von Stilen, Bildern, Skripten, Vorlagen ... im Allgemeinen für alle statischen Dateien, die auf der Seite verwendet werden.
Es handelt sich um Dateien, ohne die die Anwendung nicht wie die Dateien funktionieren kann, die beim ersten Download nativer Anwendungen enthalten sind.
self.addEventListener('install', event => {
event.waitUntil(
caches.open('mysite-static-v3')
.then(cache => cache.addAll([
'/css/whatever-v3.css',
'/css/imgs/sprites-v6.png',
'/css/fonts/whatever-v8.woff',
'/js/all-min-v4.js'
// ..
]))
)
})
event.waitUntil akzeptiert ein Versprechen, die Dauer und das Ergebnis der Installation zu bestimmen. Wenn das Versprechen abgelehnt wird, wird der Worker nicht installiert. caches.open und cache.addAlle geben Versprechen zurück. Wenn eine der Ressourcen nicht verfügbar ist, wird der
Aufruf von cache.addAll abgelehnt.
Bei Installation nicht als Abhängigkeit
Dies ähnelt dem vorherigen Beispiel, aber in diesem Fall warten wir nicht auf den Abschluss der Installation, sodass die Installation nicht abgebrochen wird.
Geeignet für große Ressourcen, die derzeit nicht benötigt werden, z. B. Ressourcen für die späteren Level des Spiels.
self.addEventListener('install', event => {
event.waitUntil(
caches.open('mygame-core-v1')
.then(cache => {
cache.addAll(
// 11-20
)
return cache.addAll(
// 1-10
)
})
)
})
Wir geben das Versprechen cache.addAll nicht an event.waitUntil für Level 11-20 weiter. Wenn es abgelehnt wird, wird das Spiel weiterhin offline ausgeführt. Natürlich sollten Sie sich um mögliche Probleme beim Zwischenspeichern der ersten Ebenen kümmern und beispielsweise das Zwischenspeichern im Fehlerfall erneut versuchen.
Der Worker kann nach der Verarbeitung von Ereignissen angehalten werden, bevor die Ebenen 11 bis 20 zwischengespeichert werden. Dies bedeutet, dass diese Ebenen nicht gespeichert werden. In Zukunft ist geplant, dem Worker eine Hintergrundladeschnittstelle hinzuzufügen, um dieses Problem zu lösen, und große Dateien wie Filme herunterzuladen.
Ca. Per .: Diese Schnittstelle wurde Ende 2018 implementiert und hieß Background Fetch , funktioniert aber bisher nur in Chrome und Opera (68% laut CanIUse ).
Bei Aktivierung
Geeignet zum Löschen von altem Cache und Migrationen.
Nachdem Sie einen neuen Worker installiert und den alten gestoppt haben, wird der neue Worker aktiviert und wir erhalten ein Aktivierungsereignis. Dies ist eine großartige Gelegenheit, Ressourcen zu ersetzen und alten Cache zu löschen.
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys()
.then(cacheNames => Promise.all(
cacheNames.filter(cacheName => {
// true, , ,
// ,
}).map(cacheName => caches.delete(cacheName))
))
)
})
Während der Aktivierung werden andere Ereignisse wie das Abrufen in die Warteschlange gestellt, sodass eine lange Aktivierung theoretisch die Seite blockieren könnte. Verwenden Sie diese Phase also nur für Dinge, die der alte Arbeiter nicht tun kann.
Wenn ein benutzerdefiniertes Ereignis auftritt
Geeignet, wenn die gesamte Site nicht offline geschaltet werden kann. In diesem Fall geben wir dem Benutzer die Möglichkeit zu entscheiden, was zwischengespeichert werden soll. Zum Beispiel ein Youtube-Video, eine Wikipedia-Seite oder eine Bildergalerie auf Flickr.
Geben Sie dem Benutzer die Schaltfläche Später lesen oder Speichern. Wenn Sie auf die Schaltfläche klicken, rufen Sie die Ressource ab und schreiben Sie sie in den Cache.
document.querySelector('.cache-article').addEventListener('click', event => {
event.preventDefault()
const id = event.target.dataset.id
caches.open(`mysite-article ${id}`)
.then(cache => fetch(`/get-article-urls?id=${id}`)
.then(response => {
// get-article-urls JSON
// URL
return response.json()
}).then(urls => cache.addAll(urls)))
})
Die Caching-Oberfläche ist auf der Seite verfügbar, genau wie der Worker selbst, sodass wir diesen nicht aufrufen müssen, um Ressourcen zu sparen.
Beim Empfang einer Antwort
Geeignet für häufig aktualisierte Ressourcen wie das Postfach oder den Artikelinhalt eines Benutzers. Auch für kleinere Inhalte wie Avatare geeignet, aber seien Sie in diesem Fall vorsichtig.
Wenn sich die angeforderte Ressource nicht im Cache befindet, wird sie vom Netzwerk abgerufen, an den Client gesendet und in den Cache geschrieben.
Wenn Sie mehrere URLs anfordern, z. B. Avatar-Pfade, stellen Sie sicher, dass der Ursprungsspeicher (Ursprung - Protokoll, Host und Port) nicht überläuft. Wenn der Benutzer Speicherplatz freigeben muss, sollten Sie nicht der Erste sein. Achten Sie darauf, unnötige Ressourcen zu entfernen.
self.addEventListener('fetch', event => {
event.respondWith(
caches.open('mysite-dynamic')
.then(cache => cache.match(event.request)
.then(response => response || fetch(event.request)
.then(response => {
cache.put(event.request, response.clone())
return response
})))
)
})
Um den Speicher effizient zu nutzen, lesen wir den Antworttext nur einmal. Im obigen Beispiel wird mit der Klonmethode eine Kopie der Antwort erstellt. Dies geschieht, um gleichzeitig eine Antwort an den Client zu senden und in den Cache zu schreiben.
Während der Überprüfung auf Neuheit
Geeignet zum Aktualisieren von Ressourcen, für die nicht die neuesten Versionen erforderlich sind. Dies kann auch für Avatare gelten.
Wenn sich die Ressource im Cache befindet, verwenden wir sie, erhalten jedoch bei der nächsten Anforderung ein Update.
self.addEventListener('fetch', event => {
event.respondWith(
caches.open('mysite-dynamic')
.then(cache => cache.match(event.request)
.then(response => {
const fetchPromise = fetch(event.request)
.then(networkResponse => {
cache.put(event.request, networkResponse.clone())
return networkResponse
})
return response || fetchPromise
}))
)
})
Wenn Sie eine Push-Benachrichtigung erhalten
Die Push-API ist eine Abstraktion über den Worker. Es ermöglicht dem Worker, als Antwort auf eine Nachricht vom Betriebssystem ausgeführt zu werden. Darüber hinaus geschieht dies unabhängig vom Benutzer (wenn die Browser-Registerkarte geschlossen ist). Eine Seite sendet normalerweise eine Anfrage an den Benutzer, um die Berechtigung zum Ausführen bestimmter Aktionen zu erhalten.
Geeignet für Inhalte, die von Benachrichtigungen abhängen, wie Chat-Nachrichten, Newsfeeds, E-Mails. Wird auch zum Synchronisieren von Inhalten wie Aufgaben in einer Liste oder Markierungen in einem Kalender verwendet.
Das Ergebnis ist eine Benachrichtigung, die beim Klicken die entsprechende Seite öffnet. Es ist jedoch sehr wichtig, Ressourcen zu schonen, bevor Sie die Benachrichtigung senden. Der Benutzer ist online, wenn die Benachrichtigung empfangen wird, aber er ist möglicherweise offline, wenn er darauf klickt. Daher ist es wichtig, dass der Inhalt zu diesem Zeitpunkt offline verfügbar ist. Die Twitter Mobile App macht das etwas falsch.
Ohne Netzwerkverbindung bietet Twitter keine Benachrichtigungsinhalte. Durch Klicken auf die Benachrichtigung wird diese jedoch gelöscht. Tu das nicht!
Der folgende Code aktualisiert den Cache vor dem Senden der Benachrichtigung:
self.addEventListener('push', event => {
if (event.data.text() === 'new-email') {
event.waitUntil(
caches.open('mysite-dynamic')
.then(cache => fetch('/inbox.json')
.then(response => {
cache.put('/inbox.json', response.clone())
return response.json()
})).then(emails => {
registration.showNotification('New email', {
body: `From ${emails[0].from.name}`,
tag: 'new-email'
})
})
)
}
})
self.addEventListener('notificationclick', event => {
if (event.notification.tag === 'new-email') {
// , , /inbox/ ,
// ,
new WindowClient('/inbox/')
}
})
Mit Hintergrundsynchronisation
Hintergrundsynchronisierung ist eine weitere Abstraktion über den Worker. Sie können eine einmalige oder regelmäßige Synchronisierung der Hintergrunddaten anfordern. Es ist auch unabhängig vom Benutzer. Ein Antrag auf Erlaubnis wird jedoch auch an ihn gesendet.
Geeignet für die Aktualisierung kleinerer Ressourcen, deren regelmäßiges Versenden von Benachrichtigungen zu häufig und daher für den Benutzer ärgerlich ist, z. B. neue Ereignisse in einem sozialen Netzwerk oder neue Artikel im Newsfeed.
self.addEventListener('sync', event => {
if (event.id === 'update-leaderboard') {
event.waitUntil(
caches.open('mygame-dynamic')
.then(cache => cache.add('/leaderboard.json'))
)
}
})
Cache speichern
Ihre Quelle bietet eine bestimmte Menge an freiem Speicherplatz. Dieser Speicherplatz wird von allen Speichern gemeinsam genutzt: lokal und Sitzung, indizierte Datenbank, Dateisystem und natürlich Cache.
Die Speichergrößen sind nicht festgelegt und variieren je nach Gerät und Speicherbedingungen. Sie können es so überprüfen:
navigator.storageQuota.queryInfo('temporary').then(info => {
console.log(info.quota)
// : < >
console.log(info.usage)
// < >
})
Wenn die Größe dieses oder jenes Speichers das Limit erreicht, wird dieser Speicher nach bestimmten Regeln gelöscht, die derzeit nicht geändert werden können.
Um dieses Problem zu lösen, wurde die Schnittstelle zum Senden einer Berechtigungsanforderung (requestPersistent) vorgeschlagen:
navigator.storage.requestPersistent().then(granted => {
if (granted) {
// ,
}
})
Natürlich muss der Benutzer hierfür die Erlaubnis erteilen. Der Benutzer muss Teil dieses Prozesses sein. Wenn der Speicher auf dem Gerät des Benutzers voll ist und das Löschen kleinerer Daten das Problem nicht löst, muss der Benutzer entscheiden, welche Daten aufbewahrt und welche gelöscht werden sollen.
Damit dies funktioniert, muss das Betriebssystem die Browserspeicher als separate Elemente behandeln.
Anfragen beantworten
Es spielt keine Rolle, wie viele Ressourcen Sie zwischenspeichern, der Mitarbeiter wird sie erst verwenden, wenn Sie ihm mitteilen, wann und was er verwenden soll. Hier sind einige Vorlagen für die Bearbeitung von Anfragen.
Nur Bargeld
Geeignet für statische Ressourcen der aktuellen Version der Seite. Sie müssen diese Ressourcen während der Worker-Setup-Phase zwischenspeichern, um sie als Antwort auf Anforderungen senden zu können.
self.addEventListener('fetch', event => {
// ,
//
event.respondWith(caches.match(event.request))
})
Nur Netzwerk
Geeignet für Ressourcen, die nicht zwischengespeichert werden können, z. B. Analysedaten oder Nicht-GET-Anforderungen.
self.addEventListener('fetch', event => {
event.respondWith(fetch(event.request))
// event.respondWith
//
})
Erst der Cache, dann bei einem Ausfall das Netzwerk
Geeignet für die Bearbeitung der meisten Anfragen in Offline-Anwendungen.
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
)
})
Gespeicherte Ressourcen werden aus dem Cache zurückgegeben, nicht gespeicherte Ressourcen aus dem Netzwerk.
Wer Zeit hatte, aß er
Geeignet für kleine Ressourcen, um eine bessere Leistung für Geräte mit geringem Speicherplatz zu erzielen.
Durch die Kombination einer alten Festplatte, eines Virenschutzprogramms und einer schnellen Internetverbindung kann das Abrufen von Daten aus dem Netzwerk schneller erfolgen als das Abrufen von Daten aus dem Cache. Das Abrufen von Daten aus dem Netzwerk, während die Daten auf dem Gerät des Benutzers gespeichert sind, ist jedoch eine Verschwendung von Ressourcen.
// Promise.race ,
// .
//
const promiseAny = promises => new Promise((resolve, reject) => {
// promises
promises = promises.map(p => Promise.resolve(p))
// ,
promises.forEach(p => p.then(resolve))
// ,
promises.reduce((a, b) => a.catch(() => b))
.catch(() => reject(Error(' ')))
})
self.addEventListener('fetch', event => {
event.respondWith(
promiseAny([
caches.match(event.request),
fetch(event.request)
])
)
})
Ca. Lane: Jetzt können Sie Promise.allSettled für diesen Zweck verwenden, aber die Browserunterstützung beträgt 80%: -20% der Benutzer sind wahrscheinlich zu viel.
Netzwerk zuerst, dann bei Ausfall Cache
Geeignet für Ressourcen, die häufig aktualisiert werden und die aktuelle Version der Website nicht beeinflussen, z. B. Artikel, Avatare, Newsfeeds in sozialen Netzwerken, Spielerbewertungen usw.
Dies bedeutet, dass Sie Online-Benutzern neue Inhalte und Offline-Benutzern alte Inhalte bereitstellen. Wenn die Anforderung einer Ressource aus dem Netzwerk erfolgreich ist, sollte der Cache wahrscheinlich aktualisiert werden.
Dieser Ansatz hat einen Nachteil. Wenn der Benutzer Verbindungsprobleme hat oder langsam ist, muss er warten, bis die Anforderung abgeschlossen ist oder fehlschlägt, anstatt sofort Inhalte aus dem Cache abzurufen. Diese Wartezeit kann sehr lang sein, was zu einer schrecklichen Benutzererfahrung führt.
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request).catch(() => caches.match(event.request))
)
})
Erst der Cache, dann das Netzwerk
Geeignet für häufig aktualisierte Ressourcen.
Dazu muss die Seite zwei Anforderungen senden, eine für den Cache und eine für das Netzwerk. Die Idee ist, Daten aus dem Cache zurückzugeben und sie dann zu aktualisieren, wenn Daten aus dem Netzwerk empfangen werden.
Manchmal können Sie die aktuellen Daten ersetzen, wenn Sie neue erhalten (z. B. die Bewertung der Spieler). Dies ist jedoch bei großen Inhalten problematisch. Dies kann dazu führen, dass das, was der Benutzer gerade liest oder mit dem er interagiert, verschwindet.
Twitter fügt neue Inhalte über vorhandene Inhalte hinzu, während das Scrollen beibehalten wird: Der Benutzer sieht oben auf dem Bildschirm eine Benachrichtigung über neue Tweets. Dies ist dank der linearen Reihenfolge des Inhalts möglich. Ich habe diese Vorlage kopiert, um Inhalte aus dem Cache so schnell wie möglich anzuzeigen und neue Inhalte hinzuzufügen, die aus dem Web stammen.
Code auf Seite:
const networkDataReceived = false
startSpinner()
//
const networkUpdate = fetch('/data.json')
.then(response => response.json())
.then(data => {
networkDataReceived = true
updatePage(data)
})
//
caches.match('/data.json')
.then(response => {
if (!response) throw Error(' ')
return response.json()
}).then(data => {
//
if (!networkDataReceived) {
updatePage(data)
}
}).catch(() => {
// , -
return networkUpdate
}).catch(showErrorMessage).then(stopSpinner)
Worker Code:
Wir greifen auf das Netzwerk zu und aktualisieren den Cache.
self.addEventListener('fetch', event => {
event.respondWith(
caches.open('mysite-dynamic')
.then(cache => fetch(event.request)
.then(response => {
cache.put(event.request, response.clone())
return response
}))
)
})
Sicherheitsnetz
Wenn Versuche, die Ressource aus dem Cache und dem Netzwerk abzurufen, fehlschlagen, muss ein Fallback vorliegen.
Geeignet für Platzhalter (Ersetzen von Bildern durch Dummy), fehlgeschlagene POST-Anforderungen, Seiten "Nicht verfügbar, wenn offline".
self.addEventListener('fetch', event => {
event.respondWith(
//
// ,
caches.match(event.request)
.then(response => response || fetch(event.request))
.catch(() => {
// ,
return caches.match('/offline.html')
//
// URL
})
)
})
Wenn Ihre Seite eine E-Mail sendet, kann der Mitarbeiter diese vor dem Senden in einer indizierten Datenbank speichern und die Seite darüber informieren, dass die Übermittlung fehlgeschlagen ist, die E-Mail jedoch gespeichert wurde.
Markup auf der Arbeiterseite erstellen
Geeignet für Seiten, die auf der Serverseite gerendert werden und nicht zwischengespeichert werden können.
Das serverseitige Rendern von Seiten ist ein sehr schneller Prozess, macht das Speichern dynamischer Inhalte im Cache jedoch sinnlos, da es für jedes Rendern unterschiedlich sein kann. Wenn Ihre Seite von einem Mitarbeiter gesteuert wird, können Sie Ressourcen anfordern und die Seite genau dort rendern.
import './templating-engine.js'
self.addEventListener('fetch', event => {
const requestURL = new URL(event.request.url)
event.respondWith(
Promise.all([
caches.match('/article-template.html')
.then(response => response.text()),
caches.match(`${requestURL.path}.json`)
.then(response => response.json())
]).then(responses => {
const template = responses[0]
const data = responses[1]
return new Response(renderTemplate(template, data), {
headers: {
'Content-Type': 'text/html'
}
})
})
)
})
Alle zusammen
Sie müssen nicht auf eine Vorlage beschränkt sein. Sie müssen sie höchstwahrscheinlich je nach Anforderung kombinieren. Zum Beispiel verwendet Training to Thrill Folgendes:
- Worker-Setup-Caching für persistente UI-Elemente
- Zwischenspeichern der Serverantwort für Flickr-Bilder und -Daten
- Abrufen von Daten aus dem Cache und bei einem Fehler aus dem Netzwerk für die meisten Anforderungen
- Abrufen von Ressourcen aus dem Cache und anschließend aus dem Web für Flick-Suchergebnisse
Schauen Sie sich einfach die Anfrage an und entscheiden Sie, was Sie damit machen möchten:
self.addEventListener('fetch', event => {
// URL
const requestURL = new URL(event.request.url)
//
if (requestURL.hostname === 'api.example.com') {
event.respondWith(/* */)
return
}
//
if (requestURL.origin === location.origin) {
//
if (/^\/article\//.test(requestURL.pathname)) {
event.respondWith(/* */)
return
}
if (/\.webp$/.test(requestURL.pathname)) {
event.respondWith(/* */)
return
}
if (request.method == 'POST') {
event.respondWith(/* */)
return
}
if (/cheese/.test(requestURL.pathname)) {
event.respondWith(
// . .: - ?
new Response('Flagrant cheese error', {
//
status: 512
})
)
return
}
}
//
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
)
})
Ich hoffe, dieser Artikel hat Ihnen geholfen. Danke für die Aufmerksamkeit.