Creare una PWA in Google, parte 1

Ciò che il team di bollettino ha imparato sui lavoratori dei servizi 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.

In questo primo post parleremo di alcune informazioni di base, quindi di tutto ciò che abbiamo imparato sui service worker.

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 bollettino, abbiamo utilizzato Polymer, ma qualsiasi framework moderno e ben supportato funzionerà.

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 a mano lo script del service worker. 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 il 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 service worker, altrimenti potresti riscontrare 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, 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 client precedente con la versione N, il processo di upgrade del service worker si blocca perché le connessioni attive sono ancora aperte alla versione precedente del database
  6. Il service worker si blocca e non esegue mai l'installazione

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 aveva caricato file di grandi dimensioni sul server e salvato su IDB, la nostra soluzione per l'interruzione dei caricamenti parziali è stata quella di sfruttare il sistema ripristinabile della nostra libreria di caricamento interna, salvare l'URL di caricamento ripristinabile su IDB prima del caricamento e utilizzare questo URL per riprendere un caricamento se non era 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 adatto a 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, 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 in modo da 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 fornisce una serie di strumenti di debugging utili per le PWA. Scansiona un sito e genera report relativi a 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 nella nostra CI avrebbe evitato 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 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 mitigato se i contenuti non vengono pubblicati mentre sono inattivi o fanno scadere manualmente i contenuti dopo alcune settimane. In pratica, non abbiamo mai riscontrato problemi legati a client inattivi. La severità di un determinato team dipende dal suo caso d'uso specifico, ma le PWA offrono una flessibilità notevolmente 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 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 worker del servizio venga modificato se un file statico memorizzato nella cache viene modificato

Un pattern PWA comune per un service worker prevede l'installazione di tutti i file statici dell'applicazione durante la sua fase install, che consente ai client di accedere alla cache dell'API Cache Storage direttamente 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. Per eseguire questa operazione manualmente, incorporando un hash del set di file di risorse statiche all'interno del nostro script del service worker, ogni release generava un file JavaScript del service worker distinto. Le librerie dei 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à 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é questi file erano solo moduli JS standard, potrebbero essere più facilmente testati nelle unità con librerie di test standard.

Non perderti 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: