Creare una PWA in Google, parte 1

Cosa ha imparato il team di bollettino 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 bollettino durante la creazione di una PWA rivolta all'esterno. In questi post condivideremo alcune delle sfide che abbiamo dovuto affrontare, gli approcci adottati per superarle e consigli generali per evitare gli errori. Non si tratta affatto di una panoramica completa delle PWA. L'obiettivo è condividere ciò che abbiamo appreso dall'esperienza del nostro team.

In questo primo post, esamineremo alcune informazioni di base, dopodiché approfondiremo tutto ciò che abbiamo appreso sui service worker.

Contesto

bollettino era 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, vediamo perché la creazione di una PWA è stata un'opzione interessante per questo progetto:

  • Capacità di iterazione rapida. Particolarmente utile perché il programma sarebbe stato sperimentato in più mercati.
  • Base di codice singola. I nostri utenti sono stati suddivisi in modo più o meno equamente tra Android e iOS. Con una PWA potevamo creare un'unica app web funzionante 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 essere aggiornate automaticamente, riducendo la quantità di client obsoleti in circolazione. Siamo riusciti a implementare modifiche improvvise al backend in tempi di migrazione molto brevi 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 l'apertura di un URL.
  • Sono stati rimossi gli attriti derivanti dall'installazione di un'app.

Il nostro framework

Per Recap, abbiamo usato Polymer, ma qualsiasi framework moderno e ben supportato funzionerà.

Cosa abbiamo imparato sui Service worker

Non puoi avere una PWA senza un worker del servizio. I Service worker offrono molta potenza, ad esempio le strategie di memorizzazione nella cache avanzate, le funzionalità offline, la sincronizzazione in background e così via. Nonostante i Service worker aggiungano un po' di complessità, abbiamo scoperto che i loro vantaggi superavano la complessità aggiuntiva.

Se possibile, generalo

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

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

Non tutte le librerie sono compatibili con service-worker

Alcune librerie JS fanno presupposti che non funzionano come previsto quando vengono eseguite da un service worker. Ad esempio, supponendo che window o document siano disponibili oppure che venga utilizzata un'API non disponibile per i service worker (XMLHttpRequest, spazio di archiviazione locale e così via). Assicurati che tutte le librerie critiche necessarie per la tua applicazione siano compatibili con i worker di servizio. Per questa PWA in particolare, volevamo utilizzare gapi.js per l'autenticazione, ma non ci è stato possibile perché non supportava i service worker. Gli autori delle librerie devono inoltre ridurre o rimuovere le ipotesi non necessarie sul contesto JavaScript, ove possibile, per supportare i 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 entrare in questa situazione indesiderata:

  1. L'utente dispone di un'app web con IndexedDB (IDB) versione N
  2. La nuova app web viene inviata 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 dall'IDB prima di registrare il gestore di eventi install, attivando un ciclo di upgrade dell'IDB per passare da N a N+1
  5. Poiché l'utente ha un client precedente con la versione N, il processo di upgrade dei service worker si blocca poiché le connessioni attive sono ancora aperte alla versione precedente del database
  6. Il service worker si blocca e non lo installa mai

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

Rendi l'esperienza resiliente

Gli script dei service worker vengono eseguiti in background, ma possono anche essere terminati in qualsiasi momento, anche nel corso delle operazioni di I/O (rete, IDB e così via). Qualsiasi processo a lunga esecuzione dovrebbe essere ripristinato in qualsiasi momento.

Nel caso di un processo di sincronizzazione che carica file di grandi dimensioni sul server e salvati su IDB, la nostra soluzione per i caricamenti parziali interrotti è 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 è stato completato la prima volta. Inoltre, prima di qualsiasi operazione di I/O a lunga esecuzione, lo stato è stato salvato nell'IDB per indicare in che punto 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 che in un contesto di service worker (come logging, flag, sincronizzazione e così via). Il codice deve essere difensivo riguardo ai servizi che utilizza, come l'archiviazione locale o i cookie. Puoi utilizzare globalThis per fare riferimento all'oggetto globale in un modo che funzionerà in tutti i contesti. Inoltre, utilizza con parsimonia i dati archiviati nelle variabili globali, in quanto non vi è alcuna garanzia circa la fine dello script e la rimozione dello stato.

Sviluppo locale

Una componente importante dei service worker è la memorizzazione locale delle risorse nella cache. Tuttavia, in fase di sviluppo, questo è l'esatto opposto di ciò che vuoi, in particolare quando gli aggiornamenti vengono eseguiti in modo pigro. Vuoi comunque installare il server worker per poter risolvere i problemi riscontrati o per utilizzare altre API, come la sincronizzazione in background o le notifiche. Su Chrome puoi farlo tramite Chrome DevTools selezionando la casella di controllo Escludi per la rete (riquadro Applicazione > riquadro Service worker) e la casella di controllo Disattiva cache nel riquadro Rete per disattivare anche la cache in memoria. Per gestire 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 tutti gli asset nella cache.

Faro

Lighthouse offre una serie di strumenti di debug utili per le PWA. Esegue la scansione di un sito e genera report relativi a PWA, prestazioni, accessibilità, SEO e altre best practice. Ti consigliamo di eseguire Lighthouse in integrazione continua per ricevere un avviso se violi uno dei criteri relativi alla PWA. In realtà ci è capitato una volta, quando il service worker non stava installando e non ci siamo resi conto che prima di un push in produzione. L'utilizzo di Lighthouse come parte della nostra CI avrebbe impedito questo comportamento.

Adotta la distribuzione continua

Poiché i service worker possono essere aggiornati automaticamente, gli utenti non hanno la possibilità di limitare gli upgrade. Questo riduce notevolmente la quantità di client obsoleti in circolazione. Quando l'utente apre l'app, il service worker gestisce il vecchio client mentre scarica pigramente il nuovo client. Dopo il download del nuovo client, veniva chiesto all'utente di aggiornare la pagina per accedere alle nuove funzionalità. Anche se l'utente ignorava questa richiesta, la volta successiva che aggiornava la pagina riceverà la nuova versione del client. Di conseguenza, è piuttosto difficile per un utente rifiutare gli aggiornamenti come può fare per le app iOS/Android.

Siamo riusciti a implementare modifiche che provocano errori nel backend, con tempi di migrazione molto brevi per i client. In genere, concedevamo agli utenti un mese per passare ai clienti più recenti prima di apportare modifiche che provocano un errore. Dal momento che l'app veniva pubblicata quando non era aggiornata, i clienti meno recenti potevano esistere se l'utente non l'avesse aperta da molto tempo. Su iOS, i service worker vengono rimossi dopo un paio di settimane, quindi questo caso non succede. Per Android, questo problema potrebbe essere mitigato non pubblicando i contenuti mentre sono inattivi o facendo scadere manualmente i contenuti dopo alcune settimane. In pratica, non abbiamo mai riscontrato problemi con clienti inattivi. La severità di un determinato team dipende dal caso d'uso specifico, ma le PWA offrono una flessibilità significativamente maggiore rispetto alle app per iOS/Android.

Ottenere valori dei cookie in un service worker

A volte è necessario accedere ai valori dei cookie in un contesto di service worker. Nel nostro caso, dovevamo accedere ai valori dei cookie per generare un token per autenticare le richieste API proprietarie. In un service 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, sebbene sia possibile che il service worker venga eseguito in background senza che siano disponibili client con finestre, ad esempio durante una sincronizzazione in background. Per ovviare a questo problema, abbiamo creato un endpoint sul nostro server frontend che inviava semplicemente 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, poiché fornisce l'accesso asincrono ai cookie del browser e può essere utilizzata direttamente dal service worker.

Insidie dei service worker non generati

Assicurati che lo script del service worker venga modificato in caso di modifiche a un file statico memorizzato nella cache

Un pattern PWA comune prevede che un service worker installi tutti i file statici dell'applicazione durante la fase install, il che consente ai client di accedere direttamente alla cache dell'API Cache Storage per tutte le visite successive . I service worker vengono installati soltanto quando il browser rileva che lo script dei service worker è cambiato in qualche modo, perciò abbiamo dovuto assicurarci che il file di script dei service worker stesso fosse cambiato in qualche modo quando un file memorizzato nella cache è stato modificato. Questa operazione è stata eseguita manualmente incorporando un hash del set di file di risorse statiche nello script del service worker, in modo che ogni release abbia prodotto un file JavaScript del service worker distinto. Le librerie di service worker come Workbox automatizzano questo processo.

Test delle unità

Le API Service worker funzionano mediante l'aggiunta di 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 e l'oggetto evento, attendere il callback respondWith() e attendere la promessa prima di dichiarare il risultato finale. Un modo più semplice per strutturare questo aspetto è delegare 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à di testare le unità di uno script dei Service worker, abbiamo mantenuto il massimo possibile lo script dei Service worker principali, suddividendo la maggior parte dell'implementazione in altri moduli. Poiché questi file erano solo moduli JS standard, potevano essere testati più facilmente con le librerie di test standard.

Non perderti le parti 2 e 3

Nelle parti 2 e 3 di questa serie parleremo di gestione dei media e di problemi specifici per iOS. Se vuoi chiederci di più sulla creazione di una PWA su Google, visita i nostri profili degli autori per scoprire come contattarci: