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 bollettino 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 le insidie. Non si tratta assolutamente di una panoramica completa delle PWA. L'obiettivo è condividere quanto appreso 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.

Contesto

Il bollettino era in fase di sviluppo da metà 2017 a metà del 2019.

Perché abbiamo scelto di creare una PWA

Prima di approfondire il processo di sviluppo, vediamo perché lo sviluppo di una PWA è stata un'opzione interessante per questo progetto:

  • Capacità di iterazione rapida. Particolarmente utile perché bollettino verrebbe utilizzato in più mercati.
  • Singolo codebase. I nostri utenti erano divisi quasi equamente tra Android e iOS. L'utilizzo di PWA ci consente di creare un'unica app web che funzionasse su entrambe le piattaforme. Ciò ha aumentato la velocità e l'impatto del team.
  • Aggiornamento rapido e indipendentemente dal comportamento degli utenti. Le PWA possono aggiornare automaticamente il che riduce la quantità di client obsoleti. Abbiamo eseguito il push delle modifiche interrompenti al backend con un tempo di migrazione molto breve per i client.
  • 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.
  • Hai rimosso le difficoltà legate all'installazione di 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 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 puoi, genera

Evita di scrivere uno script del service worker a mano. 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 apprese di seguito rispecchiano a volte questo aspetto. 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, supponendo che siano disponibili window o document o utilizzare un'API non disponibile per i service worker (XMLHttpRequest, archiviazione locale e così via). Assicurati che tutte le librerie critiche necessarie per la tua applicazione siano compatibili con il service worker. Per questa PWA specifica, volevamo utilizzare gapi.js per l'autenticazione, ma non è stato possibile farlo perché non supportava i service worker. Gli autori delle librerie dovrebbero anche ridurre o rimuovere le ipotesi non necessarie sul contesto JavaScript, ove possibile, per supportare casi d'uso dei service worker, ad esempio evitando API incompatibili con i service worker ed evitando lo stato globale.

Evita 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 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 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 al momento dell'installazione del service worker, quindi se il service worker non è mai installato, gli utenti non hanno mai ricevuto l'app aggiornata.

Rendilo resiliente

Sebbene gli script dei service worker vengano eseguiti in background, possono essere terminati in qualsiasi momento, anche nel bel mezzo delle 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 a lunga esecuzione, lo stato veniva salvato in IDB per indicare in quale fase del processo ci trovavamo per ogni record.

Non dipendono dallo stato globale

Poiché i service worker esistono in un contesto diverso, molti simboli che potresti aspettarti non sono presenti. Gran parte del nostro codice è stato eseguito sia in un contesto window sia in un contesto di service worker (come logging, flag, sincronizzazione e così via). Il codice deve essere difensivo nei confronti dei servizi che utilizza, come archiviazione locale o cookie. Puoi utilizzare globalThis per fare riferimento all'oggetto globale in modo da funzionare in tutti i contesti. Inoltre, utilizza con parsimonia anche i dati archiviati nelle variabili globali, dato che non esiste alcuna garanzia su quando lo script verrà terminato e lo stato rimosso.

Sviluppo locale

Una componente importante dei service worker è la memorizzazione nella cache delle risorse localmente. 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 in modo da poter eseguire il debug dei problemi o utilizzare altre API, come la sincronizzazione in background o le notifiche. Su Chrome puoi usare Chrome DevTools selezionando la casella di controllo Ignora per la rete (riquadro Applicazione > riquadro Service worker) oltre ad attivare la casella di controllo Disabilita cache nel riquadro Rete per disattivare anche la cache in memoria. Per includere più browser, abbiamo optato per una soluzione diversa includendo un flag per disabilitare la memorizzazione nella cache nel nostro service worker, che è abilitato per impostazione predefinita nelle build degli 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 gli asset nella cache.

Faro

Lighthouse offre una serie di strumenti di debug 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 l'integrazione continua per ricevere un avviso se uno dei criteri è 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.

Sfrutta la distribuzione continua

Poiché i service worker possono eseguire gli aggiornamenti automaticamente, gli utenti non hanno la possibilità di limitare gli upgrade. Questo riduce significativamente la quantità di client obsoleti in circolazione. Quando l'utente apriva la nostra app, il service worker mostrava il vecchio client mentre scaricava lentamente il nuovo client. Una volta scaricato il nuovo client, all'utente veniva 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 come per le app per iOS/Android.

Siamo stati in grado di eseguire il push delle modifiche che provocano un errore al backend con un tempo di migrazione molto breve per i client. In genere, concediamo agli utenti un mese di tempo per passare ai nuovi clienti prima di apportare modifiche che provocano l'interruzione. 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 service worker vengono rimossi 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 i contenuti 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.

Recupero dei valori dei cookie in un service worker

A volte è necessario accedere ai valori dei cookie in un contesto di service worker. 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 che siano disponibili client con finestra, ad esempio durante una sincronizzazione in background. Per ovviare a questo problema, abbiamo creato un endpoint sul nostro server frontend che si limitava a inviare il valore del cookie al client. Il service worker ha effettuato una richiesta di rete a questo endpoint e ha letto la risposta per ottenere i valori del 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 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 service worker vengono installati solo quando il browser rileva che lo script del service worker è stato modificato in qualche modo, quindi abbiamo dovuto assicurarci che il file di script del service worker stesso cambi in qualche modo quando un file memorizzato nella cache è stato modificato. 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 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 testare perché devi simulare il trigger dell'evento, l'oggetto evento, attendere il callback respondWith() e poi la promessa, prima di asserire finalmente il 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é questi file erano solo moduli JS standard, potrebbero essere più facilmente testati per 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 media e di problemi specifici di iOS. Se vuoi chiederci di più sulla creazione di una PWA su Google, visita i nostri profili dell'autore per scoprire come contattarci: