Parliamo di… architettura?
Tratterò un argomento importante, ma potenzialmente frainteso: l'architettura che utilizzi per la tua app web e, in particolare, il modo in cui le tue decisioni architettoniche entrano in gioco quando crei un'app web progressiva.
"Architettura" può sembrare un termine vago e potrebbe non essere immediatamente chiaro il motivo per cui è importante. Un modo per pensare all'architettura è porsi le seguenti domande: quando un utente visita una pagina del mio sito, quale HTML viene caricato? E poi, cosa viene caricato quando visitano un'altra pagina?
Le risposte a queste domande non sono sempre semplici e, quando inizi a pensare alle app web progressive, possono diventare ancora più complicate. Il mio obiettivo è quindi illustrarti una possibile architettura che ho trovato efficace. In questo articolo, etichetterò le decisioni che ho preso come "il mio approccio " alla creazione di un'app web progressiva.
Puoi utilizzare il mio approccio per creare la tua PWA, ma allo stesso tempo esistono sempre altre valide alternative. Spero che vedere come tutti i pezzi si incastrano ti ispiri e che tu ti senta in grado di personalizzare questo progetto in base alle tue esigenze.
PWA di Stack Overflow
Per accompagnare questo articolo, ho creato una PWA di Stack Overflow. Passo molto tempo a leggere e contribuire a Stack Overflow e volevo creare un'app web che semplificasse la navigazione tra le domande frequenti su un determinato argomento. È basato sull'API Stack Exchange pubblica. È open source e puoi scoprire di più visitando il progetto GitHub.
App multipagina (MPA)
Prima di entrare nei dettagli, definiamo alcuni termini e spieghiamo alcuni elementi della tecnologia di base. Per prima cosa, parlerò di quelle che mi piace chiamare "app multipagina" o "MPA".
MPA è un nome sofisticato per l'architettura tradizionale utilizzata fin dagli albori del web. Ogni volta che un utente accede a un nuovo URL, il browser esegue progressivamente il rendering del codice HTML specifico per quella pagina. Non viene fatto alcun tentativo di preservare lo stato della pagina o i contenuti tra una navigazione e l'altra. Ogni volta che visiti una nuova pagina, ricominci da capo.
Questo è in contrasto con il 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 le MPA sono modelli ugualmente validi da utilizzare, ma per questo post volevo esplorare i concetti delle PWA nel contesto di un'app multipagina.
Velocità e affidabilità
Mi avrai sentito (e avrai sentito innumerevoli altre persone) usare l'espressione "app web progressiva" o PWA. Potresti già avere familiarità con alcuni dei materiali di base, disponibili altrove su questo sito.
Puoi pensare a una PWA come a un'app web che offre un'esperienza utente di prim'ordine e che si guadagna davvero un posto nella schermata Home dell'utente. L'acronimo "FIRE", che sta per Fast (veloce), Integrated (integrata), Reliable (affidabile) ed Engaging (coinvolgente), riassume tutti gli attributi da considerare quando si crea una PWA.
In questo articolo, mi concentrerò su un sottoinsieme di questi attributi: Veloce e Affidabile.
Velocità: sebbene "velocità" significhi cose diverse in contesti diversi, parlerò dei vantaggi in termini di velocità derivanti dal caricamento del minor numero possibile di dati dalla rete.
Affidabilità: la velocità pura non basta. 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.
Velocità affidabile: infine, riformulerò leggermente la definizione di PWA e vedremo cosa significa creare un'app con una velocità affidabile. Non basta essere veloci e affidabili solo quando si è su una rete a bassa latenza. La velocità affidabile significa che la velocità della tua app web è costante, indipendentemente dalle condizioni della rete sottostante.
Tecnologie abilitanti: service worker + API Cache Storage
Le PWA introducono un livello elevato di velocità e resilienza. Fortunatamente, la piattaforma web offre alcuni componenti di base per rendere possibile questo tipo di prestazioni. Mi riferisco ai service worker e all'API Cache Storage.
Puoi creare un service worker che ascolta le richieste in arrivo, ne trasmette alcune alla rete e memorizza una copia della risposta per un utilizzo futuro tramite l'API Cache Storage.
La volta successiva che l'app web effettua la stessa richiesta, il service worker può controllare le sue cache e restituire semplicemente la risposta memorizzata nella cache in precedenza.
Evitare la rete quando possibile è una parte fondamentale per offrire prestazioni affidabili e veloci.
JavaScript "isomorfo"
Un altro concetto che voglio trattare è quello che a volte viene definito JavaScript "isomorfo" o "universale". In parole povere, è 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 mio server di backend e il service worker.
Esistono molti approcci validi per condividere il codice in questo modo, ma il mio approccio è stato quello di utilizzare i moduli ES come codice sorgente definitivo. Poi ho eseguito il transpiling e il raggruppamento di questi moduli per il server e il service worker utilizzando una combinazione di Babel e Rollup. Nel mio progetto, i file con estensione del file .mjs sono codice che si trova in un modulo ES.
Il server
Tenendo presenti questi concetti e questa terminologia, vediamo come ho creato la mia PWA di Stack Overflow. Inizierò parlando del nostro server di backend e spiegando come si inserisce nell'architettura complessiva.
Cercavo una combinazione di un backend dinamico con un hosting statico e il mio approccio è stato quello di utilizzare la piattaforma Firebase.
Cloud Functions for Firebase avvia automaticamente un ambiente basato su Node quando riceve una richiesta in entrata e si integra con il popolare framework HTTP Express, che già conoscevo. Offre anche l'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, segue questo flusso:
Il server indirizza la richiesta in base all'URL e utilizza la logica di creazione dei modelli per creare un documento HTML completo. Utilizzo una combinazione di dati dell'API Stack Exchange e frammenti HTML parziali che il server archivia localmente. Una volta che il service worker sa come rispondere, può iniziare a trasmettere l'HTML alla nostra app web.
Ci sono due aspetti di questo quadro che vale la pena esplorare in modo più dettagliato: il routing e i modelli.
Routing
Per quanto riguarda il routing, il mio approccio è stato quello di utilizzare la sintassi di routing nativa del framework Express. È abbastanza flessibile da corrispondere a prefissi URL semplici, nonché a URL che includono parametri come parte del percorso. Qui creo una mappatura tra i nomi delle route e il pattern Express sottostante con cui eseguire la corrispondenza.
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 viene trovata una corrispondenza per un determinato pattern Express, il gestore appropriato risponde con una logica di creazione di 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 questa logica di creazione dei modelli? Ho scelto un approccio che metteva insieme frammenti HTML parziali in sequenza, uno dopo l'altro. Questo modello si presta bene allo streaming.
Il server invia immediatamente un boilerplate HTML iniziale e il browser è in grado di eseguire subito il rendering della pagina parziale. Man mano che il server mette insieme 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 per 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 lo stream di risposta, senza bloccare alcuna origine dati esterna. Il browser prende questo codice HTML iniziale e visualizza immediatamente un'interfaccia significativa e un messaggio di caricamento.
La parte successiva della nostra pagina utilizza i dati dell'API Stack Exchange. Per ottenere questi dati, 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 la elabora, ma almeno gli utenti non guardano uno schermo vuoto mentre aspettano.
Una volta che l'app web ha ricevuto la risposta dall'API Stack Exchange, chiama una funzione di creazione di modelli personalizzati per tradurre i dati dell'API nel codice HTML corrispondente.
Linguaggio di creazione dei modelli
La creazione di modelli può essere un argomento sorprendentemente controverso e quello che ho scelto è solo uno dei tanti approcci. Ti consigliamo di sostituire la tua soluzione, soprattutto se hai legami legacy con un framework di modelli esistente.
Per il mio caso d'uso, la soluzione migliore è stata fare affidamento sui template literal di JavaScript, con una logica suddivisa in funzioni helper. Uno dei vantaggi della creazione di un'MPA è che non devi tenere traccia degli aggiornamenti dello stato e ripetere il rendering dell'HTML, quindi un approccio di base che produceva HTML statico ha funzionato per me.
Ecco un esempio di come creo il modello della parte HTML dinamica dell'indice della mia app web. Come per i miei percorsi, la logica di creazione dei modelli è memorizzata in un modulo ES che può essere importato sia nel server che 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 dei modelli sono in JavaScript puro ed è utile suddividere la logica in funzioni helper più piccole, se opportuno. In questo caso, passo ciascuno degli elementi restituiti nella risposta dell'API a 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>`;
}
In particolare, ho aggiunto un attributo dati
a ogni link, data-cache-url, impostato sull'URL dell'API Stack Exchange
che mi serve per visualizzare la domanda corrispondente. Tienilo a mente. Ci tornerò più tardi.
Tornando al mio gestore di route, una volta completato il modello, trasmetto in streaming la parte finale dell'HTML della mia pagina al browser e termino lo stream. Questo è il segnale per il browser che il rendering progressivo è stato completato.
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();
});
Questo è un breve tour della configurazione del mio server. Gli utenti che visitano la mia app web per la prima volta riceveranno sempre una risposta dal server, ma quando un visitatore torna alla mia app web, il mio service worker inizierà a rispondere. Vediamo questo aspetto.
Il service worker
Questo diagramma dovrebbe esserti familiare: molti degli elementi che ho già trattato sono qui in una disposizione leggermente diversa. Esaminiamo il flusso di richieste, tenendo conto del 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 di creazione di modelli per capire come rispondere.
L'approccio è lo stesso di prima, ma con primitive di basso livello diverse, come fetch() e l'API Storage Cache. Utilizzo queste origini dati per creare la risposta HTML, che il service worker restituisce all'app web.
Workbox
Anziché iniziare da zero con primitive di basso livello, creerò il mio service worker su un insieme di librerie di alto livello chiamate Workbox. Fornisce una base solida per la logica di memorizzazione nella cache, routing e generazione delle risposte di qualsiasi service worker.
Routing
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 quello di 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 con le espressioni regolari, registro ogni espressione regolare con il router di Workbox. All'interno di ogni route posso fornire una logica di modello personalizzata per generare una risposta. La creazione di modelli nel service worker è un po' più complessa rispetto al server di backend, ma Workbox aiuta molto con il lavoro più pesante.
import regExpRoutes from './regexp-routes.mjs';
workbox.routing.registerRoute(
regExpRoutes.get('index')
// Templating logic.
);
Memorizzazione nella cache degli asset statici
Una parte fondamentale della creazione di modelli è assicurarsi che i miei modelli HTML parziali siano disponibili localmente tramite l'API Cache Storage e che vengano aggiornati quando eseguo il deployment delle modifiche nell'app web. La manutenzione della cache può essere soggetta a errori se eseguita manualmente, quindi mi rivolgo a Workbox per gestire il pre-memorizzazione nella cache nell'ambito del mio processo di build.
Indico a Workbox quali URL prememorizzare nella cache utilizzando un file di configurazione, che punta alla directory contenente 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 eseguita ogni volta che ricreo 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 del service worker finale. Workbox ora ha tutto ciò di cui ha bisogno per rendere i file prememorizzati nella cache sempre disponibili e aggiornati. Il risultato è un file service-worker.js che contiene un testo simile al seguente:
workbox.precaching.precacheAndRoute([
{
url: 'partials/about.html',
revision: '518747aad9d7e',
},
{
url: 'partials/foot.html',
revision: '69bf746a9ecc6',
},
// etc.
]);
Per chi utilizza un processo di build più complesso, Workbox dispone sia di un plug-in webpack sia di un modulo Node generico, oltre alla sua interfaccia a riga di comando.
Streaming
Successivamente, voglio che il service worker trasmetta immediatamente l'HTML parziale prememorizzato nella cache all'app web. Questa è una parte fondamentale dell'essere "affidabilmente veloci": ottengo sempre qualcosa di significativo sullo schermo immediatamente. Fortunatamente, l'utilizzo dell'API Streams all'interno del nostro service worker lo rende possibile.
Forse hai già sentito parlare dell'API Streams. Il mio collega Jake Archibald ne tesse le lodi da anni. Ha fatto l'audace previsione che il 2016 sarebbe stato l'anno degli stream web. L'API Streams è fantastica oggi come lo era due anni fa, ma con una differenza fondamentale.
All'epoca solo Chrome supportava gli stream, ma ora l'API Streams è più ampiamente supportata. La storia complessiva è positiva e, con un codice di riserva appropriato, non c'è nulla che ti impedisca di utilizzare gli stream nel tuo service worker oggi stesso.
Beh… potrebbe esserci una cosa che ti frena, ovvero capire come funziona effettivamente l'API Streams. Espone un insieme molto potente di primitive e gli sviluppatori che lo utilizzano possono creare flussi di dati complessi, come i seguenti:
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 facile per tutti. Anziché analizzare questa logica, parliamo del mio approccio allo streaming dei service worker.
Sto utilizzando un wrapper di alto livello completamente nuovo, workbox-streams.
In questo modo, posso trasmettere un mix di origini di flussi, 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, in caso contrario, crea una risposta equivalente non in streaming. Ciò significa che non devi preoccuparti di scrivere i fallback, poiché gli stream si avvicinano al 100% del supporto dei browser.
Memorizzazione nella cache di runtime
Vediamo come il mio service worker gestisce i dati di runtime dall'API Stack Exchange. Sto utilizzando il supporto integrato di Workbox per una strategia di memorizzazione nella cache stale-while-revalidate, insieme alla scadenza per garantire che lo spazio di archiviazione dell'app web non cresca senza limiti.
Ho configurato due strategie in Workbox per gestire le diverse origini che comporranno la risposta in streaming. 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 prememorizzati nella cache, come i nostri modelli HTML parziali.
L'altra strategia implementa la logica di memorizzazione nella cache stale-while-revalidate, insieme alla scadenza della cache utilizzata meno di recente una volta raggiunte le 50 voci.
Ora che ho implementato queste strategie, non mi resta che indicare a Workbox come utilizzarle per creare una risposta di streaming completa. Passo un array di origini come funzioni e ognuna di queste funzioni verrà eseguita immediatamente. Workbox prende il risultato di ogni origine e lo trasmette in streaming all'app web, in sequenza, ritardando solo se la funzione successiva nell'array non è ancora stata completata.
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 prememorizzati nella cache letti direttamente dall'API Cache Storage, quindi saranno sempre disponibili immediatamente. In questo modo, la nostra implementazione del service worker sarà sempre veloce nel rispondere alle richieste, proprio come il mio codice lato server.
La nostra prossima funzione di origine recupera i dati dall'API Stack Exchange ed elabora la risposta nel codice HTML previsto dall'app web.
La strategia stale-while-revalidate prevede che, se ho una risposta precedentemente memorizzata nella cache per questa chiamata API, potrò trasmetterla immediatamente alla pagina, aggiornando al contempo la voce della cache "in background" per la richiesta successiva.
Infine, trasmetto in streaming una copia memorizzata nella cache del mio piè di pagina e chiudo i tag HTML finali per completare la risposta.
La condivisione del codice mantiene sincronizzati i dati
Noterai che alcune parti del codice del service worker sembrano familiari. La logica di templating e HTML parziale utilizzata dal mio service worker è identica a quella utilizzata dal mio gestore lato server. Questa condivisione del codice garantisce agli utenti un'esperienza coerente, sia che visitino la mia app web per la prima volta sia che tornino a una pagina visualizzata dal service worker. È questo l'aspetto più vantaggioso di JavaScript isomorfo.
Miglioramenti dinamici e progressivi
Ho esaminato sia il server che il service worker per la mia 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 sono state trasmesse in streaming completamente.
Questo codice migliora progressivamente l'esperienza utente, ma non è fondamentale: l'app web funzionerà comunque anche se non viene eseguito.
Metadati di pagina
La mia app utilizza JavaScript lato client per aggiornare i metadati di una pagina in base alla risposta dell'API. Poiché utilizzo lo stesso bit iniziale di HTML memorizzato nella cache per ogni pagina, l'app web finisce con tag generici nell'intestazione del documento. Tuttavia, grazie al coordinamento tra il codice di creazione dei modelli e il codice lato client, posso aggiornare il titolo della finestra utilizzando i metadati specifici della pagina.
Nell'ambito del codice di creazione dei modelli, il mio approccio consiste nell'includere un tag di script contenente la stringa con escape corretto.
const metadataScript = `<script>
self._title = '${escape(item.title)}';
</script>`;
Poi, una volta caricata la pagina, leggo la stringa e aggiorno il titolo del documento.
if (self._title) {
document.title = unescape(self._title);
}
Se vuoi aggiornare altri metadati specifici di pagina nella tua app web, puoi seguire lo stesso approccio.
UX offline
L'altro potenziamento progressivo che ho aggiunto viene utilizzato per attirare l'attenzione sulle nostre funzionalità offline. Ho creato una PWA affidabile e voglio che gli utenti sappiano 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 memorizzate nella cache in precedenza e lo traduco in un elenco di URL.
Ricordi gli attributi dei dati speciali di cui ho parlato, ognuno dei quali contiene l'URL della richiesta API necessaria per visualizzare una domanda? Posso fare un controllo incrociato di questi attributi dei dati con l'elenco degli 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 loop nell'elenco dei link non memorizzati nella cache e disattivo quelli che non funzioneranno. Tieni presente che si tratta solo di un suggerimento visivo per l'utente su cosa aspettarsi da queste pagine. 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);
Errori comuni
Abbiamo visto il mio approccio alla creazione di una PWA di più pagine. Ci sono molti fattori da considerare quando si sviluppa un approccio personale e potresti fare scelte diverse dalle mie. Questa flessibilità è uno dei grandi vantaggi dello sviluppo per il web.
Ci sono alcune insidie comuni che potresti incontrare quando prendi le tue decisioni sull'architettura e voglio risparmiarti qualche problema.
Non memorizzare nella cache l'intero codice HTML
Ti sconsiglio di archiviare 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 copie dello stesso markup più e più volte.
Ancora più importante, se esegui il deployment di una modifica alla struttura HTML condivisa del tuo sito, ogni pagina memorizzata nella cache in precedenza rimane bloccata con il vecchio layout. Immagina la frustrazione di un visitatore di ritorno che vede un mix di pagine vecchie e nuove.
Drift del server/service worker
L'altra trappola da evitare riguarda la mancata sincronizzazione tra il server e il service worker. Il mio approccio è stato quello di utilizzare JavaScript isomorfo, in modo che lo stesso codice venisse eseguito in entrambi i punti. A seconda dell'architettura del server esistente, questo non è sempre possibile.
Indipendentemente dalle decisioni di architettura che prendi, devi avere una strategia per eseguire il codice di routing e di templating equivalente nel server e nel service worker.
Scenari peggiori
Layout/design incoerente
Cosa succede quando ignori queste insidie? Beh, sono possibili tutti i tipi di errori, ma lo scenario peggiore è che un utente di ritorno visiti una pagina memorizzata nella cache con un layout molto obsoleto, ad esempio con un testo dell'intestazione non aggiornato o che utilizza nomi di classi CSS non più validi.
Scenario peggiore: routing interrotto
In alternativa, un utente potrebbe imbattersi in un URL gestito dal tuo server, ma non dal tuo service worker. Un sito pieno di layout zombie e vicoli ciechi non è una PWA affidabile.
Suggerimenti per il successo
Ma non devi affrontare tutto con le tue sole forze. I seguenti suggerimenti possono aiutarti a evitare questi errori:
Utilizza librerie di modelli e routing con implementazioni multilingue
Prova a utilizzare librerie di modelli e routing con implementazioni JavaScript. Ora, so che non tutti gli sviluppatori hanno il lusso di migrare dal proprio server web e linguaggio di template attuale.
Tuttavia, esistono implementazioni di diversi framework di routing e modelli popolari in più linguaggi. Se riesci a trovarne uno che funzioni sia con JavaScript sia con il linguaggio del tuo server attuale, sei un passo più vicino a mantenere sincronizzati il service worker e il server.
Preferisci i modelli sequenziali a quelli nidificati
Successivamente, ti consiglio di utilizzare una serie di modelli sequenziali che possono essere trasmessi in streaming uno dopo l'altro. Non è un problema se le parti successive della pagina utilizzano una logica di template più complessa, purché tu possa eseguire lo streaming della parte iniziale del codice HTML il più rapidamente possibile.
Memorizza nella cache i contenuti statici e dinamici nel service worker
Per ottenere il massimo rendimento, devi prememorizzare nella cache tutte le risorse statiche critiche del tuo sito. Devi anche configurare la logica di memorizzazione nella cache di runtime per gestire i contenuti dinamici, come le richieste API. L'utilizzo di Workbox ti consente di basarti su strategie ben testate e pronte per la produzione, anziché implementare tutto da zero.
Blocca sulla rete solo se è assolutamente necessario
Inoltre, dovresti bloccare la rete solo quando non è possibile trasmettere in streaming una risposta dalla cache. La visualizzazione immediata di una risposta dell'API memorizzata nella cache può spesso portare a un'esperienza utente migliore rispetto all'attesa di dati aggiornati.