Oltre le SPA: architetture alternative per la tua PWA

Parliamo di... architettura?

Tratterò un argomento importante, ma potenzialmente frainteso: l'architettura che usi per la tua app web e, in particolare, il modo in cui le decisioni relative all'architettura entrano in gioco quando crei un'app web progressiva.

La parola "Architettura" può sembrare vaga e potrebbe non essere subito chiaro perché questo sia importante. Per capire cosa si intende per architettura, poniti le seguenti domande: quando un utente visita una pagina del mio sito, quale codice HTML viene caricato? Poi, cosa viene caricato quando un utente visita un'altra pagina?

Le risposte a queste domande non sono sempre precise e, quando si iniziano a pensare alle app web progressive, possono diventare ancora più complesse. Il mio obiettivo è illustrarti una possibile architettura che ho trovato efficace. In questo articolo, contrassegnerò le decisioni che ho preso come "il mio approccio " alla creazione di un'app web progressiva.

Puoi usare il mio approccio quando crei la tua PWA, ma allo stesso tempo ci sono sempre altre alternative valide. La mia speranza è che vedere in che modo tutti i pezzi interagiscono ti ispiri e che tu ti senta in grado di personalizzarla in base alle tue esigenze.

PWA Stack Overflow

Per accompagnare questo articolo, ho creato una PWA di Stack Overflow. Trascorro molto tempo a leggere e a contribuire a Stack Overflow e volevo creare un'app web che facilitasse la consultazione delle domande frequenti su un determinato argomento. Si basa sull'API Stack Exchange pubblica. È open source e puoi scoprire di più visitando il progetto GitHub.

App su più pagine (MPA)

Prima di entrare nello specifico, definiamo alcuni termini e spieghiamo le tecnologie sottostanti. Prima di tutto, parlerò di ciò che voglio chiamare "App multipagina" o "MPA".

MPA è un nome fantasioso per l'architettura tradizionale utilizzata fin dall'inizio del web. Ogni volta che un utente accede a un nuovo URL, il browser esegue progressivamente il rendering del codice HTML specifico per la pagina. Non esiste alcun tentativo di preservare lo stato della pagina o i contenuti tra una navigazione e l'altra. Ogni volta che visiti una nuova pagina, inizi da capo.

A differenza del modello di app a pagina singola (SPA) per la creazione di app web, in cui il browser esegue il codice JavaScript per aggiornare la pagina esistente quando l'utente visita una nuova sezione. Sia le SPA che gli MPA sono modelli altrettanto validi da usare, ma per questo post ho voluto esplorare i concetti delle PWA nel contesto di un'app con più pagine.

Affidabilità veloce

Avete sentito parlare da me (e da innumerevoli altri) di usare la frase "app web progressiva" o PWA. Potresti già conoscere parte del materiale di sfondo, in altre sezioni di questo sito.

Puoi pensare a una PWA come a un'app web che offre un'esperienza utente di prim'ordine e che si aggiudica davvero un posto nella schermata Home dell'utente. L'acronimo "FIRE", che indica Fast, Integrato, Raffidabile e Engaging, riassume tutti gli attributi da considerare per la creazione di una PWA.

In questo articolo, mi soffermerò su un sottoinsieme di questi attributi: Veloce e Affidabile.

Veloce: anche se "veloce" significa cose diverse a seconda dei contesti, tratterò i vantaggi in termini di velocità del caricamento dalla rete il meno possibile.

Affidabile: ma la velocità grezza non è sufficiente. Per sembrare una PWA, la tua app web deve essere affidabile. Deve essere abbastanza resiliente da caricare sempre qualcosa, anche se si tratta solo di una pagina di errore personalizzata, indipendentemente dallo stato della rete.

Affidabilità veloce:per concludere, riformulerò leggermente la definizione di PWA e analizzerò cosa significa creare qualcosa di veloce e affidabile. Non è abbastanza veloce e affidabile solo se usi una rete a bassa latenza. Una velocità affidabile significa che la velocità della tua app web è coerente, indipendentemente dalle condizioni di rete sottostanti.

Tecnologie abilitanti: service worker + API Cache Storage

Le PWA introducono uno standard elevato in termini di velocità e resilienza. Fortunatamente, la piattaforma web offre alcuni componenti di base per realizzare questo tipo di prestazioni. Mi riferisco ai service worker e all'API Cache Storage.

Puoi creare un service worker che rimane in ascolto delle richieste in entrata, passandone alcune alla rete e archiviando una copia della risposta per uso futuro, tramite l'API Cache Storage.

Un service worker che utilizza l'API Cache Storage per salvare una copia di una risposta di rete.

La volta successiva che l'app web effettuerà la stessa richiesta, il suo service worker potrà controllare le sue cache e restituire semplicemente la risposta precedentemente memorizzata nella cache.

Un service worker che utilizza l'API Cache Storage per rispondere, bypassando la rete.

Evitando la rete, quando possibile, è fondamentale per offrire prestazioni affidabili e veloci.

JavaScript "isomorfico"

Un altro concetto di cui voglio parlare è ciò che a volte è definito JavaScript "isomorfico" o "universale". In breve, l'idea è che lo stesso codice JavaScript possa essere condiviso tra diversi ambienti di runtime. Quando ho creato la mia PWA, volevo condividere il codice JavaScript tra il server back-end e il service worker.

Esistono molti approcci validi per condividere il codice in questo modo, ma il mio approccio era utilizzare i moduli ES come codice sorgente definitivo. Quindi, ho eseguito il transpilo e il bundle dei moduli per il server e il service worker utilizzando una combinazione di Babel e Rollup. Nel mio progetto, i file con un'estensione di .mjs sono codice che risiede in un modulo ES.

Il server

Tenendo a mente questi concetti e terminologia, analizziamo come ho effettivamente creato la mia PWA Stack Overflow. Inizierò coprendo il nostro server di backend e spiegando come si inserisce nell'architettura complessiva.

Stavo cercando una combinazione di backend dinamico e hosting statico. Il mio approccio consisteva nell'utilizzare la piattaforma Firebase.

Firebase Cloud Functions avvierà automaticamente un ambiente basato su nodi quando arriva una richiesta in entrata e si integrerà con il popolare framework HTTP Express, che già conoscevo. Offre inoltre hosting pronto all'uso per tutte le risorse statiche del mio sito. Vediamo come il server gestisce le richieste.

Quando un browser effettua una richiesta di navigazione al nostro server, passa attraverso il seguente flusso:

Una panoramica della generazione di una risposta di navigazione, lato server.

Il server instrada la richiesta in base all'URL e utilizza la logica dei modelli per creare un documento HTML completo. Utilizzo una combinazione di dati dell'API Stack Exchange e frammenti HTML parziali archiviati dal server localmente. Quando il nostro service worker sa come rispondere, può iniziare a trasmettere il codice HTML alla nostra applicazione web.

Ci sono due elementi che vale la pena approfondire in questa immagine: i percorsi e i modelli.

Routing

Per quanto riguarda il routing, il mio approccio era utilizzare la sintassi di routing nativo del framework Express. È sufficientemente flessibile da abbinare ai prefissi URL semplici e agli URL che includono parametri come parte del percorso. Qui creo una mappatura tra i nomi delle route con il pattern Express sottostante da abbinare.

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

export default routes;

Posso quindi fare riferimento a questa mappatura direttamente dal codice del server. Quando esiste una corrispondenza per un determinato pattern Express, il gestore appropriato risponde con la logica dei modelli specifica per la route corrispondente.

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

Modelli lato server

E che aspetto ha la logica dei modelli? Ho optato per un approccio che raggruppa frammenti HTML parziali in sequenza, uno dopo l'altro. Questo modello si presta bene allo streaming.

Il server restituisce immediatamente il boilerplate HTML iniziale e il browser è in grado di visualizzare immediatamente quella pagina parziale. Quando il server riunisce il resto delle origini dati, le trasmette in streaming al browser fino al completamento del documento.

Per capire cosa intendo, dai un'occhiata al codice Express di uno dei nostri percorsi:

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

Utilizzando il metodo write() dell'oggetto response e facendo riferimento a modelli parziali archiviati localmente, posso avviare immediatamente il flusso di risposte, senza bloccare alcuna origine dati esterna. Il browser prende questo codice HTML iniziale per visualizzare un'interfaccia e caricare il messaggio.

La parte successiva della pagina utilizza i dati dell'API Stack Exchange. Ottenere quei dati significa che il nostro server deve effettuare una richiesta di rete. L'app web non può eseguire il rendering di nient'altro finché non riceve una risposta e non la elabora, ma almeno gli utenti non guardano uno schermo vuoto nell'attesa.

Dopo aver ricevuto la risposta dall'API Stack Exchange, l'app web chiama una funzione di definizione dei modelli personalizzata per tradurre i dati dall'API nel relativo codice HTML.

Linguaggio dei modelli

I modelli possono essere un argomento sorprendentemente controverso e quello che ho scelto è solo uno dei tanti approcci. Dovrai sostituire la tua soluzione, soprattutto se hai collegamenti legacy con un framework di modelli esistente.

Per il mio caso d'uso serviva semplicemente fare affidamento sui valori letterali dei modelli di JavaScript, con una logica suddivisa in funzioni helper. Uno degli aspetti positivi della creazione di un MPA è che non devi tenere traccia degli aggiornamenti di stato ed eseguire il rendering del codice HTML, per cui un approccio di base che ha prodotto HTML statico ha funzionato per me.

Ecco un esempio di modello per la parte HTML dinamica dell'indice della mia applicazione web. Come per le mie route, la logica dei modelli è archiviata in un modulo ES che può essere importato sia nel server sia nel service worker.

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

Queste funzioni per il modello sono puramente JavaScript ed è utile suddividere la logica in funzioni helper più piccole, quando opportuno. Qui passo ogni elemento restituito nella risposta dell'API in una di queste funzioni, che crea un elemento HTML standard con tutti gli attributi appropriati impostati.

function questionCard({id, title}) {
  return `<a class="card"
             href="/questions/${id}"
             data-cache-url="${questionUrl(id)}">${title}</a>`;
}

Di particolare nota è un attributo dei dati che aggiungo a ogni link, data-cache-url, impostato sull'URL dell'API Stack Exchange necessario per visualizzare la domanda corrispondente. Non dimenticarlo. Lo rivedrò più tardi.

Tornando al mio gestore del percorso, una volta completato il modello, trasmetterò in streaming la parte finale del codice HTML della mia pagina al browser e termino lo stream. Questo indica al browser che il rendering progressivo è completo.

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

Ecco un breve tour della configurazione del mio server. Gli utenti che visitano la mia applicazione web per la prima volta riceveranno sempre una risposta dal server, ma quando un visitatore torna alla mia applicazione web, il mio service worker inizia a rispondere. Vediamoli in dettaglio.

Il service worker

Una panoramica della generazione di una risposta di navigazione, nel service worker.

Questo diagramma dovrebbe avere un aspetto familiare. Molti degli elementi che ho illustrato in precedenza sono disposti in una posizione leggermente diversa. Analizziamo il flusso delle richieste, tenendo in considerazione il service worker.

Il nostro service worker gestisce una richiesta di navigazione in entrata per un determinato URL e, proprio come ha fatto il mio server, utilizza una combinazione di logica di routing e modello per capire come rispondere.

L'approccio è lo stesso di prima, ma con diverse primitive di basso livello, come fetch() e l'API Cache Storage. Utilizzo queste origini dati per creare la risposta HTML, che il service worker ritrasmette all'app web.

Workbox

Anziché partire da zero con primitive di basso livello, creerò il mio service worker su un insieme di librerie di alto livello chiamate Workbox. Fornisce una solida base per qualsiasi service worker per la memorizzazione nella cache, il routing e la logica di generazione di risposte.

Routing

Proprio come per il codice lato server, il service worker deve sapere come abbinare una richiesta in entrata alla logica di risposta appropriata.

Il mio approccio è stato tradurre ogni route Express in un'espressione regolare corrispondente, utilizzando un'utile libreria chiamata regexparam. Una volta eseguita la traduzione, posso sfruttare il supporto integrato di Workbox per il routing delle espressioni regolari.

Dopo aver importato il modulo che contiene le espressioni regolari, registro ogni espressione regolare con il router di Workbox. All'interno di ogni route, posso fornire una logica di modelli personalizzati per generare una risposta. La creazione di modelli nel service worker è un po' più complessa di quella nel mio server di backend, ma Workbox aiuta a svolgere molte attività gravose.

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

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

Memorizzazione nella cache degli asset statici

Una parte fondamentale della storia dei modelli è assicurarmi che i miei modelli HTML parziali siano disponibili in locale tramite l'API Cache Storage e che siano aggiornati quando eseguo il deployment delle modifiche all'app web. La manutenzione della cache può essere soggetta a errori se eseguita a mano, quindi mi rivolgo a Workbox per gestire la precaching nell'ambito del processo di compilazione.

Indica a Workbox quali URL prememorizzare nella cache usando un file di configurazione, che indirizza alla directory che contiene tutti i miei asset locali insieme a un insieme di pattern da abbinare. Questo file viene letto automaticamente dall'interfaccia a riga di comando di Workbox, che viene run ogni volta che ricrei il sito.

module.exports = {
  globDirectory: 'build',
  globPatterns: ['**/*.{html,js,svg}'],
  // Other options...
};

Workbox acquisisce un'istantanea dei contenuti di ogni file e inserisce automaticamente l'elenco di URL e revisioni nel file finale del service worker. Ora Workbox ha tutto ciò di cui ha bisogno per rendere sempre disponibili e aggiornati i file pre-memorizzati nella cache. Il risultato è un file service-worker.js contenente qualcosa simile a quanto segue:

workbox.precaching.precacheAndRoute([
  {
    url: 'partials/about.html',
    revision: '518747aad9d7e',
  },
  {
    url: 'partials/foot.html',
    revision: '69bf746a9ecc6',
  },
  // etc.
]);

Per chi usa un processo di compilazione più complesso, Workbox offre sia un plug-in webpack sia un modulo nodo generico, oltre alla sua interfaccia a riga di comando.

Flussi di dati

Successivamente, voglio che il service worker riporti immediatamente il codice HTML parziale pre-cache all'app web. Questo è un aspetto fondamentale dell'essere "affidabilemente veloci": vedo sempre qualcosa di significativo sullo schermo. Fortunatamente, l'utilizzo dell'API Streams all'interno del nostro service worker lo rende possibile.

Probabilmente hai già sentito parlare dell'API Streams. Il mio collega Jake Archibald canta le sue lodi da anni. Ha fatto una previsione coraggiosa che il 2016 sarebbe stato l'anno dei flussi web. L'API Streams è fantastico oggi come due anni fa, ma con una differenza cruciale.

Anche se all'epoca supportava solo gli Streams da Chrome, l'API Streams ora è più ampiamente supportata. La storia complessiva è positiva e, con l'adeguato codice di fallback, niente potrà impedirti di utilizzare gli stream nel tuo service worker oggi.

Beh... potrebbe esserci un problema che ti ferma, e questo riguarda il funzionamento effettivo dell'API Streams. Espone un insieme molto potente di primitivi e gli sviluppatori che lo utilizzano possono creare flussi di dati complessi, come il seguente:

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

Tuttavia, comprendere tutte le implicazioni di questo codice potrebbe non essere adatto a tutti. Anziché analizzare questa logica, parliamo del mio approccio ai flussi di service worker.

Utilizzo un wrapper di alto livello nuovo di zecca, workbox-streams. Posso trasmetterlo in un mix di origini di flussi di dati, sia da cache che da dati di runtime che potrebbero provenire dalla rete. Workbox si occupa di coordinare le singole origini e di unirle in un'unica risposta in streaming.

Inoltre, Workbox rileva automaticamente se l'API Streams è supportata e, quando non lo è, crea una risposta equivalente non di streaming. Ciò significa che non devi preoccuparti di scrivere elementi di riserva, in quanto gli stream si avvicinano a un 100% di supporto del browser.

Memorizzazione nella cache del runtime

Vediamo come il mio service worker gestisce i dati di runtime dall'API Stack Exchange. Faccio uso del supporto integrato di Workbox per una strategia di memorizzazione nella cache inutilizzata durante la riconvalida, insieme alla scadenza per garantire che lo spazio di archiviazione dell'app web non aumenti senza limiti.

Ho impostato due strategie in Workbox per gestire le diverse origini che costituiranno la risposta in modalità flusso. Con poche chiamate di funzione e configurazioni, Workbox ci consente di fare ciò che altrimenti richiederebbe centinaia di righe di codice scritto a mano.

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})],
});

La prima strategia legge i dati pre-memorizzati nella cache, come i nostri modelli HTML parziali.

L'altra strategia implementa la logica di memorizzazione nella cache inutilizzata durante la riconvalida, insieme alla scadenza della cache meno recente quando raggiungiamo le 50 voci.

Ora che ho implementato queste strategie, non ti resta che dire a Workbox come utilizzarle per creare una risposta completa in modalità flusso. Passo un array di origini come funzioni, e ognuna di queste funzioni verrà eseguita immediatamente. Workbox acquisisce il risultato da ogni origine e lo trasmette all'app web, in sequenza, solo ritardando il completamento della funzione successiva nell'array.

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'}),
]);

Le prime due origini sono modelli parziali pre-memorizzati nella cache letti direttamente dall'API Cache Storage, pertanto saranno sempre disponibili immediatamente. Ciò garantisce che la nostra implementazione dei service worker risponda in modo affidabile e veloce alle richieste, proprio come il mio codice lato server.

La prossima funzione di origine recupera i dati dall'API Stack Exchange ed elabora la risposta nell'HTML previsto dall'app web.

La strategia di riconvalida in caso di inattività significa che, se ho una risposta precedentemente memorizzata nella cache per questa chiamata API, posso trasmetterla immediatamente alla pagina e aggiornare la voce della cache "in background" per la prossima volta che viene richiesta.

Infine, invio in streaming una copia memorizzata nella cache del piè di pagina e chiudo i tag HTML finali per completare la risposta.

La condivisione del codice mantiene sincronizzati gli elementi

Noterai che alcuni bit del codice del service worker hanno un aspetto familiare. L'HTML parziale e la logica dei modelli utilizzati dal mio service worker sono identici a quelli utilizzati dal mio gestore lato server. Questa condivisione del codice garantisce agli utenti un'esperienza coerente, sia che stiano visitando la mia app web per la prima volta o che tornino a una pagina visualizzata dal service worker. Questo è il bello del JavaScript isomorfico.

Miglioramenti dinamici e progressivi

Ho esaminato sia il server sia il service worker della PWA, ma c'è un'ultima parte di logica da trattare: c'è una piccola quantità di JavaScript che viene eseguita su ciascuna delle mie pagine, dopo che è stata trasmessa completamente in streaming.

Questo codice migliora progressivamente l'esperienza utente, ma non è fondamentale: l'app web continuerà a funzionare anche se non viene eseguita.

Metadati di pagina

La mia app utilizza JavaScipt lato client per aggiornare i metadati di una pagina in base alla risposta dell'API. Poiché utilizzo la stessa porzione iniziale di HTML memorizzata nella cache per ogni pagina, l'app web restituisce tag generici nell'intestazione del documento. Ma grazie al coordinamento tra il mio modello e il codice lato client, posso aggiornare il titolo della finestra utilizzando metadati specifici della pagina.

Nell'ambito del codice dei modelli, il mio approccio consiste nell'includere un tag script contenente la stringa di escape corretta.

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

Poi, una volta che la pagina è stata caricata, leggo la stringa e aggiorno il titolo del documento.

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

Se vuoi aggiornare altri metadati specifici di una pagina nella tua app web, puoi seguire lo stesso approccio.

UX offline

L'altro miglioramento progressivo che ho aggiunto è quello utilizzato per attirare l'attenzione sulle nostre funzionalità offline. Ho creato una PWA affidabile e voglio che gli utenti sapranno che quando sono offline possono comunque caricare le pagine visitate in precedenza.

Innanzitutto utilizzo l'API Cache Storage per ottenere un elenco di tutte le richieste API precedentemente memorizzate nella cache, che lo traduco in un elenco di URL.

Ricordi gli attributi speciali dei dati, di cui ho parlato, ciascuno contenente l'URL della richiesta API, necessario per visualizzare una domanda? Posso eseguire un controllo incrociato di questi attributi di dati con l'elenco di URL memorizzati nella cache e creare un array di tutti i link alle domande che non corrispondono.

Quando il browser entra in stato offline, eseguo un ciclo in sequenza dell'elenco dei link non memorizzati nella cache e oscura i link che non funzionano. Tieni presente che questo è solo un suggerimento visivo per l'utente che cosa dovrebbe aspettarsi da quelle pagine: in realtà non sto disattivando i link né impedendo all'utente di navigare.

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

Insidie comuni

Ho esaminato il mio approccio alla creazione di una PWA di più pagine. Quando scegli il tuo approccio, devi tenere in considerazione molti fattori e potresti dover fare scelte diverse da me. Questa flessibilità è uno dei grandi vantaggi della creazione per il web.

Quando si prendono delle decisioni architettoniche da soli, si possono presentare alcuni inconvenienti comuni e vorrei evitare un po' di fatica.

Non memorizzare nella cache l'HTML completo

Sconsiglio l'archiviazione di documenti HTML completi nella cache. Innanzitutto, è uno spreco di spazio. Se la tua app web utilizza la stessa struttura HTML di base per ogni pagina, finirai per archiviare sempre più copie dello stesso markup.

Ancora più importante, se implementi una modifica alla struttura HTML condivisa del tuo sito, tutte le pagine precedentemente memorizzate nella cache rimangono bloccate con il vecchio layout. Immagina la frustrazione di un visitatore di ritorno che vede un mix di pagine vecchie e nuove.

Deviazione server / service worker

L'altra insidia da evitare è la perdita della sincronizzazione tra server e service worker. Il mio approccio prevedeva l'uso di JavaScript isomorfico, in modo che lo stesso codice venisse eseguito in entrambe le posizioni. A seconda dell'architettura server esistente, questo non è sempre possibile.

A prescindere dalle decisioni di architettura che prendi, dovresti avere una strategia per l'esecuzione del codice di routing e modello equivalente nel tuo server e nel tuo service worker.

Scenari peggiori

Layout / design incoerente

Cosa succede quando ignori queste insidie? Sono possibili tutti i tipi di errori, ma la peggiore delle ipotesi è che un utente di ritorno visiti una pagina memorizzata nella cache con un layout molto inattivo, magari una con testo di intestazione obsoleto, oppure che utilizzi nomi di classi CSS non più validi.

Scenario peggiore: routing non funzionante

In alternativa, un utente potrebbe trovare un URL gestito dal tuo server, ma non dal tuo service worker. Un sito pieno di layout di zombie e strade senza uscita non è una PWA affidabile.

Suggerimenti per ottenere risultati

Ma non sei sola. I seguenti suggerimenti possono aiutarti a evitare questi insidie:

Utilizzare librerie di modelli e routing con implementazioni multilingue

Prova a utilizzare librerie di modelli e routing che includono implementazioni JavaScript. So che non tutti gli sviluppatori hanno il lusso di migrare dall'attuale server web e creare modelli.

Tuttavia, alcuni framework di deployment e modelli popolari hanno implementazioni in più linguaggi. Se riesci a trovare un codice che funziona con JavaScript e la lingua del tuo attuale server, devi fare un passo avanti per mantenere sincronizzati il worker e il server.

Preferisco i modelli sequenziali, invece che nidificati

Consiglio inoltre di utilizzare una serie di modelli sequenziali che possono essere trasmessi in streaming uno dopo l'altro. Non è un problema se parti successive della pagina utilizzano una logica di creazione dei modelli più complicata, purché tu riesca a trasmettere in streaming la parte iniziale del codice HTML il più rapidamente possibile.

Memorizza nella cache contenuti statici e dinamici nel tuo service worker

Per ottenere prestazioni ottimali, devi pre-cache tutte le risorse statiche critiche del tuo sito. Devi inoltre configurare la logica di memorizzazione nella cache di runtime per gestire i contenuti dinamici, come le richieste API. L'utilizzo di Workbox consente di basarti su strategie già testate e pronte per la produzione, invece di implementare tutto da zero.

Blocca sulla rete solo quando assolutamente necessario

Inoltre, dovresti bloccare sulla rete solo quando non è possibile trasmettere una risposta dalla cache. Mostrare immediatamente una risposta dell'API memorizzata nella cache può spesso portare a un'esperienza utente migliore rispetto all'attesa di dati aggiornati.

Risorse