Laten we het over... architectuur hebben?
Ik ga een belangrijk, maar mogelijk verkeerd begrepen onderwerp bespreken: de architectuur die u voor uw web-app gebruikt en specifiek hoe uw architectuurbeslissingen een rol spelen bij het bouwen van een progressieve web-app.
"Architectuur" klinkt misschien vaag en het is misschien niet meteen duidelijk waarom dit belangrijk is. Een manier om over architectuur na te denken, is door jezelf de volgende vragen te stellen: welke HTML wordt er geladen wanneer een gebruiker een pagina op mijn site bezoekt? En wat wordt er geladen wanneer hij of zij een andere pagina bezoekt?
De antwoorden op die vragen zijn niet altijd even eenvoudig, en zodra je begint na te denken over progressieve webapps, kunnen ze nog ingewikkelder worden. Daarom is het mijn doel om je een mogelijke architectuur te laten zien die ik effectief vond. In dit artikel benoem ik de beslissingen die ik heb genomen als "mijn aanpak" voor het bouwen van een progressieve webapp.
Je kunt mijn aanpak gebruiken bij het bouwen van je eigen PWA, maar er zijn altijd andere goede alternatieven. Ik hoop dat het zien hoe alle stukjes in elkaar passen je inspireert en dat je je gesterkt voelt om deze aan te passen aan jouw behoeften.
Stack Overflow PWA
Ter ondersteuning van dit artikel heb ik een Stack Overflow PWA gebouwd. Ik besteed veel tijd aan het lezen en bijdragen aan Stack Overflow , en ik wilde een webapp bouwen die het gemakkelijk zou maken om veelgestelde vragen over een bepaald onderwerp te bekijken. De app is gebouwd op de openbare Stack Exchange API . Het is open source en je kunt meer informatie vinden op het GitHub-project .
Apps met meerdere pagina's (MPA's)
Voordat ik in detail treed, zullen we eerst een aantal termen definiëren en de onderliggende technologie uitleggen. Eerst ga ik in op wat ik "Multi Page Apps" of "MPA's" noem.
MPA is een mooie naam voor de traditionele architectuur die sinds het begin van het web wordt gebruikt. Telkens wanneer een gebruiker naar een nieuwe URL navigeert, rendert de browser progressief HTML-code die specifiek is voor die pagina. Er wordt niet geprobeerd de status of de inhoud van de pagina tussen navigaties te behouden. Elke keer dat u een nieuwe pagina bezoekt, begint u helemaal opnieuw.
Dit staat in contrast met het single-page app (SPA)-model voor het bouwen van webapps, waarbij de browser JavaScript-code uitvoert om de bestaande pagina bij te werken wanneer de gebruiker een nieuwe sectie bezoekt. Zowel SPA's als MPA's zijn even valide modellen om te gebruiken, maar voor deze blogpost wilde ik PWA-concepten verkennen binnen de context van een multi-page app.
Betrouwbaar snel
Je hebt mij (en talloze anderen) de term "progressive web app" of PWA horen gebruiken. Je bent misschien al bekend met wat achtergrondinformatie elders op deze site .
Je kunt een PWA zien als een webapp die een eersteklas gebruikerservaring biedt en die daadwerkelijk een plek verdient op het startscherm van de gebruiker. De afkorting " FIRE ", die staat voor Snel , Geïntegreerd , Betrouwbaar en Aantrekkelijk , vat alle kenmerken samen waar je rekening mee moet houden bij het bouwen van een PWA.
In dit artikel ga ik in op een deel van deze kenmerken: Snel en betrouwbaar .
Snel: Hoewel "snel" in verschillende contexten verschillende dingen kan betekenen, ga ik het hebben over de snelheidsvoordelen van zo min mogelijk laden van het netwerk.
Betrouwbaar: Maar pure snelheid is niet genoeg. Om als een PWA te voelen, moet je webapp betrouwbaar zijn. Hij moet veerkrachtig genoeg zijn om altijd iets te laden, zelfs als het maar een aangepaste foutpagina is, ongeacht de status van het netwerk.
Betrouwbaar snel: Tot slot ga ik de PWA-definitie iets herformuleren en kijken wat het betekent om iets te bouwen dat betrouwbaar snel is. Het is niet goed genoeg om alleen snel en betrouwbaar te zijn wanneer je op een netwerk met lage latentie zit. Betrouwbaar snel zijn betekent dat de snelheid van je webapp consistent is, ongeacht de onderliggende netwerkomstandigheden.
Ondersteunende technologieën: Service Workers + Cache Storage API
PWA's leggen de lat hoog voor snelheid en veerkracht. Gelukkig biedt het webplatform een aantal bouwstenen om dit soort prestaties te realiseren. Ik heb het dan over service workers en de Cache Storage API .
U kunt een service worker bouwen die luistert naar inkomende verzoeken, een deel hiervan doorstuurt naar het netwerk en een kopie van het antwoord opslaat voor toekomstig gebruik via de Cache Storage API.

De volgende keer dat de web-app hetzelfde verzoek doet, kan de service worker de caches controleren en gewoon het eerder gecachte antwoord retourneren.

Het zoveel mogelijk vermijden van het netwerk is essentieel om betrouwbare, snelle prestaties te kunnen leveren.
"Isomorfe" JavaScript
Nog een concept dat ik wil bespreken, is wat soms "isomorfe" of "universele" JavaScript wordt genoemd. Simpel gezegd is het het idee dat dezelfde JavaScript-code gedeeld kan worden tussen verschillende runtime-omgevingen. Toen ik mijn PWA bouwde, wilde ik JavaScript-code delen tussen mijn back-endserver en de service worker.
Er zijn talloze goede manieren om code op deze manier te delen, maar mijn aanpak was om ES-modules als definitieve broncode te gebruiken. Vervolgens heb ik die modules getranspileerd en gebundeld voor de server en de service worker met behulp van een combinatie van Babel en Rollup . In mijn project zijn bestanden met de extensie .mjs code die zich in een ES-module bevindt.
De server
Met deze concepten en terminologie in gedachten, laten we eens kijken hoe ik mijn Stack Overflow PWA heb gebouwd. Ik begin met het bespreken van onze backendserver en leg uit hoe die past in de algehele architectuur.
Ik zocht naar een combinatie van een dynamische backend met statische hosting en mijn aanpak was om het Firebase-platform te gebruiken.
Firebase Cloud Functions start automatisch een Node-gebaseerde omgeving op wanneer er een verzoek binnenkomt en integreert met het populaire Express HTTP-framework , waar ik al bekend mee was. Het biedt ook kant-en-klare hosting voor alle statische resources van mijn site. Laten we eens kijken hoe de server verzoeken verwerkt.
Wanneer een browser een navigatieverzoek naar onze server stuurt, doorloopt hij de volgende procedure:

De server routeert het verzoek op basis van de URL en gebruikt templatelogica om een volledig HTML-document te maken. Ik gebruik een combinatie van gegevens van de Stack Exchange API en gedeeltelijke HTML-fragmenten die de server lokaal opslaat. Zodra onze service worker weet hoe hij moet reageren, kan hij HTML terugstreamen naar onze webapp.
Er zijn twee onderdelen van dit plaatje die de moeite waard zijn om nader te bekijken: routing en templates.
Routering
Wat betreft routing, gebruikte ik de standaard routingsyntaxis van het Express-framework. Deze is flexibel genoeg om eenvoudige URL-prefixen te matchen, evenals URL's die parameters als onderdeel van het pad bevatten. Hier maak ik een mapping tussen routenamen en het onderliggende Express-patroon dat moet worden gematcht.
const routes = new Map([
['about', '/about'],
['questions', '/questions/:questionId'],
['index', '/'],
]);
export default routes;
Ik kan deze mapping dan rechtstreeks vanuit de code van de server raadplegen. Wanneer er een match is voor een bepaald Express-patroon, reageert de juiste handler met templatelogica die specifiek is voor de overeenkomende route.
import routes from './lib/routes.mjs';
app.get(routes.get('index'), as>ync (req, res) = {
// Templating logic.
});
Server-side sjablonen
En hoe ziet die templatelogica eruit? Nou, ik heb gekozen voor een aanpak waarbij gedeeltelijke HTML-fragmenten achter elkaar worden samengevoegd. Dit model leent zich goed voor streaming.
De server stuurt direct een eerste HTML-boilerplate terug, waarna de browser die gedeeltelijke pagina direct kan weergeven. Terwijl de server de rest van de gegevensbronnen samenvoegt, worden deze naar de browser gestreamd totdat het document compleet is.
Om te zien wat ik bedoel, kijk eens naar de Express-code voor een van onze routes:
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();
});
Door de write() -methode van het response te gebruiken en te verwijzen naar lokaal opgeslagen gedeeltelijke sjablonen, kan ik de responsstroom direct starten, zonder een externe gegevensbron te blokkeren. De browser gebruikt deze initiële HTML en geeft direct een betekenisvolle interface en laadmelding weer.
Het volgende deel van onze pagina gebruikt gegevens van de Stack Exchange API . Om die gegevens te verkrijgen, moet onze server een netwerkverzoek indienen. De webapp kan niets anders weergeven totdat er een antwoord is ontvangen en verwerkt, maar gebruikers hoeven tenminste niet naar een leeg scherm te staren terwijl ze wachten.
Zodra de webapp het antwoord van de Stack Exchange API heeft ontvangen, roept deze een aangepaste sjabloonfunctie aan om de gegevens van de API te vertalen naar de bijbehorende HTML.
Sjabloontaal
Templating kan een verrassend controversieel onderwerp zijn, en wat ik heb gekozen is slechts één van de vele benaderingen. Je zult je eigen oplossing willen gebruiken, vooral als je nog steeds gebruikmaakt van bestaande templates.
Wat voor mijn use case logisch was, was om gewoon te vertrouwen op de template literals van JavaScript, met wat logica opgesplitst in helperfuncties. Een van de voordelen van het bouwen van een MPA is dat je geen statusupdates hoeft bij te houden en je HTML niet opnieuw hoeft te renderen. Een eenvoudige aanpak die statische HTML opleverde, werkte dus voor mij.
Hier is een voorbeeld van hoe ik het dynamische HTML-gedeelte van de index van mijn webapp templateer. Net als bij mijn routes wordt de templatelogica opgeslagen in een ES-module die zowel in de server als in de service worker kan worden geïmporteerd.
export function index(tag, items) {
const title = `<h3>Top "${escape(tag)}"< Qu>estions/h3`;
cons<t form = `form me>tho<d=&qu>ot;GET".../form`;
const questionCards = i>tems
.map(item =
questionCard({
id: item.question_id,
title: item.title,
})
)
.join('&<#39;);
const que>stions = `div id<=&qu>ot;questions"${questionCards}/div`;
return title + form + questions;
}
Deze sjabloonfuncties zijn pure JavaScript, en het is handig om de logica op te splitsen in kleinere, hulpfuncties waar nodig. Hier geef ik elk van de items die in de API-respons worden geretourneerd door aan een dergelijke functie, wat een standaard HTML-element creëert met alle relevante attributen.
function questionCard({id, title}) {
return `<a class="card"
href="/questions/${id}"
data-cache-url=>"${<qu>estionUrl(id)}"${title}/a`;
}
Van bijzonder belang is een data-attribuut dat ik aan elke link toevoeg, data-cache-url , ingesteld op de Stack Exchange API URL die ik nodig heb om de bijbehorende vraag weer te geven. Houd dat in gedachten. Ik kom er later op terug.
Terug naar mijn routehandler : zodra de template klaar is, stream ik het laatste deel van de HTML van mijn pagina naar de browser en beëindig ik de stream. Dit is het signaal aan de browser dat de progressieve rendering voltooid is.
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();
});
Dat was een korte rondleiding door mijn serveropstelling. Gebruikers die mijn webapp voor het eerst bezoeken, krijgen altijd een reactie van de server, maar wanneer een bezoeker terugkeert naar mijn webapp, begint mijn service worker te reageren. Laten we daar eens dieper op ingaan.
De servicemedewerker

Dit diagram zou bekend moeten voorkomen: veel van de onderdelen die ik eerder heb behandeld, zijn hier in een iets andere indeling weergegeven. Laten we de aanvraagstroom eens doorlopen, rekening houdend met de service worker.
Onze service worker verwerkt een binnenkomende navigatieaanvraag voor een bepaalde URL en gebruikt, net als mijn server, een combinatie van routing- en templatelogica om te bepalen hoe erop moet worden gereageerd.
De aanpak is dezelfde als voorheen, maar met andere primitieven op laag niveau, zoals fetch() en de Cache Storage API . Ik gebruik deze gegevensbronnen om de HTML-respons te construeren, die de service worker terugstuurt naar de webapp.
Werkbox
In plaats van helemaal opnieuw te beginnen met primitieven op laag niveau, ga ik mijn service worker bouwen op een set hoogwaardige bibliotheken genaamd Workbox . Dit biedt een solide basis voor de caching-, routing- en responsgeneratielogica van elke service worker.
Routering
Net als bij mijn server-side code moet mijn service worker weten hoe een binnenkomende aanvraag moet worden gekoppeld aan de juiste responslogica.
Mijn aanpak was om elke Express-route te vertalen naar een bijbehorende reguliere expressie , gebruikmakend van een handige bibliotheek genaamd regexparam . Zodra die vertaling is uitgevoerd, kan ik profiteren van de ingebouwde ondersteuning van Workbox voor het routeren van reguliere expressies .
Nadat ik de module met de reguliere expressies heb geïmporteerd, registreer ik elke reguliere expressie bij de router van Workbox. Binnen elke route kan ik aangepaste templatelogica gebruiken om een respons te genereren. Het templaten in de service worker is iets complexer dan in mijn backendserver, maar Workbox helpt me met veel van het zware werk.
import regExpRoutes from './regexp-routes.mjs';
workbox.routing.registerRoute(
regExpRoutes.get('index')
// Templating logic.
);
Statische asset-caching
Een belangrijk onderdeel van het templatesverhaal is ervoor te zorgen dat mijn gedeeltelijke HTML-sjablonen lokaal beschikbaar zijn via de Cache Storage API en up-to-date blijven wanneer ik wijzigingen in de webapp implementeer. Cachebeheer kan foutgevoelig zijn wanneer dit handmatig wordt gedaan, dus schakel ik Workbox in om precaching af te handelen als onderdeel van mijn buildproces.
Ik vertel Workbox welke URL's ik moet precachen met behulp van een configuratiebestand , dat verwijst naar de map met al mijn lokale assets, samen met een set patronen die daarbij passen. Dit bestand wordt automatisch gelezen door de CLI van Workbox , die elke keer wordt uitgevoerd wanneer ik de site opnieuw opbouw.
module.exports = {
globDirectory: 'build',
globPatterns: ['**/*.{html,js,svg}'],
// Other options...
};
Workbox maakt een momentopname van de inhoud van elk bestand en voegt die lijst met URL's en revisies automatisch toe aan mijn uiteindelijke service worker-bestand. Workbox beschikt nu over alles wat nodig is om de pre-cache bestanden altijd beschikbaar en up-to-date te houden. Het resultaat is een service-worker.js -bestand met ongeveer de volgende inhoud:
workbox.precaching.precacheAndRoute([
{
url: 'partials/about.html',
revision: '518747aad9d7e',
},
{
url: 'partials/foot.html',
revision: '69bf746a9ecc6',
},
// etc.
]);
Voor mensen die een complexer bouwproces gebruiken, heeft Workbox zowel een webpack plug-in als een generieke knooppuntmodule , naast de opdrachtregelinterface .
Streamen
Vervolgens wil ik dat de service worker die vooraf gecachede, gedeeltelijke HTML direct terugstreamt naar de webapp. Dit is een cruciaal onderdeel van "betrouwbare snelheid": ik krijg altijd direct iets betekenisvols op het scherm. Gelukkig maakt de Streams API in onze service worker dit mogelijk.
Je hebt misschien al eens van de Streams API gehoord. Mijn collega Jake Archibald prijst de API al jaren. Hij deed de gewaagde voorspelling dat 2016 het jaar van webstreams zou worden. En de Streams API is vandaag de dag nog steeds net zo geweldig als twee jaar geleden, maar met een cruciaal verschil.
Hoewel destijds alleen Chrome Streams ondersteunde, wordt de Streams API nu breder ondersteund . Het algemene verhaal is positief en met de juiste fallback-code staat niets je meer in de weg om vandaag de dag Streams in je service worker te gebruiken.
Nou... er is misschien één ding dat je tegenhoudt, en dat is begrijpen hoe de Streams API eigenlijk werkt. Het legt een zeer krachtige set primitieven bloot, en ontwikkelaars die er vertrouwd mee zijn, kunnen complexe datastromen creëren, zoals de volgende:
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);
}
});
},
});
Maar het begrijpen van de volledige implicaties van deze code is mogelijk niet voor iedereen weggelegd. In plaats van deze logica te analyseren, bespreken we mijn aanpak voor service worker streaming.
Ik gebruik een gloednieuwe, geavanceerde wrapper, workbox-streams . Daarmee kan ik een mix van streamingbronnen doorgeven, zowel vanuit caches als runtimegegevens die mogelijk van het netwerk afkomstig zijn. Workbox zorgt voor de coördinatie van de afzonderlijke bronnen en het samenvoegen ervan tot één streamingrespons.
Bovendien detecteert Workbox automatisch of de Streams API wordt ondersteund, en zo niet, dan genereert het een gelijkwaardige, niet-streamende respons. Dit betekent dat u zich geen zorgen hoeft te maken over het schrijven van fallbacks, aangezien streams steeds dichter bij 100% browserondersteuning komen.
Runtime-caching
Laten we eens kijken hoe mijn service worker omgaat met runtime-gegevens van de Stack Exchange API. Ik maak gebruik van Workbox' ingebouwde ondersteuning voor een stale-while-revalidate cachingstrategie , in combinatie met expiratie om te voorkomen dat de opslagruimte van de webapp onbegrensd wordt.
Ik heb twee strategieën in Workbox opgezet om de verschillende bronnen te verwerken die de streamingrespons vormen. Met een paar functieaanroepen en configuraties laat Workbox ons doen wat anders honderden regels handgeschreven code zou kosten.
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})],
});
De eerste strategie leest gegevens die al in de cache zijn opgeslagen, zoals onze gedeeltelijke HTML-sjablonen.
De andere strategie implementeert de stale-while-revalidate cachelogica, samen met het laten verlopen van de minst recent gebruikte cache zodra we 50 vermeldingen bereiken.
Nu ik die strategieën heb geïmplementeerd, hoef ik Workbox alleen nog maar te vertellen hoe ze gebruikt moeten worden om een complete, streaming respons te construeren. Ik geef een array van bronnen door als functies, en elk van die functies wordt direct uitgevoerd. Workbox neemt het resultaat van elke bron en streamt het naar de webapp, in volgorde, en vertraagt alleen als de volgende functie in de array nog niet voltooid is.
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'}),
]);
De eerste twee bronnen zijn vooraf gecachede gedeeltelijke sjablonen die rechtstreeks uit de Cache Storage API worden gelezen, zodat ze altijd direct beschikbaar zijn. Dit zorgt ervoor dat onze service worker-implementatie betrouwbaar en snel reageert op verzoeken, net als mijn server-side code.
Onze volgende bronfunctie haalt gegevens op uit de Stack Exchange API en verwerkt het antwoord in de HTML die de webapp verwacht.
De stale-while-revalidate-strategie houdt in dat ik, als ik een eerder gecacht antwoord voor deze API-aanroep heb, dit onmiddellijk naar de pagina kan streamen, terwijl de cache-invoer 'op de achtergrond' wordt bijgewerkt voor de volgende keer dat het antwoord wordt aangevraagd.
Tot slot stream ik een gecachte kopie van mijn voettekst en sluit ik de laatste HTML-tags om het antwoord te voltooien.
Door code te delen blijven dingen synchroon
Je zult merken dat bepaalde delen van de service worker-code je bekend voorkomen. De gedeeltelijke HTML- en templatelogica die mijn service worker gebruikt, is identiek aan die van mijn server-side handler. Deze codedeling zorgt ervoor dat gebruikers een consistente ervaring krijgen, of ze nu mijn webapp voor het eerst bezoeken of terugkeren naar een pagina die door de service worker is gerenderd. Dat is het mooie van isomorfe JavaScript.
Dynamische, progressieve verbeteringen
Ik heb zowel de server als de service worker voor mijn PWA doorgenomen, maar er is nog een laatste stukje logica dat ik wil bespreken: er wordt een kleine hoeveelheid JavaScript uitgevoerd op elk van mijn pagina's, nadat ze volledig zijn gestreamd.
Deze code verbetert de gebruikerservaring geleidelijk, maar is niet cruciaal: de webapp werkt ook als deze niet wordt uitgevoerd.
Paginametagegevens
Mijn app gebruikt client-side JavaScipt om de metadata van een pagina bij te werken op basis van de API-respons. Omdat ik voor elke pagina hetzelfde initiële stukje gecachte HTML gebruik, bevat de webapp uiteindelijk generieke tags in de head van mijn document. Maar dankzij coördinatie tussen mijn templates en client-side code kan ik de titel van het venster bijwerken met paginaspecifieke metadata.
Mijn aanpak is om als onderdeel van de templatecode een scripttag op te nemen die de correct geëscapete string bevat.
const metadataScript = `<script>
self._title = '${escape(item.title)<}';>
/script`;
Zodra mijn pagina geladen is , lees ik die string en werk ik de documenttitel bij.
if (self._title) {
document.title = unescape(self._title);
}
Als u andere paginaspecifieke metagegevens in uw eigen web-app wilt bijwerken, kunt u dezelfde aanpak volgen.
Offline UX
De andere progressieve verbetering die ik heb doorgevoerd, is bedoeld om de aandacht te vestigen op onze offline mogelijkheden. Ik heb een betrouwbare PWA gebouwd en ik wil dat gebruikers weten dat ze, zelfs als ze offline zijn, eerder bezochte pagina's nog steeds kunnen laden.
Eerst gebruik ik de Cache Storage API om een lijst op te vragen van alle eerder gecachte API-verzoeken. Die vertaal ik naar een lijst met URL's.
Herinner je je die speciale data-attributen waar ik het over had , die elk de URL bevatten voor de API-aanvraag die nodig is om een vraag weer te geven? Ik kan die data-attributen vergelijken met de lijst met gecachte URL's en een array maken van alle vraaglinks die niet overeenkomen.
Wanneer de browser offline gaat, loop ik door de lijst met niet-gecachte links en dim ik de links die niet werken. Houd er rekening mee dat dit slechts een visuele hint is voor de gebruiker over wat hij of zij van die pagina's kan verwachten. Ik schakel de links niet uit en voorkom ook niet dat de gebruiker kan navigeren.
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].filte>r(card = {
return !cachedUrls.includes(card.dataset.cacheUrl);
});
const offlineHandle>r = () = {
for (const uncachedCard of uncachedCards) {
uncachedCard.style.opacity = '0.3';
}
};
const onli>neHandler = () = {
for (const uncachedCard of uncachedCards) {
uncachedCard.style.opacity = '1.0';
}
};
window.addEventListener('online', onlineHandler);
window.addEventListener('offline', offlineHandler);
Veelvoorkomende valkuilen
Ik heb nu een rondleiding gegeven door mijn aanpak voor het bouwen van een PWA met meerdere pagina's. Er zijn veel factoren waarmee je rekening moet houden bij het bedenken van je eigen aanpak, en het kan zijn dat je uiteindelijk andere keuzes maakt dan ik. Die flexibiliteit is een van de voordelen van bouwen voor het web.
Er zijn een paar veelvoorkomende valkuilen die u kunt tegenkomen wanneer u zelf architectuurbeslissingen neemt. Ik wil u graag wat pijn besparen.
Cache niet de volledige HTML
Ik raad af om complete HTML-documenten in je cache op te slaan. Ten eerste is het ruimteverspilling. Als je webapp voor elke pagina dezelfde basis-HTML-structuur gebruikt, zul je uiteindelijk steeds dezelfde opmaak kopiëren.
Belangrijker nog: als je een wijziging doorvoert in de gedeelde HTML-structuur van je site, blijft al die eerder gecachte pagina's nog steeds in de oude lay-out staan. Stel je de frustratie voor van een terugkerende bezoeker die een mix van oude en nieuwe pagina's ziet.
Server/servicemedewerker drift
De andere valkuil die je moet vermijden, is dat je server en service worker niet synchroon lopen. Mijn aanpak was om isomorfe JavaScript te gebruiken, zodat dezelfde code op beide plekken werd uitgevoerd. Afhankelijk van je bestaande serverarchitectuur is dat niet altijd mogelijk.
Welke architectuurbeslissingen u ook neemt, u moet een strategie hebben voor het uitvoeren van de equivalente routerings- en sjablooncode op uw server en in uw service worker.
Ergste scenario's
Inconsistente lay-out/ontwerp
Wat gebeurt er als je die valkuilen negeert? Nou ja, allerlei mislukkingen zijn mogelijk, maar het ergste scenario is dat een terugkerende gebruiker een gecachte pagina bezoekt met een verouderde lay-out – misschien een pagina met verouderde headertekst, of een pagina die CSS-klassenamen gebruikt die niet langer geldig zijn.
Ergste scenario: verbroken routering
Een gebruiker kan ook een URL tegenkomen die wel door je server wordt beheerd, maar niet door je service worker. Een site vol zombie-layouts en doodlopende wegen is geen betrouwbare PWA.
Tips voor succes
Maar je staat er niet alleen voor! De volgende tips kunnen je helpen deze valkuilen te vermijden:
Gebruik sjabloon- en routeringsbibliotheken met meertalige implementaties
Probeer template- en routingbibliotheken te gebruiken die JavaScript-implementaties hebben. Nu weet ik dat niet elke ontwikkelaar de luxe heeft om te migreren van je huidige webserver en templatetaal.
Maar een aantal populaire template- en routingframeworks hebben implementaties in meerdere talen. Als je er een kunt vinden die zowel met JavaScript als met de taal van je huidige server werkt, ben je een stap dichter bij het synchroon houden van je service worker en server.
Geef de voorkeur aan sequentiële sjablonen in plaats van geneste sjablonen
Vervolgens raad ik aan om een reeks opeenvolgende sjablonen te gebruiken die achter elkaar kunnen worden geplaatst. Het is prima als latere delen van je pagina complexere sjabloonlogica gebruiken, zolang je het eerste deel van je HTML maar zo snel mogelijk kunt plaatsen.
Cache zowel statische als dynamische inhoud in uw service worker
Voor de beste prestaties moet u alle kritieke statische bronnen van uw site precachen. U moet ook runtime-cachinglogica instellen om dynamische content, zoals API-verzoeken, te verwerken. Met Workbox kunt u voortbouwen op beproefde, productieklare strategieën in plaats van alles vanaf nul te moeten implementeren.
Alleen blokkeren op het netwerk als het absoluut noodzakelijk is
En daarmee samenhangend, zou je alleen op het netwerk moeten blokkeren wanneer het niet mogelijk is om een respons uit de cache te streamen. Het direct weergeven van een gecachte API-respons kan vaak leiden tot een betere gebruikerservaring dan wachten op nieuwe data.