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 aggiornarsi automaticamente, il che consente di ridurre la quantità di client obsoleti in circolazione. Siamo riusciti a implementare modifiche di interruzione del backend con un tempo di migrazione molto breve per i clienti.
  • Si integra facilmente 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 worker di servizio. I service worker offrono molte funzionalità, come strategie di memorizzazione nella cache avanzate, funzionalità offline, sincronizzazione in background e così via. Sebbene i service worker aggiungano un po' di complessità, abbiamo riscontrato che i vantaggi superano la complessità aggiuntiva.

Se possibile, generalo

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

Detto questo, a causa del nostro tech stack 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ù, consulta la sezione Insidie per i service worker non generati.

Non tutte le librerie sono compatibili con Service Worker

Alcune librerie JS fanno supposizioni che non funzionano come previsto quando vengono eseguite da un servizio 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.

Evitare 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. La nuova app web viene inviata con la versione N+1 di IDB
  3. L'utente visita la PWA, che 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 worker di servizio 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 lungo termine deve essere riprendibile 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 dipendere dallo stato globale

Poiché i worker di servizio 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 con parsimonia i dati memorizzati nelle variabili globali, 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, questo accade esattamente al contrario di quanto vuoi, in particolare quando gli aggiornamenti vengono eseguiti in modo lazy. 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. In questo modo, gli sviluppatori ricevono 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 fornisce una serie di strumenti di debugging 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. Ci è successo una volta, quando il service worker non si installava e non ce ne siamo accorti 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. In questo modo, si riduce notevolmente la quantità di client obsoleti in circolazione. Quando l'utente apriva la nostra app, il service worker 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 ha ignorato questa richiesta, al successivo aggiornamento della pagina riceverà 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. Poiché l'app veniva pubblicata in stato non aggiornato, era possibile che i client meno recenti fossero presenti se l'utente non aveva aperto l'app per 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.

Ottenere i valori dei cookie in un worker di servizio

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 worker di servizio, 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 worker del servizio venga modificato se un file statico memorizzato nella cache viene modificato

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 di worker di servizio funzionano aggiungendo ascoltatori 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 questa operazione è delegare tutta l'implementazione a un altro file, che è più facilmente testabile.

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

A causa delle difficoltà di test di unità di uno script di worker di servizio, abbiamo mantenuto lo script di base del worker di servizio il più essenziale possibile, 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: