Beyond SPA's - alternatieve architecturen voor uw PWA

Laten we het hebben over... architectuur?

Ik ga een belangrijk, maar mogelijk verkeerd begrepen onderwerp behandelen: de architectuur die u gebruikt voor uw web-app, en specifiek, hoe uw architecturale beslissingen een rol spelen wanneer u een progressieve web-app bouwt.

‘Architectuur’ kan vaag klinken, en het is misschien niet meteen duidelijk waarom dit ertoe doet. Eén manier om over architectuur na te denken is door uzelf de volgende vragen te stellen: welke HTML wordt geladen wanneer een gebruiker een pagina op mijn site bezoekt? En wat wordt er dan geladen als ze een andere pagina bezoeken?

De antwoorden op deze vragen zijn niet altijd eenvoudig, en als je eenmaal begint na te denken over progressieve webapps, kunnen ze nog ingewikkelder worden. Mijn doel is dus om u door een mogelijke architectuur te leiden die ik effectief vond. In dit artikel zal ik de beslissingen die ik heb genomen bestempelen als 'mijn aanpak' bij het bouwen van een progressieve web-app.

Je bent vrij om mijn aanpak te gebruiken bij het bouwen van je eigen PWA, maar tegelijkertijd zijn er altijd andere geldige alternatieven. Ik hoop dat het zien van hoe alle stukjes in elkaar passen je zal inspireren, en dat je je gesterkt zult voelen om dit aan je behoeften aan te passen.

Stackoverflow-PWA

Bij dit artikel heb ik een Stack Overflow PWA gebouwd. Ik besteed veel tijd aan het lezen van en bijdragen aan Stack Overflow , en ik wilde een webapp bouwen waarmee je gemakkelijk door veelgestelde vragen over een bepaald onderwerp kunt bladeren. Het is gebouwd bovenop de openbare Stack Exchange API . Het is open source en je kunt meer leren door het GitHub-project te bezoeken .

Apps met meerdere pagina's (MPA's)

Voordat ik op de details inga, zullen we eerst enkele termen definiëren en de onderliggende technologie uitleggen. Eerst ga ik het hebben over wat ik 'Multi Page Apps' of 'MPA's' noem.

MPA is een mooie naam voor de traditionele architectuur die sinds het begin van het internet wordt gebruikt. Elke keer dat een gebruiker naar een nieuwe URL navigeert, geeft de browser geleidelijk HTML weer die specifiek is voor die pagina. Er wordt geen poging ondernomen om de status van de pagina of de inhoud tussen navigaties door te behouden. Elke keer dat u een nieuwe pagina bezoekt, begint u opnieuw.

Dit in tegenstelling tot het SPA-model ( single-page app ) 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 geldige modellen om te gebruiken, maar voor dit bericht wilde ik PWA-concepten verkennen binnen de context van een app met meerdere pagina's.

Betrouwbaar snel

Je hebt mij (en talloze anderen) de uitdrukking "progressieve webapp" of PWA horen gebruiken. Mogelijk bent u al bekend met een deel van het achtergrondmateriaal elders op deze site .

Een PWA kun je zien als een webapp die een eersteklas gebruikerservaring biedt, en die echt een plek verdient op het startscherm van de gebruiker. Het acroniem " FIRE ", dat staat voor F ast, Integrated , Re liable en E ngaging, vat alle eigenschappen samen waar u aan moet denken bij het bouwen van een PWA.

In dit artikel ga ik me concentreren op een subset van deze kenmerken: Snel en betrouwbaar .

Snel: Hoewel 'snel' verschillende dingen betekent in verschillende contexten, ga ik in op de snelheidsvoordelen van zo min mogelijk laden vanaf het netwerk.

Betrouwbaar: Maar pure snelheid is niet genoeg. Om u het gevoel te geven dat u een PWA bent, moet uw webapp betrouwbaar zijn. Het moet veerkrachtig genoeg zijn om altijd iets te laden, ook al is het maar een aangepaste foutpagina, ongeacht de status van het netwerk.

Betrouwbaar snel: En tot slot ga ik de PWA-definitie enigszins herformuleren en kijken naar wat het betekent om iets te bouwen dat betrouwbaar snel is. Het is niet goed genoeg om alleen snel en betrouwbaar te zijn als u zich op een netwerk met lage latentie bevindt. Betrouwbaar snel betekent dat de snelheid van uw webapp consistent is, ongeacht de onderliggende netwerkomstandigheden.

Technologieën inschakelen: servicemedewerkers + cache-opslag-API

PWA's leggen de lat hoog voor snelheid en veerkracht. Gelukkig biedt het webplatform enkele bouwstenen om dat soort prestaties werkelijkheid te maken. Ik heb het over servicemedewerkers en de Cache Storage API .

U kunt via de Cache Storage API een servicemedewerker bouwen die luistert naar binnenkomende verzoeken, enkele doorgeeft aan het netwerk en een kopie van het antwoord opslaat voor toekomstig gebruik.

Een servicemedewerker die de Cache Storage API gebruikt om een ​​kopie van een netwerkantwoord op te slaan.

De volgende keer dat de webapp hetzelfde verzoek doet, kan de servicemedewerker de caches controleren en gewoon het eerder in de cache opgeslagen antwoord retourneren.

Een servicemedewerker die de Cache Storage API gebruikt om te reageren, waarbij het netwerk wordt omzeild.

Het vermijden van het netwerk waar mogelijk is een cruciaal onderdeel van het bieden van betrouwbare snelle prestaties.

"Isomorf" JavaScript

Nog een concept dat ik wil behandelen is wat soms "isomorf" of "universeel" JavaScript wordt genoemd. Simpel gezegd is het het idee dat dezelfde JavaScript-code kan worden gedeeld tussen verschillende runtime-omgevingen. Toen ik mijn PWA bouwde, wilde ik JavaScript-code delen tussen mijn back-endserver en de servicemedewerker.

Er zijn veel geldige benaderingen om code op deze manier te delen, maar mijn benadering was om ES-modules als de definitieve broncode te gebruiken. Vervolgens heb ik die modules voor de server en de servicemedewerker getranspileerd en gebundeld met behulp van een combinatie van Babel en Rollup . In mijn project zijn bestanden met de bestandsextensie .mjs code die in een ES-module leeft.

De server

Laten we, met deze concepten en terminologie in gedachten, eens kijken hoe ik mijn Stack Overflow PWA feitelijk heb gebouwd. Ik ga beginnen met het bespreken van onze backend-server en uitleggen hoe dat in de algehele architectuur past.

Ik was op zoek naar een combinatie van een dynamische backend en statische hosting, en mijn aanpak was om het Firebase-platform te gebruiken.

Firebase Cloud Functions zal automatisch een op knooppunten gebaseerde omgeving opstarten wanneer er een inkomend verzoek is, en integreren met het populaire Express HTTP-framework , waarmee ik al bekend was. Het biedt ook kant-en-klare hosting voor alle statische bronnen van mijn site. Laten we eens kijken hoe de server omgaat met verzoeken.

Wanneer een browser een navigatieverzoek doet tegen onze server, doorloopt deze de volgende procedure:

Een overzicht van het genereren van een navigatiereactie, serverzijde.

De server stuurt het verzoek door op basis van de URL en gebruikt sjabloonlogica om een ​​compleet HTML-document te maken. Ik gebruik een combinatie van gegevens uit de Stack Exchange API, evenals gedeeltelijke HTML-fragmenten die de server lokaal opslaat. Zodra onze servicemedewerker weet hoe hij moet reageren, kan hij beginnen met het terugstreamen van HTML naar onze webapp.

Er zijn twee delen van deze afbeelding die de moeite waard zijn om nader te onderzoeken: routing en templates.

Routering

Als het om routering gaat, was mijn aanpak om de eigen routeringssyntaxis van het Express-framework te gebruiken. Het is flexibel genoeg om eenvoudige URL-voorvoegsels te matchen, evenals URL's die parameters bevatten als onderdeel van het pad. Hier maak ik een mapping tussen de routenamen en het onderliggende Express-patroon waarmee ik moet matchen.

const routes = new Map([
  ['about', '/about'],
  ['questions', '/questions/:questionId'],
  ['index', '/'],
]);

export default routes;

Ik kan dan rechtstreeks vanuit de servercode naar deze mapping verwijzen. Wanneer er een overeenkomst is voor een bepaald Express-patroon, reageert de juiste handler met sjabloonlogica die specifiek is voor de overeenkomende route.

import routes from './lib/routes.mjs';
app.get(routes.get('index'), async (req, res) => {
  // Templating logic.
});

Sjablonen aan de serverzijde

En hoe ziet die sjabloonlogica eruit? Nou, ik ging voor een aanpak waarbij gedeeltelijke HTML-fragmenten in volgorde, de een na de ander, werden samengevoegd. Dit model leent zich goed voor streaming.

De server stuurt onmiddellijk een initiële HTML-boilerplate terug en de browser kan die gedeeltelijke pagina meteen weergeven. Terwijl de server de rest van de gegevensbronnen samenvoegt, streamt hij deze naar de browser totdat het document compleet is.

Om te zien wat ik bedoel, bekijk 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 response te gebruiken en te verwijzen naar lokaal opgeslagen gedeeltelijke sjablonen, kan ik de responsstroom onmiddellijk starten, zonder enige externe gegevensbron te blokkeren. De browser neemt deze initiële HTML en geeft meteen een betekenisvolle interface en laadbericht weer.

Het volgende gedeelte van onze pagina gebruikt gegevens van de Stack Exchange API . Het verkrijgen van die gegevens betekent dat onze server een netwerkverzoek moet doen. De webapp kan niets anders weergeven totdat er een reactie is ontvangen en deze is verwerkt, maar gebruikers staren in ieder geval niet naar een leeg scherm terwijl ze wachten.

Zodra de web-app het antwoord van de Stack Exchange API heeft ontvangen, roept deze een aangepaste sjabloonfunctie aan om de gegevens van de API naar de bijbehorende HTML te vertalen.

Sjabloontaal

Sjablonen kunnen een verrassend controversieel onderwerp zijn, en wat ik heb gekozen is slechts één van de vele benaderingen. U zult uw eigen oplossing willen vervangen, vooral als u verouderde banden heeft met een bestaand sjabloonframework.

Wat voor mijn gebruiksscenario logisch was, was om gewoon te vertrouwen op de template-literals van JavaScript, waarbij wat logica werd opgesplitst in helperfuncties. Een van de leuke dingen van het bouwen van een MPA is dat je geen statusupdates hoeft bij te houden en je HTML niet opnieuw hoeft weer te geven, dus een basisaanpak die statische HTML produceerde, werkte voor mij.

Hier is een voorbeeld van hoe ik het dynamische HTML-gedeelte van de index van mijn webapp vormgeef. Net als bij mijn routes wordt de sjabloonlogica opgeslagen in een ES-module die zowel in de server als in de servicemedewerker kan worden geïmporteerd.

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;
}

Deze sjabloonfuncties zijn puur JavaScript en het is handig om de logica, indien nodig, op te splitsen in kleinere hulpfuncties. Hier geef ik elk van de items die in het API-antwoord worden geretourneerd door aan een dergelijke functie, die een standaard HTML-element creëert met alle juiste attributen ingesteld.

function questionCard({id, title}) {
  return `<a class="card"
             href="/questions/${id}"
             data-cache-url="${questionUrl(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. Onthoud dat. Ik zal het later opnieuw bekijken.

Als ik terugga naar mijn route-handler , stream ik, zodra het templaten is voltooid, het laatste deel van de HTML van mijn pagina naar de browser en beëindig ik de stream. Dit is het signaal voor de browser dat de progressieve weergave 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();
});

Dus dat is een korte rondleiding door mijn serverconfiguratie. 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 servicemedewerker te reageren. Laten we erin duiken.

De servicemedewerker

Een overzicht van het genereren van een navigatiereactie in de servicemedewerker.

Dit diagram zou er bekend uit moeten zien; veel van dezelfde stukken die ik eerder heb behandeld, staan ​​hier in een iets andere opstelling. Laten we de aanvraagstroom eens doornemen, waarbij we rekening houden met de servicemedewerker.

Onze servicemedewerker verwerkt een binnenkomend navigatieverzoek voor een bepaalde URL, en net als mijn server gebruikt deze een combinatie van routerings- en sjabloonlogica om erachter te komen hoe er moet worden gereageerd.

De aanpak is hetzelfde als voorheen, maar met verschillende primitieven op laag niveau, zoals fetch() en de Cache Storage API . Ik gebruik deze gegevensbronnen om het HTML-antwoord samen te stellen, dat de servicemedewerker terugstuurt naar de webapp.

Werkdoos

In plaats van helemaal opnieuw te beginnen met primitieven op een laag niveau, ga ik mijn servicemedewerker bouwen bovenop een reeks bibliotheken op hoog niveau, genaamd Workbox . Het biedt een solide basis voor de caching-, routing- en responsgeneratielogica van elke servicemedewerker.

Routering

Net als bij mijn server-side code moet mijn servicemedewerker weten hoe hij een binnenkomend verzoek moet matchen met de juiste antwoordlogica.

Mijn aanpak was om elke Express-route te vertalen naar een overeenkomstige reguliere expressie , waarbij ik gebruik maakte van een behulpzame bibliotheek genaamd regexparam . Zodra die vertaling is uitgevoerd, kan ik profiteren van de ingebouwde ondersteuning van Workbox voor routering 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 sjabloonlogica bieden om een ​​antwoord te genereren. Het maken van sjablonen in de servicemedewerker is iets ingewikkelder dan in mijn backend-server, maar Workbox helpt bij veel van het zware werk.

import regExpRoutes from './regexp-routes.mjs';

workbox.routing.registerRoute(
  regExpRoutes.get('index')
  // Templating logic.
);

Statische activacaching

Een belangrijk onderdeel van het sjabloonverhaal is ervoor zorgen dat mijn gedeeltelijke HTML-sjablonen lokaal beschikbaar zijn via de Cache Storage API, en up-to-date worden gehouden wanneer ik wijzigingen in de webapp doorvoer. Cache-onderhoud kan foutgevoelig zijn als het handmatig wordt gedaan, dus ik wend me tot Workbox om precaching af te handelen als onderdeel van mijn bouwproces.

Ik vertel Workbox welke URL's vooraf in de cache moeten worden geplaatst met behulp van een configuratiebestand , verwijzend naar de map die al mijn lokale middelen bevat, samen met een reeks bijpassende patronen. Dit bestand wordt automatisch gelezen door de CLI van Workbox , die elke keer wordt uitgevoerd als 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 injecteert die lijst met URL's en revisies automatisch in mijn uiteindelijke servicemedewerkerbestand. Workbox heeft nu alles wat nodig is om de vooraf in de cache opgeslagen bestanden altijd beschikbaar en up-to-date te houden. Het resultaat is een service-worker.js -bestand dat iets bevat dat lijkt op het volgende:

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 servicemedewerker de vooraf in de cache opgeslagen gedeeltelijke HTML onmiddellijk terugstuurt naar de webapp. Dit is een cruciaal onderdeel van 'betrouwbaar snel' zijn: ik krijg altijd meteen iets betekenisvols op het scherm. Gelukkig maakt het gebruik van de Streams API binnen onze servicemedewerker dat mogelijk.

Misschien heb je al eerder over de Streams API gehoord. Mijn collega Jake Archibald zingt er al jaren lof voor. Hij voorspelde dat 2016 het jaar van de webstreams zou worden. En de Streams API is vandaag de dag nog net zo geweldig als twee jaar geleden, maar met een cruciaal verschil.

Terwijl destijds alleen Chrome Streams ondersteunde, wordt de Streams API nu breder ondersteund . Het algemene verhaal is positief, en met de juiste fallback-code houdt niets u tegen om vandaag nog streams in uw servicemedewerker te gebruiken.

Nou... er is misschien één ding dat je tegenhoudt, en dat is dat je je afvraagt ​​hoe de Streams API eigenlijk werkt. Het legt een zeer krachtige reeks primitieven bloot, en ontwikkelaars die er vertrouwd mee zijn, kunnen complexe gegevensstromen 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 misschien niet voor iedereen weggelegd. Laten we, in plaats van deze logica te ontleden, het hebben over mijn benadering van het streamen van servicemedewerkers.

Ik gebruik een gloednieuwe wrapper op hoog niveau, workbox-streams . Hiermee kan ik het doorgeven aan een mix van streamingbronnen, zowel vanuit caches als runtimegegevens die mogelijk afkomstig zijn van het netwerk. Workbox zorgt voor de coördinatie van de afzonderlijke bronnen en voegt ze samen tot één streamingantwoord.

Bovendien detecteert Workbox automatisch of de Streams API wordt ondersteund, en wanneer dit niet het geval is, creëert het een gelijkwaardig, niet-streaming antwoord. Dit betekent dat u zich geen zorgen hoeft te maken over het schrijven van fallbacks, aangezien streams dichter bij 100% browserondersteuning komen.

Runtime-caching

Laten we eens kijken hoe mijn servicemedewerker omgaat met runtimegegevens vanuit de Stack Exchange API. Ik maak gebruik van de ingebouwde ondersteuning van Workbox voor een cachingstrategie die verouderd en opnieuw valideert , samen met expiratie om ervoor te zorgen dat de opslag van de webapp niet onbeperkt groeit.

Ik heb twee strategieën opgezet in Workbox om de verschillende bronnen te verwerken waaruit het streamingantwoord zal bestaan. Met een paar functieaanroepen en configuratie laat Workbox ons doen wat anders honderden regels handgeschreven code zou vergen.

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 vooraf in de cache zijn geplaatst, zoals onze gedeeltelijke HTML-sjablonen.

De andere strategie implementeert de caching-logica die verouderd en opnieuw valideert, samen met het verlopen van de minst recent gebruikte cache zodra we 50 vermeldingen hebben bereikt.

Nu ik deze strategieën heb, hoef ik Workbox alleen nog maar te vertellen hoe ik ze moet gebruiken om een ​​compleet, streaming antwoord te construeren. Ik geef een reeks bronnen door als functies, en elk van deze functies wordt onmiddellijk uitgevoerd. Workbox neemt het resultaat van elke bron en streamt dit achtereenvolgens naar de web-app, waarbij alleen vertraging optreedt als de volgende functie in de array nog niet is voltooid.

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 in de cache opgeslagen gedeeltelijke sjablonen die rechtstreeks uit de Cache Storage API worden gelezen, zodat ze altijd onmiddellijk beschikbaar zijn. Dit zorgt ervoor dat de implementatie van onze servicemedewerkers betrouwbaar 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 strategie voor verouderde-terwijl-revalideren betekent dat als ik een eerder in de cache opgeslagen reactie voor deze API-aanroep heb, ik deze onmiddellijk naar de pagina kan streamen, terwijl ik de cache-invoer "op de achtergrond" bijwerk voor de volgende keer dat deze wordt aangevraagd .

Ten slotte stream ik een in de cache opgeslagen kopie van mijn voettekst en sluit ik de laatste HTML-tags om het antwoord te voltooien.

Door code te delen, blijven de zaken gesynchroniseerd

U zult merken dat bepaalde stukjes van de servicemedewerkercode u bekend voorkomen. De gedeeltelijke HTML- en sjabloonlogica die door mijn servicemedewerker wordt gebruikt, is identiek aan wat mijn server-side handler gebruikt. Dit delen van code zorgt ervoor dat gebruikers een consistente ervaring krijgen, of ze nu mijn web-app voor de eerste keer bezoeken of terugkeren naar een pagina die door de servicemedewerker wordt weergegeven. Dat is het mooie van isomorf JavaScript.

Dynamische, progressieve verbeteringen

Ik heb zowel de server als de servicemedewerker voor mijn PWA doorgenomen, maar er moet nog een laatste stukje logica worden behandeld: er is een kleine hoeveelheid JavaScript die op elk van mijn pagina's wordt uitgevoerd, nadat ze volledig zijn gestreamd.

Deze code verbetert geleidelijk de gebruikerservaring, maar is niet cruciaal: de webapp blijft werken als deze niet wordt uitgevoerd.

Metagegevens van de pagina

Mijn app gebruikt JavaScipt aan de clientzijde om de metagegevens van een pagina bij te werken op basis van het API-antwoord. Omdat ik voor elke pagina hetzelfde aanvankelijke stukje in de cache opgeslagen HTML gebruik, krijgt de webapp algemene tags in het hoofd van mijn document. Maar door de coördinatie tussen mijn sjablonen en de code aan de clientzijde kan ik de titel van het venster bijwerken met behulp van paginaspecifieke metagegevens.

Als onderdeel van de sjablooncode is mijn aanpak om een ​​scripttag op te nemen die de correct geëscapede tekenreeks bevat.

const metadataScript = `<script>
  self._title = '${escape(item.title)}';
</script>`;

Vervolgens lees ik, zodra mijn pagina is geladen , die tekenreeks en werk ik de documenttitel bij.

if (self._title) {
  document.title = unescape(self._title);
}

Als er andere stukjes paginaspecifieke metadata zijn die u in uw eigen webapp wilt bijwerken, kunt u dezelfde aanpak volgen.

Offline-UX

De andere progressieve verbetering die ik heb toegevoegd, wordt gebruikt om de aandacht te vestigen op onze offline mogelijkheden. Ik heb een betrouwbare PWA gebouwd en ik wil dat gebruikers weten dat ze, wanneer ze offline zijn, nog steeds eerder bezochte pagina's kunnen laden.

Eerst gebruik ik de Cache Storage API om een ​​lijst te krijgen van alle eerder in de cache opgeslagen API-verzoeken, en die vertaal ik naar een lijst met URL's.

Weet je nog die speciale data-attributen waar ik het over had , die elk de URL bevatten voor het API-verzoek dat nodig is om een ​​vraag weer te geven? Ik kan deze gegevenskenmerken vergelijken met de lijst met in de cache opgeslagen 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 mag verwachten. Ik schakel niet echt de links uit, of verhinder niet dat de gebruiker navigeert.

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);

Veelvoorkomende valkuilen

Ik heb nu een rondleiding gehad door mijn aanpak voor het bouwen van een PWA met meerdere pagina's. Er zijn veel factoren waarmee u rekening moet houden bij het bedenken van uw eigen aanpak, en het kan zijn dat u andere keuzes maakt dan ik. Die flexibiliteit is een van de geweldige dingen van bouwen voor internet.

Er zijn een paar veelvoorkomende valkuilen die u kunt tegenkomen bij het nemen van uw eigen architecturale beslissingen, en ik wil u wat pijn besparen.

Cache geen volledige HTML

Ik raad af om volledige HTML-documenten in uw cache op te slaan. Om te beginnen is het zonde van de ruimte. Als uw webapp voor elke pagina dezelfde basis-HTML-structuur gebruikt, zult u steeds opnieuw kopieën van dezelfde opmaak opslaan.

Wat nog belangrijker is: als u een wijziging doorvoert in de gedeelde HTML-structuur van uw site, blijven al die eerder in de cache opgeslagen pagina's nog steeds vastzitten aan uw oude lay-out. 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 u moet vermijden, is dat uw server en servicemedewerker niet meer synchroon lopen. Mijn aanpak was om isomorf JavaScript te gebruiken, zodat op beide plaatsen dezelfde code werd uitgevoerd. Afhankelijk van uw bestaande serverarchitectuur is dat niet altijd mogelijk.

Welke architecturale beslissingen u ook neemt, u moet een strategie hebben voor het uitvoeren van de gelijkwaardige routerings- en sjablooncode op uw server en uw servicemedewerker.

Worstcasescenario’s

Inconsistente lay-out / ontwerp

Wat gebeurt er als je deze valkuilen negeert? Er zijn allerlei soorten fouten mogelijk, maar in het ergste geval bezoekt een terugkerende gebruiker een in het cachegeheugen opgeslagen pagina met een zeer verouderde lay-out, misschien een pagina met verouderde koptekst, of die CSS-klassenamen gebruikt die niet langer geldig zijn.

In het slechtste geval: kapotte routering

Het kan ook zijn dat een gebruiker een URL tegenkomt die door uw server wordt afgehandeld, maar niet door uw servicemedewerker. Een site vol zombie-indelingen en doodlopende wegen is geen betrouwbare PWA.

Tips voor succes

Maar je staat er niet alleen voor! Met de volgende tips kunt u deze valkuilen vermijden:

Gebruik sjabloon- en routeringsbibliotheken met meertalige implementaties

Probeer sjabloon- en routeringsbibliotheken te gebruiken die JavaScript-implementaties hebben. Nu weet ik dat niet elke ontwikkelaar de luxe heeft om van uw huidige webserver en sjabloontaal te migreren.

Maar een aantal populaire template- en routeringsframeworks hebben implementaties in meerdere talen. Als u er een kunt vinden die zowel met JavaScript als met de taal van uw huidige server werkt, bent u een stap dichter bij het gesynchroniseerd houden van uw servicemedewerker en server.

Geef de voorkeur aan sequentiële, in plaats van geneste, sjablonen

Vervolgens raad ik aan een reeks opeenvolgende sjablonen te gebruiken die na elkaar kunnen worden gestreamd. Het is geen probleem als latere gedeelten van uw pagina ingewikkeldere sjabloonlogica gebruiken, zolang u het eerste deel van uw HTML maar zo snel mogelijk kunt streamen.

Cache zowel statische als dynamische inhoud in uw servicemedewerker

Voor de beste prestaties moet u alle kritieke statische bronnen van uw site vooraf in de cache opslaan. U moet ook runtime-cachinglogica instellen om dynamische inhoud, zoals API-verzoeken, te verwerken. Het gebruik van Workbox betekent dat u kunt voortbouwen op goed geteste, productieklare strategieën in plaats van deze helemaal opnieuw te implementeren.

Blokkeer alleen op het netwerk als dit absoluut noodzakelijk is

En in verband hiermee moet u het netwerk alleen blokkeren als het niet mogelijk is om een ​​antwoord uit de cache te streamen. Het onmiddellijk weergeven van een in de cache opgeslagen API-antwoord kan vaak leiden tot een betere gebruikerservaring dan wachten op nieuwe gegevens.

Bronnen