Una guida completa alle applicazioni Web progressive

Pubblicato: 2022-03-10
Riepilogo rapido ↬ In questo articolo, esamineremo i punti deboli degli utenti che stanno navigando su vecchi siti Web non PWA e la promessa delle PWA di rendere il Web eccezionale. Imparerai la maggior parte delle tecnologie importanti che creano PWA interessanti, come gli operatori dei servizi, le notifiche push Web e IndexedDB.

Era il compleanno di mio padre e volevo ordinargli una torta al cioccolato e una maglietta. Sono andato su Google per cercare torte al cioccolato e ho fatto clic sul primo collegamento nei risultati di ricerca. C'era uno schermo vuoto per alcuni secondi; Non ho capito cosa stesse succedendo. Dopo qualche secondo di pazienza, lo schermo del mio cellulare si è riempito di deliziose torte. Non appena ho cliccato su uno di essi per controllarne i dettagli, ho ricevuto un brutto popup grasso, che mi chiedeva di installare un'applicazione Android in modo da poter ottenere un'esperienza liscia come la seta mentre ordinavo una torta.

È stato deludente. La mia coscienza non mi ha permesso di cliccare sul pulsante “Installa”. Tutto quello che volevo fare era ordinare una piccola torta ed essere per la mia strada.

Ho fatto clic sull'icona a forma di croce all'estrema destra del popup per uscirne il prima possibile. Ma poi il popup dell'installazione si trovava nella parte inferiore dello schermo, occupando un quarto dello spazio. E con l'interfaccia utente traballante, scorrere verso il basso è stata una sfida. In qualche modo sono riuscito a ordinare una torta olandese.

Dopo questa terribile esperienza, la mia prossima sfida è stata ordinare una maglietta per mio padre. Come prima, cerco su Google le magliette. Ho cliccato sul primo link e, in un batter d'occhio, l'intero contenuto era proprio di fronte a me. Lo scorrimento è stato fluido. Nessun banner di installazione. Mi sembrava di navigare in un'applicazione nativa. C'è stato un momento in cui la mia terribile connessione a Internet ha ceduto, ma sono stato comunque in grado di vedere il contenuto invece di un gioco di dinosauri. Anche con il mio internet janky, sono riuscito a ordinare una maglietta e dei jeans per mio padre. La cosa più sorprendente di tutte, stavo ricevendo notifiche sul mio ordine.

Lo definirei un'esperienza liscia come la seta. Queste persone stavano facendo qualcosa di giusto. Ogni sito web dovrebbe farlo per i propri utenti. Si chiama un'app web progressiva.

Come afferma Alex Russell in uno dei suoi post sul blog:

“Di tanto in tanto capita sul web che tecnologie potenti vengano a esistere senza il vantaggio dei dipartimenti di marketing o di imballaggi eleganti. Indugiano e crescono alle periferie, diventando vecchi per un piccolo gruppo pur rimanendo quasi invisibili a tutti gli altri. Finché qualcuno non li nomina".

Un'esperienza fluida come la seta sul Web, a volte conosciuta come un'applicazione Web progressiva

Le applicazioni web progressive (PWA) sono più di una metodologia che prevede una combinazione di tecnologie per creare potenti applicazioni web. Con un'esperienza utente migliorata, le persone trascorreranno più tempo sui siti Web e vedranno più annunci pubblicitari. Tendono ad acquistare di più e, con gli aggiornamenti delle notifiche, è più probabile che visitino spesso. Il Financial Times ha abbandonato le sue app native nel 2011 e ha creato un'app Web utilizzando le migliori tecnologie disponibili all'epoca. Ora, il prodotto è diventato un vero e proprio PWA.

Ma perché, dopo tutto questo tempo, dovresti creare un'app Web quando un'app nativa fa il lavoro abbastanza bene?

Esaminiamo alcune delle metriche condivise in Google IO 17.

Altro dopo il salto! Continua a leggere sotto ↓

Cinque miliardi di dispositivi sono connessi al web, rendendo il web la piattaforma più grande nella storia dell'informatica. Sul Web mobile, 11,4 milioni di visitatori unici mensili vanno alle prime 1000 proprietà web e 4 milioni alle prime mille app. Il Web mobile raccoglie circa quattro volte il numero di utenti delle applicazioni native. Ma questo numero diminuisce drasticamente quando si tratta di fidanzamento.

Un utente trascorre in media 188,6 minuti in app native e solo 9,3 minuti sul Web mobile. Le applicazioni native sfruttano la potenza dei sistemi operativi per inviare notifiche push per fornire agli utenti aggiornamenti importanti. Offrono una migliore esperienza utente e si avviano più rapidamente rispetto ai siti Web in un browser. Invece di digitare un URL nel browser web, gli utenti devono semplicemente toccare l'icona di un'app nella schermata iniziale.

È improbabile che la maggior parte dei visitatori del Web torni, quindi gli sviluppatori hanno escogitato la soluzione alternativa di mostrare loro banner per installare applicazioni native, nel tentativo di mantenerli profondamente coinvolti. Ma poi, gli utenti dovrebbero seguire la noiosa procedura di installazione del binario di un'applicazione nativa. Costringere gli utenti a installare un'applicazione è fastidioso e riduce ulteriormente le possibilità che la installino in primo luogo. L'opportunità per il web è chiara.

Letture consigliate : Native e PWA: scelte, non sfidanti!

Se le applicazioni web sono dotate di una ricca esperienza utente, notifiche push, supporto offline e caricamento istantaneo, possono conquistare il mondo. Questo è ciò che fa un'applicazione web progressiva.

Una PWA offre un'esperienza utente ricca perché ha diversi punti di forza:

  • Veloce
    L'interfaccia utente non è traballante. Lo scorrimento è fluido. E l'app risponde rapidamente all'interazione dell'utente.

  • Affidabile
    Un normale sito Web costringe gli utenti ad aspettare, senza fare nulla, mentre è impegnato a raggiungere il server. Una PWA, nel frattempo, carica i dati istantaneamente dalla cache. Una PWA funziona perfettamente, anche su una connessione 2G. Ogni richiesta di rete per recuperare una risorsa o un dato passa attraverso un addetto al servizio (ne parleremo più avanti), che prima verifica se la risposta per una particolare richiesta è già nella cache. Quando gli utenti ottengono contenuti reali quasi istantaneamente, anche con una connessione scadente, si fidano di più dell'app e la considerano più affidabile.

  • Coinvolgente
    Una PWA può guadagnare un posto nella schermata iniziale dell'utente. Offre un'esperienza simile a un'app nativa fornendo un'area di lavoro a schermo intero. Fa uso di notifiche push per mantenere gli utenti coinvolti.

Ora che sappiamo cosa offrono le PWA, entriamo nei dettagli di ciò che offre alle PWA un vantaggio rispetto alle applicazioni native. Le PWA sono costruite con tecnologie come service worker, manifest di app Web, notifiche push e IndexedDB/struttura dati locale per la memorizzazione nella cache. Esaminiamo ciascuno in dettaglio.

Operatori di servizio

Un service worker è un file JavaScript che viene eseguito in background senza interferire con le interazioni dell'utente. Tutte le richieste GET al server passano attraverso un addetto al servizio. Funziona come un proxy lato client. Intercettando le richieste di rete, assume il controllo completo sulla risposta inviata al client. Una PWA viene caricata istantaneamente perché gli addetti ai servizi eliminano la dipendenza dalla rete rispondendo con i dati dalla cache.

Un addetto al servizio può intercettare solo una richiesta di rete che rientra nel suo ambito. Ad esempio, un lavoratore del servizio con ambito root può intercettare tutte le richieste di recupero provenienti da una pagina Web. Un service worker opera come un sistema basato su eventi. Va in uno stato dormiente quando non è necessario, conservando così la memoria. Per utilizzare un service worker in un'applicazione web, dobbiamo prima registrarlo nella pagina con JavaScript.

 (function main () { /* navigator is a WEB API that allows scripts to register themselves and carry out their activities. */ if ('serviceWorker' in navigator) { console.log('Service Worker is supported in your browser') /* register method takes in the path of service worker file and returns a promises, which returns the registration object */ navigator.serviceWorker.register('./service-worker.js').then (registration => { console.log('Service Worker is registered!') }) } else { console.log('Service Worker is not supported in your browser') } })()

Per prima cosa controlliamo se il browser supporta i lavoratori del servizio. Per registrare un service worker in un'applicazione web, forniamo il suo URL come parametro alla funzione register , disponibile in navigator.serviceWorker ( navigator è una web API che consente agli script di registrarsi e svolgere le proprie attività). Un lavoratore del servizio viene registrato una sola volta. La registrazione non avviene ad ogni caricamento della pagina. Il browser scarica il file di lavoro del servizio ( ./service-worker.js ) solo se c'è una differenza di byte tra il lavoratore del servizio attivato esistente e quello più recente o se il suo URL è cambiato.

Il suddetto lavoratore del servizio intercetterà tutte le richieste provenienti dalla radice ( / ). Per limitare l'ambito di un lavoratore del servizio, passeremo un parametro facoltativo con una delle chiavi come ambito.

 if ('serviceWorker' in navigator) { /* register method takes in an optional second parameter as an object. To restrict the scope of a service worker, the scope should be provided. scope: '/books' will intercept requests with '/books' in the url. */ navigator.serviceWorker.register('./service-worker.js', { scope: '/books' }).then(registration => { console.log('Service Worker for scope /books is registered', registration) }) }

L'operatore di servizio sopra intercetterà le richieste che hanno /books nell'URL. Ad esempio, non intercetterà le richieste con /products , ma potrebbe benissimo intercettare le richieste con /books/products .

Come accennato, un addetto ai servizi opera come un sistema basato su eventi. Ascolta gli eventi (installa, attiva, recupera, push) e di conseguenza chiama il rispettivo gestore di eventi. Alcuni di questi eventi fanno parte del ciclo di vita di un addetto ai servizi, che attraversa questi eventi in sequenza per essere attivato.

Installazione

Una volta che un lavoratore del servizio è stato registrato correttamente, viene generato un evento di installazione. Questo è un buon posto per eseguire il lavoro di inizializzazione, come impostare la cache o creare archivi di oggetti in IndexedDB. (IndexedDB avrà più senso per te una volta entrati nei suoi dettagli. Per ora, possiamo solo dire che è una struttura di coppia chiave-valore.)

 self.addEventListener('install', (event) => { let CACHE_NAME = 'xyz-cache' let urlsToCache = [ '/', '/styles/main.css', '/scripts/bundle.js' ] event.waitUntil( /* open method available on caches, takes in the name of cache as the first parameter. It returns a promise that resolves to the instance of cache All the URLS above can be added to cache using the addAll method. */ caches.open(CACHE_NAME) .then (cache => cache.addAll(urlsToCache)) ) })

Qui stiamo memorizzando nella cache alcuni dei file in modo che il prossimo caricamento sia istantaneo. self si riferisce all'istanza di service worker. event.waitUntil fa sì che il lavoratore del servizio attenda fino al termine dell'esecuzione di tutto il codice al suo interno.

Attivazione

Una volta che un service worker è stato installato, non è ancora in grado di ascoltare le richieste di recupero. Al contrario, viene activate un evento di attivazione. Se nessun lavoratore del servizio attivo opera sul sito Web nello stesso ambito, il lavoratore del servizio installato viene attivato immediatamente. Tuttavia, se un sito Web ha già un operatore del servizio attivo, l'attivazione di un nuovo operatore del servizio viene ritardata fino alla chiusura di tutte le schede che operano sul vecchio operatore del servizio. Ciò ha senso perché il vecchio service worker potrebbe utilizzare l'istanza della cache che ora è stata modificata in quella più recente. Quindi, il passaggio di attivazione è un buon posto per sbarazzarsi delle vecchie cache.

 self.addEventListener('activate', (event) => { let cacheWhitelist = ['products-v2'] // products-v2 is the name of the new cache event.waitUntil( caches.keys().then (cacheNames => { return Promise.all( cacheNames.map( cacheName => { /* Deleting all the caches except the ones that are in cacheWhitelist array */ if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName) } }) ) }) ) })

Nel codice sopra, stiamo eliminando la vecchia cache. Se il nome di una cache non corrisponde a cacheWhitelist , viene eliminata. Per saltare la fase di attesa e attivare immediatamente il service worker, utilizziamo skip.waiting() .

 self.addEventListener('activate', (event) => { self.skipWaiting() // The usual stuff })

Una volta attivato, il lavoratore del servizio può ascoltare le richieste di recupero e inviare gli eventi.

Recupera gestore eventi

Ogni volta che una pagina Web attiva una richiesta di recupero per una risorsa sulla rete, viene chiamato l'evento di recupero dal lavoratore del servizio. Il gestore dell'evento fetch cerca prima la risorsa richiesta nella cache. Se è presente nella cache, restituisce la risposta con la risorsa memorizzata nella cache. In caso contrario, avvia una richiesta di recupero al server e, quando il server restituisce la risposta con la risorsa richiesta, la inserisce nella cache per le richieste successive.

 /* Fetch event handler for responding to GET requests with the cached assets */ self.addEventListener('fetch', (event) => { event.respondWith( caches.open('products-v2') .then (cache => { /* Checking if the request is already present in the cache. If it is present, sending it directly to the client */ return cache.match(event.request).then (response => { if (response) { console.log('Cache hit! Fetching response from cache', event.request.url) return response } /* If the request is not present in the cache, we fetch it from the server and then put it in cache for subsequent requests. */ fetch(event.request).then (response => { cache.put(event.request, response.clone()) return response }) }) }) ) })

event.respondWith consente all'operatore del servizio di inviare una risposta personalizzata al client.

Offline-first ora è una cosa. Per qualsiasi richiesta non critica, dobbiamo servire la risposta dalla cache, invece di fare un giro al server. Se un asset non è presente nella cache, lo otteniamo dal server e quindi lo inseriamo nella cache per le richieste successive.

I service worker lavorano solo su siti Web HTTPS perché hanno il potere di manipolare la risposta a qualsiasi richiesta di recupero. Qualcuno con intenzioni dannose potrebbe manomettere la risposta a una richiesta su un sito Web HTTP. Quindi, l'hosting di una PWA su HTTPS è obbligatorio. Gli addetti ai servizi non interrompono il normale funzionamento del DOM. Non possono comunicare direttamente con la pagina web. Per inviare qualsiasi messaggio a una pagina Web, utilizza i messaggi di posta.

Notifiche push web

Supponiamo che tu sia impegnato a giocare sul tuo cellulare e che venga visualizzata una notifica che ti informa di uno sconto del 30% sulla tua marca preferita. Senza ulteriori indugi, fai clic sulla notifica e fai acquisti senza fiato. Ricevere aggiornamenti in tempo reale su, ad esempio, una partita di cricket o di calcio o ricevere e-mail e promemoria importanti come notifiche è un grosso problema quando si tratta di coinvolgere gli utenti con un prodotto. Questa funzione era disponibile solo nelle applicazioni native fino all'arrivo di PWA. Una PWA utilizza le notifiche push web per competere con questa potente funzionalità che le app native forniscono immediatamente. Un utente riceverà comunque una notifica push web anche se la PWA non è aperta in nessuna delle schede del browser e anche se il browser non è aperto.

Un'applicazione Web deve chiedere il permesso all'utente di inviare notifiche push.

Browser Richiesta di autorizzazione per le notifiche Web Push
Browser Richiesta di autorizzazione per le notifiche Web Push. (Grande anteprima)

Una volta che l'utente conferma facendo clic sul pulsante "Consenti", il browser genera un token di abbonamento univoco. Questo token è unico per questo dispositivo. Il formato del token di abbonamento generato da Chrome è il seguente:

 { "endpoint": "https://fcm.googleapis.com/fcm/send/c7Veb8VpyM0:APA91bGnMFx8GIxf__UVy6vJ-n9i728CUJSR1UHBPAKOCE_SrwgyP2N8jL4MBXf8NxIqW6NCCBg01u8c5fcY0kIZvxpDjSBA75sVz64OocQ-DisAWoW7PpTge3SwvQAx5zl_45aAXuvS", "expirationTime": null, "keys": { "p256dh": "BJsj63kz8RPZe8Lv1uu-6VSzT12RjxtWyWCzfa18RZ0-8sc5j80pmSF1YXAj0HnnrkyIimRgLo8ohhkzNA7lX4w", "auth": "TJXqKozSJxcWvtQasEUZpQ" } }

L' endpoint contenuto nel token sopra sarà univoco per ogni sottoscrizione. In un sito Web medio, migliaia di utenti accetterebbero di ricevere notifiche push e, per ciascuno di essi, questo endpoint sarebbe unico. Quindi, con l'aiuto di questo endpoint , l'applicazione è in grado di indirizzare questi utenti in futuro inviando loro notifiche push. expirationTime è il periodo di validità dell'abbonamento per un determinato dispositivo. Se expirationTime è 20 giorni, significa che l'abbonamento push dell'utente scadrà dopo 20 giorni e l'utente non sarà in grado di ricevere notifiche push sull'abbonamento precedente. In questo caso, il browser genererà un nuovo token di abbonamento per quel dispositivo. Le chiavi auth e p256dh vengono utilizzate per la crittografia.

Ora, per inviare notifiche push a queste migliaia di utenti in futuro, dobbiamo prima salvare i rispettivi token di abbonamento. È compito del server delle applicazioni (il server back-end, forse uno script Node.js) inviare notifiche push a questi utenti. Potrebbe sembrare semplice come effettuare una richiesta POST all'URL dell'endpoint con i dati di notifica nel payload della richiesta. Tuttavia, va notato che se un utente non è online quando una notifica push a lui destinata viene attivata dal server, dovrebbe comunque ricevere tale notifica una volta tornato online. Il server dovrebbe occuparsi di tali scenari, oltre a inviare migliaia di richieste agli utenti. Un server che tiene traccia della connessione dell'utente sembra complicato. Quindi, qualcosa nel mezzo sarebbe responsabile dell'instradamento delle notifiche push Web dal server al client. Questo è chiamato servizio push e ogni browser ha la propria implementazione di un servizio push. Il browser deve comunicare le seguenti informazioni al servizio push per inviare qualsiasi notifica:

  1. Il tempo da vivere
    Questo è il tempo per cui un messaggio deve essere messo in coda, nel caso in cui non venga consegnato all'utente. Trascorso questo tempo, il messaggio verrà rimosso dalla coda.
  2. Urgenza del messaggio
    In questo modo il servizio push preserva la batteria dell'utente inviando solo messaggi ad alta priorità.

Il servizio push instrada i messaggi al client. Poiché il push deve essere ricevuto dal client anche se la rispettiva applicazione Web non è aperta nel browser, gli eventi push devono essere ascoltati da qualcosa che monitora continuamente in background. Hai indovinato: questo è il lavoro del lavoratore dei servizi. L'operatore del servizio ascolta gli eventi push e fa il lavoro di mostrare le notifiche all'utente.

Quindi, ora sappiamo che il browser, il servizio push, il lavoratore del servizio e il server delle applicazioni lavorano in armonia per inviare notifiche push all'utente. Esaminiamo i dettagli di implementazione.

Client push web

Chiedere il permesso dell'utente è una cosa una tantum. Se un utente ha già concesso l'autorizzazione a ricevere le notifiche push, non dovremmo chiedere di nuovo. Il valore dell'autorizzazione viene salvato in Notification.permission .

 /* Notification.permission can have one of these three values: default, granted or denied. */ if (Notification.permission === 'default') { /* The Notification.requestPermission() method shows a notification permission prompt to the user. It returns a promise that resolves to the value of permission*/ Notification.requestPermission().then (result => { if (result === 'denied') { console.log('Permission denied') return } if (result === 'granted') { console.log('Permission granted') /* This means the user has clicked the Allow button. We're to get the subscription token generated by the browser and store it in our database. The subscription token can be fetched using the getSubscription method available on pushManager of the serviceWorkerRegistration object. If subscription is not available, we subscribe using the subscribe method available on pushManager. The subscribe method takes in an object. */ serviceWorkerRegistration.pushManager.getSubscription() .then (subscription => { if (!subscription) { const applicationServerKey = ' ' serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, // All push notifications from server should be displayed to the user applicationServerKey // VAPID Public key }) } else { saveSubscriptionInDB(subscription, userId) // A method to save subscription token in the database } }) } }) } /* Notification.permission can have one of these three values: default, granted or denied. */ if (Notification.permission === 'default') { /* The Notification.requestPermission() method shows a notification permission prompt to the user. It returns a promise that resolves to the value of permission*/ Notification.requestPermission().then (result => { if (result === 'denied') { console.log('Permission denied') return } if (result === 'granted') { console.log('Permission granted') /* This means the user has clicked the Allow button. We're to get the subscription token generated by the browser and store it in our database. The subscription token can be fetched using the getSubscription method available on pushManager of the serviceWorkerRegistration object. If subscription is not available, we subscribe using the subscribe method available on pushManager. The subscribe method takes in an object. */ serviceWorkerRegistration.pushManager.getSubscription() .then (subscription => { if (!subscription) { const applicationServerKey = ' ' serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, // All push notifications from server should be displayed to the user applicationServerKey // VAPID Public key }) } else { saveSubscriptionInDB(subscription, userId) // A method to save subscription token in the database } }) } }) }

Nel metodo di subscribe sopra, stiamo passando userVisibleOnly e applicationServerKey per generare un token di sottoscrizione. La proprietà userVisibleOnly dovrebbe essere sempre true perché indica al browser che qualsiasi notifica push inviata dal server verrà mostrata al client. Per comprendere lo scopo di applicationServerKey , consideriamo uno scenario.

Se una persona si impossessa delle tue migliaia di token di abbonamento, potrebbe benissimo inviare notifiche agli endpoint contenuti in questi abbonamenti. Non è possibile collegare l'endpoint alla tua identità univoca. Per fornire un'identità univoca ai token di abbonamento generati sulla tua applicazione web, utilizziamo il protocollo VAPID. Con VAPID, il server delle applicazioni si identifica volontariamente nel servizio push durante l'invio di notifiche push. Generiamo due chiavi in ​​questo modo:

 const webpush = require('web-push') const vapidKeys = webpush.generateVAPIDKeys()

web-push è un modulo npm. vapidKeys avrà una chiave pubblica e una chiave privata. La chiave del server delle applicazioni utilizzata sopra è la chiave pubblica.

Server push web

Il lavoro del server push web (server delle applicazioni) è semplice. Invia un payload di notifica ai token di abbonamento.

 const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options) const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options) const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options)

Utilizza il metodo sendNotification dalla libreria web push.

Operatori di servizio

L'operatore del servizio mostra la notifica all'utente in quanto tale:

 self.addEventListener('push', (event) => { let options = { body: event.data.body, icon: 'images/example.png', } event.waitUntil( /* The showNotification method is available on the registration object of the service worker. The first parameter to showNotification method is the title of notification, and the second parameter is an object */ self.registration.showNotification(event.data.title, options) ) })

Finora, abbiamo visto come un addetto ai servizi utilizza la cache per archiviare le richieste e crea una PWA veloce e affidabile, e abbiamo visto come le notifiche push web mantengono gli utenti coinvolti.

Per archiviare una serie di dati sul lato client per il supporto offline, abbiamo bisogno di una struttura di dati gigantesca. Diamo un'occhiata al Financial Times PWA. Devi testimoniare di persona la potenza di questa struttura di dati. Carica l'URL nel tuo browser, quindi spegni la connessione a Internet. Ricarica la pagina. Gah! Funziona ancora? È. (Come ho detto, offline è il nuovo nero.) I dati non provengono dai cavi. Viene servito dalla casa. Vai alla scheda "Applicazioni" degli Strumenti per sviluppatori di Chrome. Sotto "Archiviazione", troverai "IndexedDB".

IndexedDB memorizza i dati degli articoli in Financial Times PWA
IndexedDB su Financial Times PWA. (Grande anteprima)

Dai un'occhiata al negozio di oggetti "Articoli" ed espandi uno qualsiasi degli oggetti per vedere tu stesso la magia. Il Financial Times ha archiviato questi dati per il supporto offline. Questa struttura di dati che ci consente di archiviare un'enorme quantità di dati è chiamata IndexedDB. IndexedDB è un database orientato agli oggetti basato su JavaScript per la memorizzazione di dati strutturati. Possiamo creare diversi archivi di oggetti in questo database per vari scopi. Ad esempio, come possiamo vedere nell'immagine sopra, "Resources", "ArticleImages" e "Articoli" sono chiamati come negozi di oggetti. Ogni record in un archivio oggetti è identificato in modo univoco con una chiave. IndexedDB può anche essere utilizzato per archiviare file e BLOB.

Proviamo a capire IndexedDB creando un database per la memorizzazione dei libri.

 let openIdbRequest = window.indexedDB.open('booksdb', 1)

Se il database booksdb non esiste già, il codice sopra creerà un database booksdb . Il secondo parametro del metodo open è la versione del database. La specifica di una versione si occupa delle modifiche relative allo schema che potrebbero verificarsi in futuro. Ad esempio, booksdb ora ha solo una tabella, ma quando l'applicazione cresce, intendiamo aggiungervi altre due tabelle. Per assicurarci che il nostro database sia sincronizzato con lo schema aggiornato, specificheremo una versione superiore alla precedente.

La chiamata al metodo open non apre immediatamente il database. È una richiesta asincrona che restituisce un oggetto IDBOpenDBRequest . Questo oggetto ha proprietà di successo e di errore; dovremo scrivere gestori appropriati per queste proprietà per gestire lo stato della nostra connessione.

 let dbInstance openIdbRequest.onsuccess = (event) => { dbInstance = event.target.result console.log('booksdb is opened successfully') } openIdbRequest.onerror = (event) => { console.log('There was an error in opening booksdb database') } openIdbRequest.onupgradeneeded = (event) => { let db = event.target.result let objectstore = db.createObjectStore('books', { keyPath: 'id' }) }

Per gestire la creazione o la modifica di archivi oggetti (gli archivi oggetti sono analoghi alle tabelle basate su SQL — hanno una struttura chiave-valore), il metodo onupgradeneeded viene chiamato sull'oggetto openIdbRequest . Il metodo onupgradeneeded verrà richiamato ogni volta che la versione cambia. Nello snippet di codice sopra, stiamo creando un negozio di oggetti libri con una chiave univoca come ID.

Diciamo che, dopo aver distribuito questo pezzo di codice, dobbiamo creare un altro object store, chiamato users . Quindi, ora la versione del nostro database sarà 2 .

 let openIdbRequest = window.indexedDB.open('booksdb', 2) // New Version - 2 /* Success and error event handlers remain the same. The onupgradeneeded method gets called when the version of the database changes. */ openIdbRequest.onupgradeneeded = (event) => { let db = event.target.result if (!db.objectStoreNames.contains('books')) { let objectstore = db.createObjectStore('books', { keyPath: 'id' }) } let oldVersion = event.oldVersion let newVersion = event.newVersion /* The users tables should be added for version 2. If the existing version is 1, it will be upgraded to 2, and the users object store will be created. */ if (oldVersion === 1) { db.createObjectStore('users', { keyPath: 'id' }) } }

Abbiamo memorizzato nella cache dbInstance il gestore dell'evento di successo della richiesta aperta. Per recuperare o aggiungere dati in IndexedDB, utilizzeremo dbInstance . Aggiungiamo alcuni record di libri nel nostro negozio di oggetti libri.

 let transaction = dbInstance.transaction('books') let objectstore = transaction.objectstore('books') let bookRecord = { id: '1', name: 'The Alchemist', author: 'Paulo Coelho' } let addBookRequest = objectstore.add(bookRecord) addBookRequest.onsuccess = (event) => { console.log('Book record added successfully') } addBookRequest.onerror = (event) => { console.log('There was an error in adding book record') }

Facciamo uso di transactions , in particolare durante la scrittura di record su negozi di oggetti. Una transazione è semplicemente un wrapper attorno a un'operazione per garantire l'integrità dei dati. Se una qualsiasi delle azioni in una transazione ha esito negativo, non viene eseguita alcuna azione sul database.

Modifichiamo un record di libro con il metodo put :

 let modifyBookRequest = objectstore.put(bookRecord) // put method takes in an object as the parameter modifyBookRequest.onsuccess = (event) => { console.log('Book record updated successfully') }

Recuperiamo un record di un libro con il metodo get :

 let transaction = dbInstance.transaction('books') let objectstore = transaction.objectstore('books') /* get method takes in the id of the record */ let getBookRequest = objectstore.get(1) getBookRequest.onsuccess = (event) => { /* event.target.result contains the matched record */ console.log('Book record', event.target.result) } getBookRequest.onerror = (event) => { console.log('Error while retrieving the book record.') }

Aggiunta di icone nella schermata iniziale

Ora che non c'è quasi alcuna distinzione tra una PWA e un'applicazione nativa, ha senso offrire una posizione privilegiata alla PWA. Se il tuo sito web soddisfa i criteri di base di una PWA (ospitata su HTTPS, si integra con i service worker e ha un manifest.json ) e dopo che l'utente ha trascorso un po' di tempo sulla pagina web, il browser invocherà un prompt in fondo, chiedendo l'utente per aggiungere l'app alla propria schermata iniziale, come mostrato di seguito:

Richiedi l'aggiunta della PWA del Financial Times nella schermata iniziale
Richiedi l'aggiunta della PWA del Financial Times nella schermata iniziale. (Grande anteprima)

Quando un utente fa clic su "Aggiungi FT alla schermata Home", la PWA può mettere piede sulla schermata principale, oltre che nel cassetto delle app. Quando un utente cerca qualsiasi applicazione sul proprio telefono, verranno elencate tutte le PWA che corrispondono alla query di ricerca. Verranno visualizzati anche nelle impostazioni di sistema, il che semplifica la loro gestione da parte degli utenti. In questo senso, una PWA si comporta come un'applicazione nativa.

Le PWA utilizzano manifest.json per fornire questa funzionalità. Esaminiamo un semplice file manifest.json .

 { "name": "Demo PWA", "short_name": "Demo", "start_url": "/?standalone", "background_color": "#9F0C3F", "theme_color": "#fff1e0", "display": "standalone", "icons": [{ "src": "/lib/img/icons/xxhdpi.png?v2", "sizes": "192x192" }] }

Lo short_name appare nella schermata iniziale dell'utente e nelle impostazioni di sistema. Il name viene visualizzato nel prompt di Chrome e nella schermata iniziale. La schermata iniziale è ciò che l'utente vede quando l'app si sta preparando per l'avvio. start_url è la schermata principale della tua app. È ciò che gli utenti ottengono quando toccano un'icona nella schermata iniziale. Il background_color viene utilizzato nella schermata iniziale. theme_color imposta il colore della barra degli strumenti. Il valore standalone per la modalità di display indica che l'app deve essere utilizzata in modalità a schermo intero (nascondendo la barra degli strumenti del browser). Quando un utente installa una PWA, la sua dimensione è solo in kilobyte, anziché in megabyte di applicazioni native.

Gli addetti all'assistenza, le notifiche push Web, IndexedDB e la posizione della schermata iniziale compensano il supporto, l'affidabilità e il coinvolgimento offline. Va notato che un addetto al servizio non prende vita e inizia a svolgere il suo lavoro al primo carico. Il primo caricamento sarà comunque lento fino a quando tutte le risorse statiche e le altre risorse non saranno state memorizzate nella cache. Possiamo implementare alcune strategie per ottimizzare il primo carico.

Raggruppamento di beni

Tutte le risorse, inclusi HTML, fogli di stile, immagini e JavaScript, devono essere recuperate dal server. Più file sono, più richieste HTTPS sono necessarie per recuperarli. Possiamo utilizzare bundler come WebPack per raggruppare le nostre risorse statiche, riducendo così il numero di richieste HTTP al server. WebPack fa un ottimo lavoro nell'ottimizzare ulteriormente il pacchetto utilizzando tecniche come la divisione del codice (ovvero raggruppando solo i file necessari per il caricamento della pagina corrente, invece di raggrupparli tutti insieme) e il tremore dell'albero (ovvero rimuovendo le dipendenze duplicate o dipendenze importate ma non utilizzate nel codice).

Riduzione dei viaggi di andata e ritorno

Uno dei motivi principali della lentezza sul web è la latenza della rete. Il tempo impiegato da un byte per viaggiare da A a B varia in base alla connessione di rete. Ad esempio, un particolare viaggio di andata e ritorno tramite Wi-Fi richiede 50 millisecondi e 500 millisecondi su una connessione 3G, ma 2500 millisecondi su una connessione 2G. Queste richieste vengono inviate utilizzando il protocollo HTTP, il che significa che mentre una particolare connessione viene utilizzata per una richiesta, non può essere utilizzata per altre richieste fino a quando non viene fornita la risposta della richiesta precedente. Un sito Web può effettuare sei richieste HTTP asincrone alla volta perché sono disponibili sei connessioni a un sito Web per effettuare richieste HTTP. Un sito web medio fa circa 100 richieste; quindi, con un massimo di sei connessioni disponibili, un utente potrebbe finire per spendere circa 833 millisecondi in un unico viaggio di andata e ritorno. (Il calcolo è 833 millisecondi - 1006 = 1666 . Dobbiamo dividere 1666 per 2 perché stiamo calcolando il tempo impiegato per un viaggio di andata e ritorno.) Con HTTP2 in atto, il tempo di risposta è drasticamente ridotto. HTTP2 non blocca la testa di connessione, quindi è possibile inviare più richieste contemporaneamente.

La maggior parte delle risposte HTTP contiene intestazioni etag e last-modified . L'intestazione last-modified è la data dell'ultima modifica del file e un etag è un valore univoco basato sul contenuto del file. Verrà modificato solo quando viene modificato il contenuto di un file. Entrambe queste intestazioni possono essere utilizzate per evitare di scaricare nuovamente il file se una versione memorizzata nella cache è già disponibile localmente. Se il browser ha una versione di questo file disponibile localmente, può aggiungere una di queste due intestazioni nella richiesta in quanto tale:

Aggiungi ETag e intestazioni Last-Modified per impedire il download di risorse memorizzate nella cache valide
Intestazioni ETag e Last-Modified. (Grande anteprima)

Il server può verificare se il contenuto del file è cambiato. If the contents of the file have not changed, then it responds with a status code of 304 ( not modified ).

If-None-Match Header to prevent downloading of valid cached assets
If-None-Match Header. (Grande anteprima)

This indicates to the browser to use the locally available cached version of the file. By doing all of this, we've prevented the file from being downloaded.

Faster responses are in now place, but our job is not done yet. We still have to parse the HTML, load the style sheets and make the web page interactive. It makes sense to show some empty boxes with a loader to the user, instead of a blank screen. While the HTML document is getting parsed, when it comes across <script src='asset.js'></script> , it will make a synchronous HTTP request to the server to fetch asset.js , and the whole parsing process will be paused until the response comes back. Imagine having a dozen of synchronous static asset references. These could very well be managed just by making use of the async keyword in script references, like <script src='asset.js' async></script> . With the introduction of the async keyword here, the browser will make an asynchronous request to fetch asset.js without hindering the parsing of the HTML. If a script file is required at a later stage, we can defer the downloading of that file until the entire HTML has been parsed. A script file can be deferred by using the defer keyword, like <script src='asset.js' defer></script> .

Conclusione

We've learned a lot of many new things that make for a cool web application. Here's a summary of all of the things we've explored in this article:

  1. Service workers make good use of the cache to speed up the loading of assets.
  2. Web push notifications work under the hood.
  3. We use IndexedDB to store a massive amount of data.
  4. Some of the optimizations for instant first load, like using HTTP2 and adding headers like Etag , last-modified and If-None-Match , prevent the downloading of valid cached assets.

That's all, folks!