Creare una PWA in Google, parte 1

Cosa ha imparato il team di Bulletin sui service worker durante lo sviluppo di una PWA.

Douglas Parker
Douglas Parker
Joel Riley
Joel Riley
Dikla Cohen
Dikla Cohen

Questo è il primo di una serie di post del blog sulle lezioni apprese dal team di Google Bulletin durante la creazione di una PWA rivolta all'esterno. In questi post condivideremo alcune delle sfide che abbiamo affrontato, gli approcci che abbiamo adottato per superarle e consigli generali per evitare errori. Non si tratta in alcun modo di una panoramica completa delle PWA. L'obiettivo è condividere le lezioni apprese dall'esperienza del nostro team.

Per questo primo post, daremo alcune informazioni di base e poi entreremo nel vivo di tutto ciò che abbiamo imparato sui worker di servizio.

Sfondo

Bulletin è stato in fase di sviluppo attivo da metà 2017 a metà 2019.

Perché abbiamo scelto di creare una PWA

Prima di approfondire il processo di sviluppo, esaminiamo perché la creazione di una PWA era un'opzione interessante per questo progetto:

  • Possibilità di eseguire l'iterazione rapidamente. Particolarmente utile perché Bulletin verrà testato in più mercati.
  • Base di codice singola. I nostri utenti erano suddivisi in modo approssimativamente uniforme tra Android e iOS. Una PWA ci ha consentito di creare un'unica app web che funzionasse su entrambe le piattaforme. Ciò ha aumentato la velocità e l'impatto del team.
  • Aggiornato rapidamente e indipendentemente dal comportamento dell'utente. Le PWA possono aggiornare automaticamente il che riduce la quantità di client obsoleti. Siamo riusciti a implementare modifiche di interruzione del backend con un tempo di migrazione molto breve per i clienti.
  • Facilmente integrato con app proprietarie e di terze parti. Queste integrazioni erano un requisito per l'app. Con una PWA spesso significava semplicemente aprire un URL.
  • È stata rimossa la difficoltà di installare un'app.

Il nostro framework

Per Bulletin abbiamo utilizzato Polymer, ma è possibile utilizzare qualsiasi framework moderno e ben supportato.

Cosa abbiamo imparato sui service worker

Non puoi avere una PWA senza un service worker. I Service worker offrono molta potenza, ad esempio strategie avanzate di memorizzazione nella cache, funzionalità offline, sincronizzazione in background e così via. Sebbene i service worker aggiungano una certa complessità, abbiamo scoperto che i loro vantaggi superano la maggiore complessità.

Se possibile, generalo

Evita di scrivere uno script di service worker manualmente. La scrittura manuale dei service worker richiede la gestione manuale delle risorse memorizzate nella cache e la logica di riscrittura comune alla maggior parte delle librerie dei service worker, ad esempio Workbox.

Detto questo, a causa del nostro stack tecnico interno non abbiamo potuto utilizzare una libreria per generare e gestire il nostro service worker. Le informazioni riportate di seguito a volte rifletteranno questo approccio. Per saperne di più, vai a Insidie per i service worker non generati.

Non tutte le librerie sono compatibili con il service worker

Alcune librerie JS partono da ipotesi che non funzionano come previsto quando vengono eseguite da un service worker. Ad esempio, se window o document sono disponibili o se utilizzi un'API non disponibile per i worker di servizio (XMLHttpRequest, archiviazione locale e così via). Assicurati che le librerie fondamentali necessarie per la tua applicazione siano compatibili con i worker di servizio. Per questa particolare PWA, volevamo utilizzare gapi.js per l'autenticazione, ma non siamo riusciti a farlo perché non supportava i service worker. Gli autori delle librerie devono anche ridurre o rimuovere, ove possibile, le assunzioni non necessarie sul contesto JavaScript per supportare i casi d'uso dei worker di servizio, ad esempio evitando le API incompatibili con i worker di servizio e evitando lo stato globale.

Evita di accedere a IndexedDB durante l'inizializzazione

Non leggere IndexedDB durante l'inizializzazione dello script del servizio worker, altrimenti potresti trovarti in questa situazione indesiderata:

  1. L'utente ha un'app web con IndexedDB (IDB) versione N
  2. Viene eseguito il push della nuova app web con IDB versione N+1
  3. L'utente visita la PWA e questo attiva il download del nuovo service worker
  4. Il nuovo service worker legge da IDB prima di registrare il gestore di eventi install, attivando un ciclo di upgrade di IDB per passare da N a N+1
  5. Poiché l'utente ha un vecchio client con la versione N, il processo di upgrade del service worker si blocca perché le connessioni attive sono ancora aperte alla vecchia versione del database
  6. Il service worker si blocca e non viene mai installato

Nel nostro caso, la cache è stata invalidata durante l'installazione del service worker, quindi se il service worker non è mai stato installato, gli utenti non hanno mai ricevuto l'app aggiornata.

Rendilo resiliente

Sebbene gli script di service worker vengano eseguiti in background, possono anche essere interrotti in qualsiasi momento, anche in mezzo a operazioni di I/O (rete, IDB e così via). Qualsiasi processo a lunga esecuzione dovrebbe essere ripristinabile in qualsiasi momento.

Nel caso di un processo di sincronizzazione che caricava file di grandi dimensioni sul server e li salvava in IDB, la nostra soluzione per i caricamenti parziali interrotti è stata quella di sfruttare il sistema di caricamento riavviabile della nostra libreria di caricamento interna, salvando l'URL di caricamento riavviabile in IDB prima del caricamento e utilizzando questo URL per riprendere un caricamento se non è stato completato la prima volta. Inoltre, prima di qualsiasi operazione di I/O di lunga durata, lo stato veniva salvato nell'IDB per indicare dove si trovava il processo per ogni record.

Non dipendono dallo stato globale

Poiché i service worker esistono in un contesto diverso, molti simboli che potresti aspettarti di trovare non sono presenti. Gran parte del nostro codice veniva eseguito sia in un contesto window sia in un contesto di worker di servizio (ad esempio logging, flag, sincronizzazione e così via). Il codice deve essere difensivo in merito ai servizi che utilizza, ad esempio lo spazio di archiviazione locale o i cookie. Puoi utilizzare globalThis per fare riferimento all'oggetto globale in un modo che funzioni in tutti i contesti. Utilizza inoltre i dati memorizzati nelle variabili globali con parsimonia, in quanto non è garantito quando lo script verrà terminato e lo stato verrà eliminato.

Sviluppo locale

Un componente importante dei worker di servizio è la memorizzazione nella cache delle risorse in locale. Tuttavia, durante lo sviluppo, è proprio l'opposto di ciò che vuoi, in particolare quando gli aggiornamenti vengono eseguiti lentamente. Vuoi comunque installare il worker del server per poter eseguire il debug dei problemi o utilizzare altre API come la sincronizzazione in background o le notifiche. Su Chrome puoi farlo tramite Chrome DevTools attivando la casella di controllo Ignora per la rete (riquadro Applicazione > riquadro Worker di servizio) e la casella di controllo Disattiva cache nel riquadro Rete per disattivare anche la cache della memoria. Per coprire più browser, abbiamo optato per una soluzione diversa includendo un flag per disattivare la memorizzazione nella cache nel nostro service worker, che è abilitato per impostazione predefinita nelle build per sviluppatori. Ciò garantisce che gli sviluppatori ricevano sempre le modifiche più recenti senza problemi di memorizzazione nella cache. È importante includere anche l'intestazione Cache-Control: no-cache per impedire al browser di memorizzare nella cache gli asset.

Faro

Lighthouse offre una serie di strumenti di debug utili per le PWA. Esegue la scansione di un sito e genera report che riguardano PWA, rendimento, accessibilità, SEO e altre best practice. Ti consigliamo di eseguire Lighthouse con integrazione continua per ricevere un avviso se violi uno dei criteri per essere una PWA. In realtà ci è successo una volta, quando il service worker non stava installando e non ce ne accorgevamo prima di un push in produzione. Avere Lighthouse nell'ambito del nostro CI avrebbe prevento questo problema.

Adotta la distribuzione continua

Poiché i worker possono aggiornarsi automaticamente, gli utenti non sono in grado di limitare gli upgrade. Ciò riduce notevolmente la quantità di client obsoleti in circolazione. Quando l'utente apriva la nostra app, il worker di servizio pubblicava il vecchio client mentre scaricava in modo lazy il nuovo client. Una volta scaricato il nuovo client, all'utente verrà chiesto di aggiornare la pagina per accedere alle nuove funzionalità. Anche se l'utente avesse ignorato questa richiesta, la prossima volta che aggiornava la pagina avrebbe ricevuto la nuova versione del client. Di conseguenza, è abbastanza difficile per un utente rifiutare gli aggiornamenti nello stesso modo in cui può farlo per le app per iOS/Android.

Siamo riusciti a implementare modifiche al backend che causano interruzioni con un tempo di migrazione molto breve per i clienti. In genere, diamo agli utenti un mese di tempo per eseguire l'aggiornamento ai client più recenti prima di apportare modifiche sostanziali. Dal momento che l'app veniva pubblicata quando non era attiva, era possibile che i client meno recenti potessero esistere anche se l'utente non l'aveva aperta da molto tempo. Su iOS, i worker di servizio vengono espulsi dopo un paio di settimane quindi questo caso non si verifica. Per Android, questo problema potrebbe essere attenuato non pubblicando i contenuti se non sono aggiornati o impostando manualmente la scadenza dei contenuti dopo alcune settimane. In pratica, non abbiamo mai riscontrato problemi con i client inattivi. Il livello di severità che un determinato team vuole applicare dipende dal suo caso d'uso specifico, ma le PWA offrono una flessibilità molto maggiore rispetto alle app per iOS/Android.

Recupero dei valori dei cookie in un service worker

A volte è necessario accedere ai valori dei cookie in un contesto di worker di servizio. Nel nostro caso, abbiamo dovuto accedere ai valori dei cookie per generare un token per autenticare le richieste API proprietarie. In un work worker, le API sincrone come document.cookies non sono disponibili. Puoi sempre inviare un messaggio ai client attivi (con finestra) dal service worker per richiedere i valori dei cookie, anche se è possibile che il service worker venga eseguito in background senza client con finestra disponibili, ad esempio durante una sincronizzazione in background. Per risolvere il problema, abbiamo creato un endpoint sul nostro server frontend che semplicemente restituisce il valore del cookie al client. Il service worker ha inviato una richiesta di rete a questo endpoint e ha letto la risposta per ottenere i valori dei cookie.

Con il rilascio dell'API Cookie Store, questa soluzione alternativa non dovrebbe più essere necessaria per i browser che la supportano, in quanto fornisce accesso asincrono ai cookie del browser e può essere utilizzata direttamente dal service worker.

Insidie per i service worker non generati

Assicurati che lo script del service worker venga modificato se vengono apportate modifiche a un file statico memorizzato nella cache

Un pattern PWA comune prevede che un worker di servizio installi tutti i file di applicazione statici durante la fase install, in modo che i client possano accedere direttamente alla cache dell'API Cache Storage per tutte le visite successive . I worker del servizio vengono installati solo quando il browser rileva che lo script del worker del servizio è cambiato in qualche modo, quindi abbiamo dovuto assicurarci che il file dello script del worker del servizio stesso sia cambiato in qualche modo quando un file memorizzato nella cache è cambiato. Lo abbiamo fatto manualmente incorporando un hash del set di file di risorse statiche nello script del nostro worker di servizio, in modo che ogni release producesse un file JavaScript distinto del worker di servizio. Le librerie di service worker come Workbox automatizzano questo processo.

Test delle unità

Le API dei service worker funzionano aggiungendo listener di eventi all'oggetto globale. Ad esempio:

self.addEventListener('fetch', (evt) => evt.respondWith(fetch('/foo')));

Questo può essere complicato da testare perché devi simulare l'attivatore dell'evento, l'oggetto evento, attendere il callback respondWith() e poi attendere la promessa, prima di eseguire infine l'affermazione sul risultato. Un modo più semplice per strutturare questo aspetto è delegare tutta l'implementazione a un altro file, che è più facilmente testato.

import fetchHandler from './fetch_handler.js';
self.addEventListener('fetch', (evt) => evt.respondWith(fetchHandler(evt)));

A causa delle difficoltà legate al test delle unità di uno script del service worker, abbiamo mantenuto lo script del service worker principale il più possibile essenziale, suddividendo la maggior parte dell'implementazione in altri moduli. Poiché si trattava solo di moduli JS standard, potevano essere testati più facilmente con librerie di test standard.

Continua a seguirci per le parti 2 e 3

Nelle parti 2 e 3 di questa serie parleremo della gestione dei contenuti multimediali e di problemi specifici di iOS. Se vuoi chiederci di più sulla creazione di una PWA in Google, visita i nostri profili autore per scoprire come contattarci: