Lass uns über Architektur reden.
Ich werde ein wichtiges, aber möglicherweise missverstandenes Thema behandeln: die Architektur, die Sie für Ihre Webanwendung verwenden, und insbesondere, wie sich Ihre Architekturentscheidungen beim Erstellen einer progressiven Webanwendung auswirken.
„Architektur“ kann vage klingen und es ist vielleicht nicht sofort klar, warum das wichtig ist. Eine Möglichkeit, über die Architektur nachzudenken, besteht darin, sich die folgenden Fragen zu stellen: Welcher HTML-Code wird geladen, wenn ein Nutzer eine Seite auf meiner Website besucht? Und was wird geladen, wenn sie eine andere Seite besuchen?
Die Antworten auf diese Fragen sind nicht immer einfach und werden noch komplizierter, wenn Sie über Progressive Web-Apps nachdenken. Mein Ziel ist es, Ihnen eine mögliche Architektur vorzustellen, die ich für effektiv halte. In diesem Artikel bezeichne ich die von mir getroffenen Entscheidungen als „meinen Ansatz“ für die Erstellung einer progressiven Web-App.
Sie können meinen Ansatz beim Erstellen Ihrer eigenen PWA verwenden, aber es gibt auch andere gültige Alternativen. Ich hoffe, dass Sie sich davon inspirieren lassen, wie alle Teile zusammenpassen, und dass Sie sich in der Lage fühlen, dies an Ihre Bedürfnisse anzupassen.
Stack Overflow-PWA
Passend zu diesem Artikel habe ich eine Stack Overflow-PWA erstellt. Ich verbringe viel Zeit damit, Stack Overflow zu lesen und Beiträge zu verfassen. Ich wollte eine Webanwendung erstellen, mit der sich häufig gestellte Fragen zu einem bestimmten Thema leicht finden lassen. Sie basiert auf der öffentlichen Stack Exchange API. Es ist Open Source. Weitere Informationen finden Sie im GitHub-Projekt.
Mehrseitige Apps (MPAs)
Bevor ich auf die Details eingehe, möchte ich einige Begriffe definieren und Teile der zugrunde liegenden Technologie erklären. Zuerst geht es um das, was ich „Multi-Page-Apps“ oder „MPAs“ nenne.
MPA ist ein schicker Name für die traditionelle Architektur, die seit den Anfängen des Webs verwendet wird. Jedes Mal, wenn ein Nutzer eine neue URL aufruft, rendert der Browser schrittweise den HTML-Code für diese Seite. Es wird nicht versucht, den Status der Seite oder den Inhalt zwischen den Navigationsvorgängen beizubehalten. Jedes Mal, wenn Sie eine neue Seite aufrufen, fangen Sie von vorn an.
Im Gegensatz dazu steht das Modell der Single-Page-App (SPA) zum Erstellen von Web-Apps. Hier führt der Browser JavaScript-Code aus, um die vorhandene Seite zu aktualisieren, wenn der Nutzer einen neuen Bereich besucht. Sowohl SPAs als auch MPAs sind gleichermaßen gültige Modelle. In diesem Beitrag wollte ich jedoch PWA-Konzepte im Kontext einer mehrseitigen App untersuchen.
Zuverlässig und schnell
Sie haben mich (und unzählige andere) den Begriff „Progressive Web-App“ oder „PWA“ verwenden hören. Möglicherweise sind Sie bereits mit einigen der Hintergrundinformationen vertraut, die Sie an anderer Stelle auf dieser Website finden.
Eine PWA ist eine Web-App, die eine erstklassige Nutzererfahrung bietet und sich einen Platz auf dem Startbildschirm des Nutzers verdient. Das Akronym FIRE steht für Fast (schnell), Integrated (integriert), Reliable (zuverlässig) und Engaging (ansprechend) und fasst alle Attribute zusammen, die beim Erstellen einer PWA berücksichtigt werden sollten.
In diesem Artikel konzentriere ich mich auf zwei dieser Attribute: Schnell und Zuverlässig.
Schnell: „Schnell“ kann in verschiedenen Kontexten unterschiedliche Bedeutungen haben. Ich werde hier auf die Geschwindigkeitsvorteile eingehen, die sich ergeben, wenn so wenig wie möglich aus dem Netzwerk geladen wird.
Zuverlässig: Aber Geschwindigkeit allein reicht nicht. Damit sich Ihre Web-App wie eine PWA anfühlt, sollte sie zuverlässig sein. Sie muss so stabil sein, dass sie immer etwas lädt, auch wenn es nur eine benutzerdefinierte Fehlerseite ist, unabhängig vom Zustand des Netzwerks.
Zuverlässig schnell: Zum Schluss möchte ich die Definition von PWAs etwas umformulieren und mir ansehen, was es bedeutet, etwas zu entwickeln, das zuverlässig schnell ist. Es reicht nicht aus, nur dann schnell und zuverlässig zu sein, wenn Sie sich in einem Netzwerk mit niedriger Latenz befinden. Zuverlässig schnell bedeutet, dass die Geschwindigkeit Ihrer Webanwendung unabhängig von den zugrunde liegenden Netzwerkbedingungen konsistent ist.
Ermöglichende Technologien: Service Worker + Cache Storage API
PWAs setzen hohe Maßstäbe für Geschwindigkeit und Ausfallsicherheit. Glücklicherweise bietet die Webplattform einige Bausteine, um diese Art von Leistung zu ermöglichen. Ich meine damit Service Worker und die Cache Storage API.
Sie können einen Service Worker erstellen, der auf eingehende Anfragen wartet, einige davon an das Netzwerk weiterleitet und eine Kopie der Antwort über die Cache Storage API für die zukünftige Verwendung speichert.
Wenn die Web-App das nächste Mal dieselbe Anfrage stellt, kann ihr Service Worker die Caches prüfen und einfach die zuvor im Cache gespeicherte Antwort zurückgeben.
Das Netzwerk nach Möglichkeit zu vermeiden, ist ein entscheidender Teil einer zuverlässig schnellen Leistung.
„Isomorphes“ JavaScript
Ein weiteres Konzept, das ich behandeln möchte, wird manchmal als „isomorphes“ oder „universelles“ JavaScript bezeichnet. Einfach ausgedrückt geht es darum, dass derselbe JavaScript-Code von verschiedenen Laufzeitumgebungen gemeinsam genutzt werden kann. Als ich meine PWA erstellt habe, wollte ich JavaScript-Code zwischen meinem Back-End-Server und dem Service Worker teilen.
Es gibt viele gültige Ansätze, um Code auf diese Weise zu teilen, aber mein Ansatz war, ES-Module als endgültigen Quellcode zu verwenden. Anschließend habe ich diese Module für den Server und den Service Worker mit einer Kombination aus Babel und Rollup transpiliert und gebündelt. In meinem Projekt sind Dateien mit der Dateiendung .mjs Code, der sich in einem ES-Modul befindet.
Der Server
Mit diesen Konzepten und Begriffen im Hinterkopf sehen wir uns nun an, wie ich meine Stack Overflow-PWA erstellt habe. Ich beginne mit dem Backend-Server und erkläre, wie er in die Gesamtarchitektur passt.
Ich suchte nach einer Kombination aus einem dynamischen Backend und statischem Hosting und mein Ansatz war, die Firebase-Plattform zu verwenden.
Firebase Cloud Functions startet automatisch eine Node-basierte Umgebung, wenn eine Anfrage eingeht, und lässt sich in das beliebte Express HTTP-Framework einbinden, mit dem ich bereits vertraut war. Außerdem bietet es sofort einsatzbereites Hosting für alle statischen Ressourcen meiner Website. Sehen wir uns an, wie der Server Anfragen verarbeitet.
Wenn ein Browser eine Navigationsanfrage an unseren Server sendet, durchläuft er den folgenden Ablauf:
Der Server leitet die Anfrage anhand der URL weiter und verwendet eine Vorlagenlogik, um ein vollständiges HTML-Dokument zu erstellen. Ich verwende eine Kombination aus Daten der Stack Exchange API und partiellen HTML-Fragmenten, die der Server lokal speichert. Sobald der Service Worker weiß, wie er antworten soll, kann er HTML-Inhalte an die Web-App streamen.
Zwei Teile dieses Bildes sind es wert, genauer untersucht zu werden: Routing und Vorlagen.
Routing
Für das Routing habe ich die native Routing-Syntax des Express-Frameworks verwendet. Sie ist flexibel genug, um einfache URL-Präfixe sowie URLs mit Parametern im Pfad abzugleichen. Hier erstelle ich eine Zuordnung zwischen Routennamen und dem zugrunde liegenden Express-Muster, mit dem sie abgeglichen werden sollen.
const routes = new Map([
['about', '/about'],
['questions', '/questions/:questionId'],
['index', '/'],
]);
export default routes;
Ich kann dann direkt im Code des Servers auf diese Zuordnung verweisen. Wenn ein bestimmtes Express-Muster übereinstimmt, antwortet der entsprechende Handler mit einer Vorlagenlogik, die für die übereinstimmende Route spezifisch ist.
import routes from './lib/routes.mjs';
app.get(routes.get('index'), async (req, res) => {
// Templating logic.
});
Serverseitige Vorlagen
Und wie sieht diese Vorlagenlogik aus? Ich habe einen Ansatz gewählt, bei dem HTML-Fragmente nacheinander zusammengesetzt werden. Dieses Modell eignet sich gut für Streaming.
Der Server sendet sofort einen ersten HTML-Boilerplate zurück und der Browser kann diese Teilseite sofort rendern. Während der Server die restlichen Datenquellen zusammenstellt, streamt er sie an den Browser, bis das Dokument vollständig ist.
Sehen Sie sich den Express-Code für eine unserer Routen an:
app.get(routes.get('index'), async (req, res) => {
res.write(headPartial + navbarPartial);
const tag = req.query.tag || DEFAULT_TAG;
const data = await requestData(...);
res.write(templates.index(tag, data.items));
res.write(footPartial);
res.end();
});
Mit der write()-Methode des response-Objekts und durch Verweis auf lokal gespeicherte Teilvorlagen kann ich den Antwortstream sofort starten, ohne externe Datenquellen zu blockieren. Der Browser nimmt dieses anfängliche HTML und rendert sofort eine aussagekräftige Benutzeroberfläche und eine Lade-Nachricht.
Der nächste Teil unserer Seite verwendet Daten aus der Stack Exchange API. Um diese Daten zu erhalten, muss unser Server eine Netzwerkanfrage stellen. Die Web-App kann nichts anderes rendern, bis sie eine Antwort erhält und verarbeitet, aber zumindest starren die Nutzer nicht auf einen leeren Bildschirm, während sie warten.
Sobald die Webanwendung die Antwort von der Stack Exchange API erhalten hat, ruft sie eine benutzerdefinierte Vorlagenfunktion auf, um die Daten aus der API in das entsprechende HTML zu übersetzen.
Vorlagensprache
Vorlagen können ein überraschend umstrittenes Thema sein. Meine Wahl ist nur ein Ansatz von vielen. Sie sollten Ihre eigene Lösung verwenden, insbesondere wenn Sie bereits ein bestehendes Vorlagen-Framework nutzen.
Für meinen Anwendungsfall war es sinnvoll, mich einfach auf die Vorlagenliterale von JavaScript zu verlassen, wobei ein Teil der Logik in Hilfsfunktionen ausgelagert wurde. Einer der Vorteile beim Erstellen einer MPA ist, dass Sie keine Statusaktualisierungen im Auge behalten und Ihren HTML-Code nicht neu rendern müssen. Ein einfacher Ansatz, der statischen HTML-Code erzeugt, hat für mich also funktioniert.
Hier ist ein Beispiel dafür, wie ich den dynamischen HTML-Teil des Index meiner Webanwendung als Vorlage erstelle. Wie bei meinen Routen wird die Vorlagenlogik in einem ES-Modul gespeichert, das sowohl in den Server als auch in den Service Worker importiert werden kann.
export function index(tag, items) {
const title = `<h3>Top "${escape(tag)}" Questions</h3>`;
const form = `<form method="GET">...</form>`;
const questionCards = items
.map(item =>
questionCard({
id: item.question_id,
title: item.title,
})
)
.join('');
const questions = `<div id="questions">${questionCards}</div>`;
return title + form + questions;
}
Diese Vorlagenfunktionen sind reines JavaScript. Es ist sinnvoll, die Logik bei Bedarf in kleinere Hilfsfunktionen aufzuteilen. Hier übergebe ich jedes der in der API-Antwort zurückgegebenen Elemente an eine solche Funktion, die ein Standard-HTML-Element mit allen entsprechenden Attributen erstellt.
function questionCard({id, title}) {
return `<a class="card"
href="/questions/${id}"
data-cache-url="${questionUrl(id)}">${title}</a>`;
}
Besonders erwähnenswert ist ein Datenattribut, das ich jedem Link hinzufüge, data-cache-url, und das auf die Stack Exchange API-URL festgelegt ist, die ich benötige, um die entsprechende Frage anzuzeigen. Beachten Sie dies. Ich schaue später noch einmal vorbei.
Zurück zu meinem Routen-Handler: Sobald die Vorlage fertig ist, streame ich den letzten Teil des HTML-Codes meiner Seite an den Browser und beende den Stream. Dies ist der Hinweis für den Browser, dass das progressive Rendering abgeschlossen ist.
app.get(routes.get('index'), async (req, res) => {
res.write(headPartial + navbarPartial);
const tag = req.query.tag || DEFAULT_TAG;
const data = await requestData(...);
res.write(templates.index(tag, data.items));
res.write(footPartial);
res.end();
});
Das war ein kurzer Überblick über meine Servereinrichtung. Nutzer, die meine Web-App zum ersten Mal besuchen, erhalten immer eine Antwort vom Server. Wenn ein Besucher jedoch zu meiner Web-App zurückkehrt, antwortet mein Service Worker. Sehen wir uns das genauer an.
Der Service Worker
Dieses Diagramm sollte Ihnen bekannt vorkommen. Viele der zuvor behandelten Elemente sind hier in einer etwas anderen Anordnung zu sehen. Sehen wir uns den Anfrageablauf unter Berücksichtigung des Service Workers an.
Unser Service Worker verarbeitet eine eingehende Navigationsanfrage für eine bestimmte URL und verwendet genau wie mein Server eine Kombination aus Routing- und Vorlagenlogik, um herauszufinden, wie er antworten soll.
Der Ansatz ist derselbe wie zuvor, aber mit anderen Low-Level-Primitiven wie fetch() und der Cache Storage API. Ich verwende diese Datenquellen, um die HTML-Antwort zu erstellen, die der Service Worker an die Web-App zurückgibt.
Workbox
Anstatt mit einfachen Primitiven von vorn anzufangen, erstelle ich meinen Service Worker auf einer Reihe von übergeordneten Bibliotheken namens Workbox. Sie bietet eine solide Grundlage für die Caching-, Routing- und Antwortgenerierungslogik von Service Workern.
Routing
Genau wie mein serverseitiger Code muss auch mein Service Worker wissen, wie eine eingehende Anfrage mit der entsprechenden Antwortlogik abgeglichen wird.
Mein Ansatz bestand darin, jede Express-Route in einen entsprechenden regulären Ausdruck zu übersetzen. Dabei habe ich eine hilfreiche Bibliothek namens regexparam verwendet. Nachdem diese Übersetzung durchgeführt wurde, kann ich die integrierte Unterstützung von Workbox für das Routing mit regulären Ausdrücken nutzen.
Nachdem ich das Modul mit den regulären Ausdrücken importiert habe, registriere ich jeden regulären Ausdruck beim Router von Workbox. In jeder Route kann ich eine benutzerdefinierte Vorlagenlogik bereitstellen, um eine Antwort zu generieren. Die Vorlagenerstellung im Service Worker ist etwas aufwendiger als auf meinem Backend-Server, aber Workbox hilft bei vielen der schwierigen Aufgaben.
import regExpRoutes from './regexp-routes.mjs';
workbox.routing.registerRoute(
regExpRoutes.get('index')
// Templating logic.
);
Caching statischer Assets
Ein wichtiger Teil der Vorlagen ist, dass meine partiellen HTML-Vorlagen lokal über die Cache Storage API verfügbar sind und auf dem neuesten Stand gehalten werden, wenn ich Änderungen an der Web-App bereitstelle. Die Cache-Wartung kann fehleranfällig sein, wenn sie manuell durchgeführt wird. Daher verwende ich Workbox, um das Pre-Caching im Rahmen meines Build-Prozesses zu verwalten.
Ich teile Workbox über eine Konfigurationsdatei mit, welche URLs vorab im Cache gespeichert werden sollen. Diese Datei verweist auf das Verzeichnis, das alle meine lokalen Assets enthält, sowie auf eine Reihe von Mustern, die übereinstimmen müssen. Diese Datei wird automatisch von der Workbox-Befehlszeile gelesen, die bei jedem Neuerstellen der Website ausgeführt wird.
module.exports = {
globDirectory: 'build',
globPatterns: ['**/*.{html,js,svg}'],
// Other options...
};
Workbox erstellt einen Snapshot des Inhalts jeder Datei und fügt diese Liste von URLs und Überarbeitungen automatisch in meine endgültige Service Worker-Datei ein. Workbox hat jetzt alles, was es braucht, um die im Vorab-Cache gespeicherten Dateien immer verfügbar und auf dem neuesten Stand zu halten. Das Ergebnis ist eine service-worker.js-Datei, die in etwa Folgendes enthält:
workbox.precaching.precacheAndRoute([
{
url: 'partials/about.html',
revision: '518747aad9d7e',
},
{
url: 'partials/foot.html',
revision: '69bf746a9ecc6',
},
// etc.
]);
Für Nutzer mit einem komplexeren Build-Prozess bietet Workbox neben der Befehlszeile auch ein webpack-Plug-in und ein generisches Knotenmodul.
Streaming
Als Nächstes soll der Service Worker diesen im Cache gespeicherten Teil-HTML-Code sofort an die Web-App zurückstreamen. Das ist ein entscheidender Teil von „zuverlässig schnell“ – ich bekomme immer sofort etwas Sinnvolles auf dem Bildschirm angezeigt. Glücklicherweise ist das mit der Streams API in unserem Service Worker möglich.
Vielleicht haben Sie schon von der Streams API gehört. Mein Kollege Jake Archibald schwärmt schon seit Jahren davon. Er stellte die kühne Prognose auf, dass 2016 das Jahr der Webstreams sein würde. Die Streams API ist heute genauso großartig wie vor zwei Jahren, aber mit einem entscheidenden Unterschied.
Damals wurde Streams nur von Chrome unterstützt, aber inzwischen ist die Streams API weiter verbreitet. Insgesamt ist die Entwicklung positiv und mit dem entsprechenden Fallback-Code steht der Verwendung von Streams in Ihrem Service Worker nichts mehr im Wege.
Es gibt jedoch eine Sache, die Sie davon abhalten könnte: Sie müssen verstehen, wie die Streams API tatsächlich funktioniert. Es stellt eine sehr leistungsstarke Reihe von Primitiven bereit. Entwickler, die sich mit der Verwendung auskennen, können komplexe Datenflüsse wie den folgenden erstellen:
const stream = new ReadableStream({
pull(controller) {
return sources[0]
.then(r => r.read())
.then(result => {
if (result.done) {
sources.shift();
if (sources.length === 0) return controller.close();
return this.pull(controller);
} else {
controller.enqueue(result.value);
}
});
},
});
Aber nicht jeder versteht die vollen Auswirkungen dieses Codes. Anstatt diese Logik zu analysieren, möchte ich über meinen Ansatz für das Service Worker-Streaming sprechen.
Ich verwende einen brandneuen, übergeordneten Wrapper, workbox-streams.
Damit kann ich eine Mischung aus Streamingquellen übergeben, sowohl aus Caches als auch aus Laufzeitdaten, die aus dem Netzwerk stammen können. Workbox übernimmt die Koordination der einzelnen Quellen und fügt sie zu einer einzigen Streaming-Antwort zusammen.
Außerdem erkennt Workbox automatisch, ob die Streams API unterstützt wird. Wenn nicht, wird eine entsprechende Nicht-Streaming-Antwort erstellt. Sie müssen sich also keine Gedanken über das Schreiben von Fallbacks machen, da Streams immer näher an eine 100%ige Browserunterstützung herankommen.
Laufzeit-Caching
Sehen wir uns an, wie mein Service Worker mit Laufzeitdaten aus der Stack Exchange API umgeht. Ich verwende die integrierte Unterstützung von Workbox für eine Caching-Strategie vom Typ „stale-while-revalidate“ sowie den Ablauf, um sicherzustellen, dass der Speicher der Web-App nicht unbegrenzt wächst.
Ich habe in Workbox zwei Strategien eingerichtet, um die verschiedenen Quellen zu verarbeiten, aus denen die Streamingantwort besteht. Mit nur wenigen Funktionsaufrufen und Konfigurationen können wir mit Workbox das tun, was sonst Hunderte von Zeilen handgeschriebenen Codes erfordern würde.
const cacheStrategy = workbox.strategies.cacheFirst({
cacheName: workbox.core.cacheNames.precache,
});
const apiStrategy = workbox.strategies.staleWhileRevalidate({
cacheName: API_CACHE_NAME,
plugins: [new workbox.expiration.Plugin({maxEntries: 50})],
});
Die erste Strategie liest Daten, die vorab im Cache gespeichert wurden, z. B. unsere partiellen HTML-Vorlagen.
Die andere Strategie implementiert die Caching-Logik „stale-while-revalidate“ und die Cache-Ablaufzeit „least-recently-used“, sobald 50 Einträge erreicht sind.
Nachdem ich diese Strategien festgelegt habe, muss ich Workbox nur noch mitteilen, wie sie verwendet werden sollen, um eine vollständige Streaming-Antwort zu erstellen. Ich übergebe ein Array von Quellen als Funktionen und jede dieser Funktionen wird sofort ausgeführt. Workbox nimmt das Ergebnis aus jeder Quelle und streamt es nacheinander an die Web-App. Es kommt nur dann zu einer Verzögerung, wenn die nächste Funktion im Array noch nicht abgeschlossen ist.
workbox.streams.strategy([
() => cacheStrategy.makeRequest({request: '/head.html'}),
() => cacheStrategy.makeRequest({request: '/navbar.html'}),
async ({event, url}) => {
const tag = url.searchParams.get('tag') || DEFAULT_TAG;
const listResponse = await apiStrategy.makeRequest(...);
const data = await listResponse.json();
return templates.index(tag, data.items);
},
() => cacheStrategy.makeRequest({request: '/foot.html'}),
]);
Die ersten beiden Quellen sind vorab im Cache gespeicherte Teilvorlagen, die direkt aus der Cache Storage API gelesen werden. Sie sind also immer sofort verfügbar. So wird sichergestellt, dass unsere Service Worker-Implementierung genauso schnell auf Anfragen reagiert wie mein serverseitiger Code.
Die nächste Quellfunktion ruft Daten von der Stack Exchange API ab und verarbeitet die Antwort in das HTML-Format, das die Webanwendung erwartet.
Die Strategie „stale-while-revalidate“ bedeutet, dass ich, wenn ich eine zuvor im Cache gespeicherte Antwort für diesen API-Aufruf habe, diese sofort an die Seite streamen kann, während der Cache-Eintrag „im Hintergrund“ für die nächste Anfrage aktualisiert wird.
Schließlich streame ich eine im Cache gespeicherte Kopie meiner Fußzeile und schließe die letzten HTML-Tags, um die Antwort abzuschließen.
Durch die gemeinsame Nutzung von Code bleibt alles synchron
Bestimmte Teile des Service Worker-Codes kommen Ihnen vielleicht bekannt vor. Die von meinem Service Worker verwendete partielle HTML- und Vorlagenlogik ist mit der Logik des serverseitigen Handlers identisch. Durch diese gemeinsame Nutzung von Code erhalten Nutzer eine einheitliche Umgebung, unabhängig davon, ob sie meine Web-App zum ersten Mal besuchen oder zu einer Seite zurückkehren, die vom Service Worker gerendert wurde. Das ist das Schöne an isomorphem JavaScript.
Dynamische, progressive Verbesserungen
Ich habe sowohl den Server als auch den Service Worker für meine PWA durchlaufen, aber es gibt noch einen letzten Teil der Logik, den ich behandeln muss: Es gibt eine kleine Menge JavaScript, die auf jeder meiner Seiten ausgeführt wird, nachdem sie vollständig gestreamt wurden.
Dieser Code verbessert die Nutzererfahrung schrittweise, ist aber nicht entscheidend. Die Web-App funktioniert auch, wenn er nicht ausgeführt wird.
Metadaten für Seiten
Meine App verwendet clientseitiges JavaScript, um die Metadaten einer Seite basierend auf der API-Antwort zu aktualisieren. Da ich für jede Seite denselben anfänglichen Teil des im Cache gespeicherten HTML-Codes verwende, enthält die Web-App am Ende generische Tags im Head-Bereich meines Dokuments. Durch die Abstimmung zwischen meinem Vorlagen- und clientseitigen Code kann ich den Titel des Fensters jedoch mithilfe von seitenspezifischen Metadaten aktualisieren.
Im Rahmen des Vorlagencodes füge ich ein Script-Tag mit dem korrekt maskierten String ein.
const metadataScript = `<script>
self._title = '${escape(item.title)}';
</script>`;
Sobald die Seite geladen ist, lese ich diesen String und aktualisiere den Dokumenttitel.
if (self._title) {
document.title = unescape(self._title);
}
Wenn du andere seitenspezifische Metadaten in deiner eigenen Web-App aktualisieren möchtest, kannst du genauso vorgehen.
Offline-UX
Die andere progressive Verbesserung, die ich hinzugefügt habe, soll die Aufmerksamkeit auf unsere Offline-Funktionen lenken. Ich habe eine zuverlässige PWA erstellt und möchte, dass Nutzer wissen, dass sie auch offline zuvor besuchte Seiten laden können.
Zuerst verwende ich die Cache Storage API, um eine Liste aller zuvor im Cache gespeicherten API-Anfragen abzurufen, und übersetze diese in eine Liste von URLs.
Erinnern Sie sich an die speziellen Datenattribute, über die ich gesprochen habe? Jedes enthält die URL für die API-Anfrage, die zum Anzeigen einer Frage erforderlich ist. Ich kann diese Datenattribute mit der Liste der im Cache gespeicherten URLs abgleichen und ein Array aller Fragenlinks erstellen, die nicht übereinstimmen.
Wenn der Browser in den Offlinemodus wechselt, gehe ich die Liste der nicht im Cache gespeicherten Links durch und blende die Links aus, die nicht funktionieren. Beachten Sie, dass dies nur ein visueller Hinweis für den Nutzer ist, was ihn auf diesen Seiten erwartet. Ich deaktiviere die Links nicht und hindere den Nutzer auch nicht daran, zu navigieren.
const apiCache = await caches.open(API_CACHE_NAME);
const cachedRequests = await apiCache.keys();
const cachedUrls = cachedRequests.map(request => request.url);
const cards = document.querySelectorAll('.card');
const uncachedCards = [...cards].filter(card => {
return !cachedUrls.includes(card.dataset.cacheUrl);
});
const offlineHandler = () => {
for (const uncachedCard of uncachedCards) {
uncachedCard.style.opacity = '0.3';
}
};
const onlineHandler = () => {
for (const uncachedCard of uncachedCards) {
uncachedCard.style.opacity = '1.0';
}
};
window.addEventListener('online', onlineHandler);
window.addEventListener('offline', offlineHandler);
Häufige Schwierigkeiten
Ich habe Ihnen nun meinen Ansatz zum Erstellen einer mehrseitigen PWA vorgestellt. Es gibt viele Faktoren, die Sie bei der Entwicklung Ihres eigenen Ansatzes berücksichtigen müssen, und Sie werden möglicherweise andere Entscheidungen treffen als ich. Diese Flexibilität ist einer der großen Vorteile bei der Entwicklung für das Web.
Es gibt einige häufige Fallstricke, auf die Sie bei Ihren eigenen Architekturentscheidungen stoßen können. Ich möchte Ihnen einige Probleme ersparen.
Vollständigen HTML-Code nicht im Cache speichern
Ich empfehle, keine vollständigen HTML-Dokumente im Cache zu speichern. Zum einen ist es eine Platzverschwendung. Wenn Ihre Webanwendung für jede ihrer Seiten dieselbe grundlegende HTML-Struktur verwendet, werden immer wieder Kopien desselben Markups gespeichert.
Wenn ihr eine Änderung an der gemeinsamen HTML-Struktur eurer Website vornehmt, bleibt das alte Layout auf allen zuvor im Cache gespeicherten Seiten erhalten. Stell dir vor, wie frustrierend es für wiederkehrende Besucher ist, wenn sie eine Mischung aus alten und neuen Seiten sehen.
Server-/Service Worker-Drift
Eine weitere Falle, die es zu vermeiden gilt, ist, dass Ihr Server und Ihr Service Worker nicht mehr synchron sind. Mein Ansatz war, isomorphes JavaScript zu verwenden, damit derselbe Code an beiden Orten ausgeführt wird. Je nach vorhandener Serverarchitektur ist das nicht immer möglich.
Unabhängig von Ihren architektonischen Entscheidungen sollten Sie eine Strategie haben, um den entsprechenden Routing- und Vorlagencode auf Ihrem Server und in Ihrem Service Worker auszuführen.
Worst-Case-Szenarien
Inkonsistentes Layout/Design
Was passiert, wenn Sie diese Fallstricke ignorieren? Es sind alle möglichen Fehler denkbar. Im schlimmsten Fall besucht ein wiederkehrender Nutzer eine im Cache gespeicherte Seite mit einem sehr veralteten Layout, z. B. mit veraltetem Headertext oder mit CSS-Klassennamen, die nicht mehr gültig sind.
Worst-Case-Szenario: Fehlerhaftes Routing
Alternativ kann ein Nutzer auf eine URL stoßen, die von deinem Server, aber nicht von deinem Service Worker verarbeitet wird. Eine Website voller Zombie-Layouts und Sackgassen ist keine zuverlässige PWA.
Tipps für den Erfolg
Aber Sie sind nicht allein. Die folgenden Tipps können Ihnen helfen, diese Fallstricke zu vermeiden:
Vorlagen- und Routingbibliotheken mit mehrsprachigen Implementierungen verwenden
Verwenden Sie Vorlagen- und Routingbibliotheken mit JavaScript-Implementierungen. Nicht alle Entwickler haben die Möglichkeit, von ihrem aktuellen Webserver und ihrer Vorlagensprache zu migrieren.
Für eine Reihe gängiger Vorlagen- und Routing-Frameworks gibt es jedoch Implementierungen in mehreren Sprachen. Wenn Sie eine finden, die sowohl mit JavaScript als auch mit der Sprache Ihres aktuellen Servers funktioniert, sind Sie der Synchronisierung von Service Worker und Server einen Schritt näher.
Sequenzielle statt verschachtelte Vorlagen verwenden
Als Nächstes empfehle ich, eine Reihe von sequenziellen Vorlagen zu verwenden, die nacheinander gestreamt werden können. Es ist in Ordnung, wenn spätere Teile Ihrer Seite eine kompliziertere Vorlagenlogik verwenden, solange Sie den ersten Teil Ihres HTML-Codes so schnell wie möglich streamen können.
Sowohl statische als auch dynamische Inhalte im Service Worker im Cache speichern
Für eine optimale Leistung sollten Sie alle wichtigen statischen Ressourcen Ihrer Website im Voraus im Cache speichern. Außerdem sollten Sie eine Laufzeit-Caching-Logik einrichten, um dynamische Inhalte wie API-Anfragen zu verarbeiten. Mit Workbox können Sie auf bewährte, produktionsreife Strategien zurückgreifen, anstatt alles von Grund auf neu zu implementieren.
Blockieren Sie nur, wenn es unbedingt erforderlich ist.
In diesem Zusammenhang sollten Sie das Netzwerk nur dann blockieren, wenn es nicht möglich ist, eine Antwort aus dem Cache zu streamen. Wenn eine im Cache gespeicherte API-Antwort sofort angezeigt wird, kann das oft zu einer besseren Nutzererfahrung führen, als wenn auf neue Daten gewartet wird.