Un ghid extins pentru aplicații web progresive

Publicat: 2022-03-10
Rezumat rapid ↬ În acest articol, ne vom uita la punctele dureroase ale utilizatorilor care navighează pe site-uri web vechi non-PWA și la promisiunea PWA-urilor de a face web-ul grozav. Veți învăța cele mai multe dintre tehnologiile importante care fac pentru PWA cool, cum ar fi lucrătorii de service, notificările push web și IndexedDB.

Era ziua tatălui meu și am vrut să-i comand un tort de ciocolată și o cămașă. M-am îndreptat către Google pentru a căuta prăjituri de ciocolată și am dat clic pe primul link din rezultatele căutării. A fost un ecran gol pentru câteva secunde; Nu am înțeles ce se întâmplă. După câteva secunde de privit cu răbdare, ecranul meu mobil s-a umplut de prăjituri cu aspect delicios. De îndată ce am dat clic pe unul dintre ele pentru a-i verifica detaliile, am primit un pop-up urât și grăsime, care mi-a cerut să instalez o aplicație Android, astfel încât să pot obține o experiență netedă în timp ce comand un tort.

A fost dezamăgitor. Conștiința mea nu mi-a permis să dau clic pe butonul „Instalare”. Tot ce voiam să fac era să comand un mic tort și să fiu pe drum.

Am dat clic pe pictograma cruce din partea dreaptă a ferestrei pop-up pentru a ieși din ea cât de curând am putut. Dar apoi fereastra de instalare a stat în partea de jos a ecranului, ocupând un sfert din spațiu. Iar cu interfața de utilizare slabă, derularea în jos a fost o provocare. Am reușit cumva să comand un tort olandez.

După această experiență teribilă, următoarea mea provocare a fost să comand o cămașă pentru tatăl meu. Ca și înainte, caut pe Google cămăși. Am dat clic pe primul link și într-o clipită, întregul conținut era chiar în fața mea. Derularea a fost lină. Fără banner de instalare. Am simțit că răsfoiesc o aplicație nativă. A existat un moment în care conexiunea mea groaznică la internet a renunțat, dar încă am putut să văd conținutul în loc de un joc cu dinozauri. Chiar și cu internetul meu neplăcut, am reușit să comand o cămașă și blugi pentru tatăl meu. Cel mai surprinzător dintre toate, primeam notificări despre comanda mea.

Aș numi asta o experiență netedă. Acești oameni făceau ceva corect. Fiecare site ar trebui să o facă pentru utilizatorii lor. Se numește aplicație web progresivă.

După cum spune Alex Russell într-una dintre postările sale de blog:

„Se întâmplă din când în când pe web să apară tehnologii puternice fără beneficiul departamentelor de marketing sau al ambalajului elegant. Ei zăbovesc și cresc la periferie, devenind o pălărie veche pentru un grup mic, rămânând aproape invizibili pentru toți ceilalți. Până când cineva le numește.”

O experiență netedă pe web, uneori cunoscută ca o aplicație web progresivă

Aplicațiile web progresive (PWA) sunt mai mult o metodologie care implică o combinație de tehnologii pentru a crea aplicații web puternice. Cu o experiență îmbunătățită a utilizatorului, oamenii vor petrece mai mult timp pe site-uri web și vor vedea mai multe reclame. Ei tind să cumpere mai mult și, cu actualizări de notificări, este mai probabil să viziteze des. Financial Times și-a abandonat aplicațiile native în 2011 și a creat o aplicație web folosind cele mai bune tehnologii disponibile la acea vreme. Acum, produsul a crescut într-un PWA cu drepturi depline.

Dar de ce, după tot acest timp, ați construi o aplicație web atunci când o aplicație nativă face treaba suficient de bine?

Să analizăm câteva dintre valorile partajate în Google IO 17.

Mai multe după săritură! Continuați să citiți mai jos ↓

Cinci miliarde de dispozitive sunt conectate la web, ceea ce face din web cea mai mare platformă din istoria computerelor. Pe web mobil, 11,4 milioane de vizitatori unici lunar merg la primele 1000 de proprietăți web, iar 4 milioane merg la primele mii de aplicații. Web-ul mobil strânge de aproximativ patru ori mai mulți utilizatori decât aplicațiile native. Dar acest număr scade drastic când vine vorba de logodna.

Un utilizator petrece în medie 188,6 minute în aplicațiile native și doar 9,3 minute pe web-ul mobil. Aplicațiile native profită de puterea sistemelor de operare pentru a trimite notificări push pentru a oferi utilizatorilor actualizări importante. Ele oferă o experiență de utilizator mai bună și pornesc mai rapid decât site-urile web dintr-un browser. În loc să tasteze o adresă URL în browserul web, utilizatorii trebuie doar să atingă pictograma unei aplicații pe ecranul de pornire.

Majoritatea vizitatorilor de pe web este puțin probabil să revină, așa că dezvoltatorii au venit cu soluția de a le arăta bannere pentru a instala aplicații native, în încercarea de a-i menține profund implicați. Dar apoi, utilizatorii ar trebui să treacă prin procedura obositoare de a instala binarul unei aplicații native. Forțarea utilizatorilor să instaleze o aplicație este enervant și reduce și mai mult șansele ca aceștia să o instaleze în primul rând. Oportunitatea pentru web este clară.

Lectură recomandată : Nativ și PWA: alegeri, nu provocări!

Dacă aplicațiile web vin cu o experiență bogată de utilizator, notificări push, asistență offline și încărcare instantanee, ele pot cuceri lumea. Aceasta este ceea ce face o aplicație web progresivă.

Un PWA oferă o experiență bogată pentru utilizator, deoarece are mai multe puncte forte:

  • Rapid
    Interfața de utilizare nu este scazută. Derularea este lină. Iar aplicația răspunde rapid la interacțiunea utilizatorului.

  • De încredere
    Un site obișnuit îi obligă pe utilizatori să aștepte, fără a face nimic, în timp ce este ocupat să facă călătorii către server. Între timp, un PWA încarcă date instantaneu din cache. Un PWA funcționează perfect, chiar și pe o conexiune 2G. Fiecare cerere de rețea pentru a prelua un activ sau o bucată de date trece printr-un lucrător de serviciu (mai multe despre asta mai târziu), care verifică mai întâi dacă răspunsul pentru o anumită solicitare este deja în cache. Când utilizatorii obțin conținut real aproape instantaneu, chiar și cu o conexiune slabă, au mai multă încredere în aplicație și o consideră mai fiabilă.

  • Angajant
    Un PWA poate câștiga un loc pe ecranul de pornire al utilizatorului. Oferă o experiență nativă asemănătoare aplicației, oferind o zonă de lucru pe ecran complet. Utilizează notificările push pentru a menține utilizatorii implicați.

Acum că știm ce aduc PWA-urile la masă, să intrăm în detalii despre ceea ce conferă PWA-urilor un avantaj față de aplicațiile native. PWA-urile sunt construite cu tehnologii precum lucrători de servicii, manifeste de aplicații web, notificări push și structură de date IndexedDB/locală pentru stocarea în cache. Să ne uităm la fiecare în detaliu.

Lucrători de servicii

Un service worker este un fișier JavaScript care rulează în fundal fără a interfera cu interacțiunile utilizatorului. Toate cererile GET către server trec printr-un lucrător de service. Acționează ca un proxy pe partea clientului. Prin interceptarea cererilor de rețea, acesta preia controlul complet asupra răspunsului care este trimis înapoi către client. Un PWA se încarcă instantaneu, deoarece lucrătorii de service elimină dependența de rețea, răspunzând cu date din cache.

Un lucrător al serviciului poate intercepta doar o solicitare de rețea care este în domeniul său. De exemplu, un lucrător al serviciului rădăcină poate intercepta toate solicitările de preluare care provin de la o pagină web. Un lucrător de service funcționează ca un sistem bazat pe evenimente. Intră într-o stare latentă când nu este necesar, conservând astfel memoria. Pentru a folosi un service worker într-o aplicație web, trebuie mai întâi să îl înregistrăm pe pagină cu 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') } })()

Mai întâi verificăm dacă browserul acceptă lucrătorii de service. Pentru a înregistra un lucrător de servicii într-o aplicație web, oferim URL-ul acestuia ca parametru al funcției de register , disponibilă în navigator.serviceWorker ( navigator este un API web care permite scripturilor să se înregistreze și să își desfășoare activitățile). Un lucrător de servicii este înregistrat o singură dată. Înregistrarea nu are loc la fiecare încărcare a paginii. Browserul descarcă fișierul service worker ( ./service-worker.js ) numai dacă există o diferență de octeți între service worker-ul activat existent și cel mai nou sau dacă adresa URL a acestuia s-a schimbat.

Lucrătorul de servicii de mai sus va intercepta toate cererile care vin de la rădăcină ( / ). Pentru a limita domeniul de aplicare al unui lucrător de servicii, am trece un parametru opțional cu una dintre chei ca domeniu de aplicare.

 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) }) }

Lucrătorul de servicii de mai sus va intercepta solicitările care au /books în URL. De exemplu, nu va intercepta cererea cu /products , dar ar putea foarte bine intercepta solicitările cu /books/products .

După cum sa menționat, un lucrător de service funcționează ca un sistem bazat pe evenimente. Acesta ascultă evenimente (instalare, activare, preluare, împingere) și, în consecință, apelează handlerul de evenimente respectiv. Unele dintre aceste evenimente fac parte din ciclul de viață al unui lucrător de servicii, care trece prin aceste evenimente în secvență pentru a fi activat.

Instalare

Odată ce un lucrător de service a fost înregistrat cu succes, este declanșat un eveniment de instalare. Acesta este un loc bun pentru a face munca de inițializare, cum ar fi configurarea memoriei cache sau crearea de depozite de obiecte în IndexedDB. (IndexedDB va avea mai mult sens pentru dvs. odată ce vom intra în detalii. Pentru moment, putem spune doar că este o structură de pereche cheie-valoare.)

 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)) ) })

Aici, stocăm în cache unele fișiere, astfel încât următoarea încărcare să fie instantanee. self se referă la instanța lucrătorului de servicii. event.waitUntil îl face pe lucrătorul de serviciu să aștepte până când tot codul din interiorul său s-a terminat de execuție.

Activare

Odată ce un lucrător de service a fost instalat, acesta nu poate asculta încă cererile de preluare. Mai degrabă, un eveniment de activate este declanșat. Dacă niciun lucrător activ de servicii nu operează pe site-ul web în același domeniu, atunci lucrătorul de service instalat este activat imediat. Cu toate acestea, dacă un site web are deja un lucrător de servicii activ, atunci activarea unui nou lucrător de servicii este amânată până când toate filele care operează pe vechiul lucrător de servicii sunt închise. Acest lucru are sens deoarece vechiul lucrător al serviciului ar putea folosi instanța cache-ului care este acum modificată în cea mai nouă. Deci, pasul de activare este un loc bun pentru a scăpa de vechile 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) } }) ) }) ) })

În codul de mai sus, ștergem vechiul cache. Dacă numele unui cache nu se potrivește cu cacheWhitelist , atunci acesta este șters. Pentru a sări peste faza de așteptare și pentru a activa imediat lucrătorul de service, folosim skip.waiting() .

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

Odată ce lucrătorul de serviciu este activat, acesta poate asculta cererile de preluare și evenimentele push.

Preluare Event Handler

Ori de câte ori o pagină web lansează o solicitare de preluare pentru o resursă prin rețea, evenimentul de preluare de la lucrătorul de servicii este apelat. Managerul de evenimente de preluare caută mai întâi resursa solicitată în cache. Dacă este prezent în cache, atunci returnează răspunsul cu resursa stocată în cache. În caz contrar, inițiază o cerere de preluare către server, iar atunci când serverul trimite înapoi răspunsul cu resursa solicitată, îl pune în cache pentru solicitările ulterioare.

 /* 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 permite lucrătorului de servicii să trimită un răspuns personalizat clientului.

Offline-mai întâi este acum un lucru. Pentru orice solicitare necritică, trebuie să servim răspunsul din cache, în loc să facem o călătorie la server. Dacă vreun activ nu este prezent în cache, îl primim de la server și apoi îl punem în cache pentru solicitările ulterioare.

Lucrătorii de servicii lucrează numai pe site-uri web HTTPS, deoarece au puterea de a manipula răspunsul la orice solicitare de preluare. Cineva cu intenții rău intenționate ar putea modifica răspunsul la o solicitare pe un site web HTTP. Deci, găzduirea unui PWA pe HTTPS este obligatorie. Lucrătorii de servicii nu întrerup funcționarea normală a DOM. Ei nu pot comunica direct cu pagina web. Pentru a trimite orice mesaj către o pagină web, se folosește mesajele postate.

Notificări push web

Să presupunem că sunteți ocupat să jucați un joc pe mobil și apare o notificare care vă anunță că aveți o reducere de 30% la marca dvs. preferată. Fără nicio altă prelungire, dați clic pe notificare și faceți cumpărături. Obținerea de actualizări live despre, de exemplu, un meci de cricket sau de fotbal sau primirea de e-mailuri și memento-uri importante sub formă de notificări este o problemă importantă atunci când vine vorba de implicarea utilizatorilor cu un produs. Această caracteristică a fost disponibilă numai în aplicațiile native până când a apărut PWA. Un PWA folosește notificările push web pentru a concura cu această funcție puternică pe care aplicațiile native o oferă imediat. Un utilizator va primi în continuare o notificare push web chiar dacă PWA nu este deschis în niciuna dintre filele browserului și chiar dacă browserul nu este deschis.

O aplicație web trebuie să ceară permisiunea utilizatorului pentru a le trimite notificări push.

Solicitare browser pentru a cere permisiunea pentru notificările Web Push
Solicitare browser pentru a cere permisiunea pentru notificările Web Push. (Previzualizare mare)

Odată ce utilizatorul confirmă făcând clic pe butonul „Permite”, browserul generează un simbol unic de abonament. Acest simbol este unic pentru acest dispozitiv. Formatul jetonului de abonament generat de Chrome este următorul:

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

Punctul endpoint conținut în simbolul de mai sus va fi unic pentru fiecare abonament. Pe un site obișnuit, mii de utilizatori ar fi de acord să primească notificări push și pentru fiecare dintre ei, acest endpoint ar fi unic. Deci, cu ajutorul acestui endpoint , aplicația poate viza acești utilizatori în viitor, trimițându-le notificări push. expirationTime este perioada de timp în care abonamentul este valabil pentru un anumit dispozitiv. Dacă expirationTime este de 20 de zile, înseamnă că abonamentul push al utilizatorului va expira după 20 de zile și utilizatorul nu va putea primi notificări push pentru abonamentul mai vechi. În acest caz, browserul va genera un nou simbol de abonament pentru dispozitivul respectiv. Cheile auth și p256dh sunt folosite pentru criptare.

Acum, pentru a trimite notificări push acestor mii de utilizatori în viitor, mai întâi trebuie să le salvăm jetoanele de abonament respective. Este sarcina serverului de aplicații (serverul back-end, poate un script Node.js) să trimită notificări push acestor utilizatori. Acest lucru ar putea suna la fel de simplu ca a face o solicitare POST la adresa URL a punctului final cu datele de notificare din sarcina utilă a cererii. Cu toate acestea, trebuie remarcat faptul că, dacă un utilizator nu este online atunci când o notificare push destinată lui este declanșată de server, ar trebui să primească acea notificare odată ce revine online. Serverul ar trebui să se ocupe de astfel de scenarii, împreună cu trimiterea de mii de solicitări către utilizatori. Un server care ține evidența conexiunii utilizatorului pare complicat. Deci, ceva la mijloc ar fi responsabil pentru rutarea notificărilor push web de la server la client. Acesta se numește serviciu push și fiecare browser are propria sa implementare a unui serviciu push. Browserul trebuie să transmită următoarele informații serviciului push pentru a trimite orice notificare:

  1. Timpul de a trăi
    Acesta este cât timp trebuie să fie pus în coadă un mesaj, în cazul în care nu este livrat utilizatorului. Odată ce acest timp a trecut, mesajul va fi eliminat din coadă.
  2. Urgența mesajului
    Acest lucru este astfel încât serviciul push să păstreze bateria utilizatorului trimițând doar mesaje cu prioritate ridicată.

Serviciul push direcționează mesajele către client. Deoarece push trebuie să fie primit de client chiar dacă aplicația web respectivă nu este deschisă în browser, evenimentele push trebuie să fie ascultate de ceva care monitorizează continuu în fundal. Ai ghicit: asta e treaba lucrătorului de serviciu. Lucrătorul de service ascultă evenimentele push și face treaba de a afișa notificări utilizatorului.

Așadar, acum știm că browserul, serviciul push, serviciul de lucru și serverul de aplicații funcționează în armonie pentru a trimite notificări push către utilizator. Să ne uităm la detaliile implementării.

Web Push Client

A cere permisiunea utilizatorului este un lucru de o singură dată. Dacă un utilizator a acordat deja permisiunea de a primi notificări push, nu ar trebui să-i mai întrebăm. Valoarea permisiunii este salvată în 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 } }) } }) }

În metoda de subscribe de mai sus, transmitem userVisibleOnly și applicationServerKey pentru a genera un token de abonament. Proprietatea userVisibleOnly ar trebui să fie întotdeauna adevărată, deoarece îi spune browserului că orice notificare push trimisă de server va fi afișată clientului. Pentru a înțelege scopul applicationServerKey , să luăm în considerare un scenariu.

Dacă cineva obține miile dvs. de jetoane de abonament, ar putea foarte bine să trimită notificări către punctele finale conținute în aceste abonamente. Nu există nicio modalitate ca punctul final să fie legat de identitatea dvs. unică. Pentru a oferi o identitate unică jetoanelor de abonament generate în aplicația dvs. web, folosim protocolul VAPID. Cu VAPID, serverul de aplicații se identifică în mod voluntar la serviciul push în timp ce trimite notificări push. Generăm două chei astfel:

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

web-push este un modul npm. vapidKeys va avea o cheie publică și o cheie privată. Cheia serverului de aplicații folosită mai sus este cheia publică.

Web Push Server

Sarcina serverului web push (server de aplicații) este simplă. Trimite o sarcină utilă de notificare către jetoanele de abonament.

 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)

Utilizează metoda sendNotification din biblioteca web push.

Lucrători de servicii

Lucrătorul de service arată notificarea utilizatorului ca atare:

 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) ) })

Până acum, am văzut cum un lucrător al serviciului folosește memoria cache pentru a stoca cereri și face un PWA rapid și fiabil și am văzut cum notificările push web mențin utilizatorii implicați.

Pentru a stoca o mulțime de date pe partea clientului pentru suport offline, avem nevoie de o structură de date gigantică. Să ne uităm la Financial Times PWA. Trebuie să fii martor la puterea acestei structuri de date. Încărcați adresa URL în browser și apoi opriți conexiunea la internet. Reîncărcați pagina. Gah! Mai functioneaza? Este. (După cum am spus, offline este noul negru.) Datele nu provin de la fire. Se servește din casă. Mergeți la fila „Aplicații” din Instrumentele pentru dezvoltatori Chrome. Sub „Stocare”, veți găsi „IndexedDB”.

IndexedDB stochează datele articolelor în Financial Times PWA
IndexedDB pe Financial Times PWA. (Previzualizare mare)

Verificați magazinul de obiecte „Articole” și extindeți oricare dintre articole pentru a vedea singur magia. Financial Times a stocat aceste date pentru asistență offline. Această structură de date care ne permite să stocăm o cantitate masivă de date se numește IndexedDB. IndexedDB este o bază de date orientată pe obiecte bazată pe JavaScript pentru stocarea datelor structurate. Putem crea diferite depozite de obiecte în această bază de date pentru diverse scopuri. De exemplu, așa cum putem vedea în imaginea de mai sus, „Resurse”, „ArticleImages” și „Articole” sunt numite ca depozite de obiecte. Fiecare înregistrare dintr-un depozit de obiecte este identificată în mod unic cu o cheie. IndexedDB poate fi folosit chiar și pentru a stoca fișiere și blob-uri.

Să încercăm să înțelegem IndexedDB creând o bază de date pentru stocarea cărților.

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

Dacă baza de date booksdb nu există deja, codul de mai sus va crea o bază de date booksdb . Al doilea parametru al metodei deschise este versiunea bazei de date. Specificarea unei versiuni are grijă de modificările legate de schemă care ar putea avea loc în viitor. De exemplu, booksdb are acum un singur tabel, dar când aplicația crește, intenționăm să mai adăugăm două tabele la ea. Pentru a ne asigura că baza noastră de date este sincronizată cu schema actualizată, vom specifica o versiune mai mare decât cea anterioară.

Apelarea metodei open nu deschide imediat baza de date. Este o solicitare asincronă care returnează un obiect IDBOpenDBRequest . Acest obiect are proprietăți de succes și eroare; va trebui să scriem handlere adecvate pentru aceste proprietăți pentru a gestiona starea conexiunii noastre.

 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' }) }

Pentru a gestiona crearea sau modificarea depozitelor de obiecte (magazinele de obiecte sunt analoge cu tabelele bazate pe SQL - au o structură cheie-valoare), metoda onupgradeneeded este apelată pe obiectul openIdbRequest . Metoda onupgradeneeded va fi invocată ori de câte ori versiunea se schimbă. În fragmentul de cod de mai sus, creăm un magazin de obiecte de cărți cu cheie unică ca ID.

Să presupunem că, după implementarea acestei bucăți de cod, trebuie să creăm încă un depozit de obiecte, numit users . Deci, acum versiunea bazei noastre de date va fi 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' }) } }

Am stocat în cache dbInstance în handlerul de evenimente de succes al cererii deschise. Pentru a prelua sau adăuga date în IndexedDB, vom folosi dbInstance . Să adăugăm câteva înregistrări de cărți în magazinul nostru de obiecte de cărți.

 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') }

Facem uz de transactions , în special în timp ce scriem înregistrări pe magazine de obiecte. O tranzacție este pur și simplu un înveliș în jurul unei operațiuni pentru a asigura integritatea datelor. Dacă oricare dintre acțiunile unei tranzacții eșuează, atunci nu se efectuează nicio acțiune în baza de date.

Să modificăm o înregistrare de carte cu metoda put :

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

Să recuperăm o înregistrare de carte cu metoda 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.') }

Adăugarea pictogramei pe ecranul de pornire

Acum că nu există aproape nicio distincție între o aplicație PWA și o aplicație nativă, este logic să oferim PWA o poziție privilegiată. Dacă site-ul dvs. îndeplinește criteriile de bază ale unui PWA (găzduit pe HTTPS, se integrează cu lucrătorii de servicii și are un manifest.json ) și după ce utilizatorul a petrecut ceva timp pe pagina web, browserul va invoca un prompt în partea de jos, întrebând utilizatorul să adauge aplicația pe ecranul de pornire, după cum se arată mai jos:

Solicitați adăugarea PWA din Financial Times pe ecranul de pornire
Solicitați adăugarea PWA din Financial Times pe ecranul de pornire. (Previzualizare mare)

Când un utilizator face clic pe „Adăugați FT la ecranul de pornire”, PWA ajunge să pună piciorul pe ecranul de pornire, precum și în sertarul de aplicații. Când un utilizator caută orice aplicație pe telefonul său, vor fi listate toate PWA care se potrivesc cu interogarea de căutare. Acestea vor fi văzute și în setările de sistem, ceea ce face ca utilizatorii să le gestioneze ușor. În acest sens, un PWA se comportă ca o aplicație nativă.

PWA-urile folosesc manifest.json pentru a oferi această caracteristică. Să ne uităm într-un fișier simplu 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" }] }

short_name apare pe ecranul de start al utilizatorului și în setările sistemului. name apare în promptul Chrome și pe ecranul de introducere. Ecranul de start este ceea ce vede utilizatorul atunci când aplicația se pregătește pentru lansare. start_url este ecranul principal al aplicației dvs. Este ceea ce primesc utilizatorii când ating o pictogramă de pe ecranul de pornire. background_color este folosit pe ecranul de introducere. theme_color stabilește culoarea barei de instrumente. Valoarea standalone pentru modul de display spune că aplicația trebuie să fie operată în modul ecran complet (ascunzând bara de instrumente a browserului). Când un utilizator instalează un PWA, dimensiunea acestuia este doar în kiloocteți, mai degrabă decât în ​​megaocteți ai aplicațiilor native.

Lucrătorii de servicii, notificările push web, IndexedDB și poziția pe ecranul de pornire compensează suportul offline, fiabilitatea și implicarea. Trebuie remarcat faptul că un lucrător de service nu prinde viață și nu începe să-și facă treaba chiar de la prima încărcare. Prima încărcare va fi încă lentă până când toate activele statice și alte resurse vor fi stocate în cache. Putem implementa câteva strategii pentru a optimiza prima încărcare.

Gruparea activelor

Toate resursele, inclusiv HTML, foile de stil, imaginile și JavaScript, vor fi preluate de pe server. Cu cât sunt mai multe fișiere, cu atât sunt necesare mai multe solicitări HTTPS pentru a le prelua. Putem folosi pachete precum WebPack pentru a grupa activele noastre statice, reducând astfel numărul de solicitări HTTP către server. WebPack face o treabă grozavă de a optimiza în continuare pachetul folosind tehnici precum împărțirea codului (adică gruparea doar a acelor fișiere care sunt necesare pentru încărcarea curentă a paginii, în loc să le grupeze pe toate împreună) și agitarea arborilor (adică eliminarea dependențelor duplicate sau dependențe care sunt importate, dar nu sunt utilizate în cod).

Reducerea călătoriilor dus-întors

Unul dintre principalele motive pentru încetineala pe web este latența rețelei. Timpul necesar unui octet pentru a călători de la A la B variază în funcție de conexiunea la rețea. De exemplu, o anumită călătorie dus-întors prin Wi-Fi durează 50 de milisecunde și 500 de milisecunde pentru o conexiune 3G, dar 2500 de milisecunde pentru o conexiune 2G. Aceste cereri sunt trimise folosind protocolul HTTP, ceea ce înseamnă că, în timp ce o anumită conexiune este utilizată pentru o solicitare, aceasta nu poate fi utilizată pentru nicio altă solicitare până când răspunsul cererii anterioare este servit. Un site web poate face șase solicitări HTTP asincrone simultan, deoarece șase conexiuni sunt disponibile pentru un site web pentru a face solicitări HTTP. Un site web face aproximativ 100 de solicitări; deci, cu maximum șase conexiuni disponibile, un utilizator ar putea ajunge să cheltuiască aproximativ 833 de milisecunde într-o singură călătorie dus-întors. (Calculul este de 833 milisecunde - 1006 = 1666 . Trebuie să împărțim 1666 la 2, deoarece calculăm timpul petrecut într-o călătorie dus-întors.) Cu HTTP2, timpul de răspuns este redus drastic. HTTP2 nu blochează capul de conexiune, astfel încât cererile multiple pot fi trimise simultan.

Majoritatea răspunsurilor HTTP conțin antete last-modified și etag . Antetul last-modified este data la care fișierul a fost modificat ultima dată, iar un etag este o valoare unică bazată pe conținutul fișierului. Acesta va fi modificat numai atunci când conținutul unui fișier este modificat. Ambele antete pot fi folosite pentru a evita descărcarea din nou a fișierului dacă o versiune în cache este deja disponibilă local. Dacă browserul are o versiune a acestui fișier disponibilă local, poate adăuga oricare dintre aceste două antete în cerere ca atare:

Adăugați ETag și anteturi Last-Modified pentru a preveni descărcarea materialelor valide din cache
ETag și anteturile cu ultima modificare. (Previzualizare mare)

Serverul poate verifica dacă conținutul fișierului s-a modificat. 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. (Previzualizare mare)

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> .

Concluzie

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!