Obszerny przewodnik po progresywnych aplikacjach internetowych

Opublikowany: 2022-03-10
Szybkie podsumowanie ↬ W tym artykule przyjrzymy się problemom użytkowników, którzy przeglądają stare witryny bez PWA, oraz obietnicy PWA, która sprawi, że sieć będzie świetna. Poznasz większość ważnych technologii, które tworzą fajne PWA, takie jak Service Worker, powiadomienia web push i IndexedDB.

To były urodziny mojego taty i chciałam zamówić dla niego ciasto czekoladowe i koszulę. Udałem się do Google, aby wyszukać ciastka czekoladowe i kliknąłem pierwszy link w wynikach wyszukiwania. Przez kilka sekund ekran był pusty; Nie rozumiałem, co się dzieje. Po kilku sekundach cierpliwego wpatrywania się, mój ekran telefonu zapełnił się pysznie wyglądającymi ciastami. Gdy tylko kliknąłem na jeden z nich, aby sprawdzić jego szczegóły, pojawiło się brzydkie, tłuste wyskakujące okienko z prośbą o zainstalowanie aplikacji na Androida, abym mógł uzyskać jedwabiście gładkie wrażenia podczas zamawiania ciasta.

To było rozczarowujące. Sumienie nie pozwoliło mi kliknąć przycisku „Zainstaluj”. Chciałam tylko zamówić mały tort i ruszyć w drogę.

Kliknąłem na ikonę krzyża po prawej stronie wyskakującego okienka, aby jak najszybciej się z niego wydostać. Ale wtedy wyskakujące okienko instalacji znalazło się na dole ekranu, zajmując jedną czwartą miejsca. A przy niestabilnym interfejsie użytkownika przewijanie w dół było wyzwaniem. Jakoś udało mi się zamówić holenderski tort.

Po tym strasznym doświadczeniu następnym wyzwaniem było zamówienie koszulki dla taty. Tak jak poprzednio, szukam w Google koszulek. Kliknąłem w pierwszy link i w mgnieniu oka cała treść znalazła się tuż przede mną. Przewijanie było płynne. Brak banera instalacyjnego. Czułem się tak, jakbym przeglądał natywną aplikację. Był moment, w którym zrezygnowałem z mojego strasznego połączenia internetowego, ale nadal mogłem zobaczyć zawartość zamiast gry z dinozaurami. Nawet z moim tandetnym internetem udało mi się zamówić koszulę i dżinsy dla mojego taty. Najbardziej zaskakujące było to, że otrzymywałem powiadomienia o moim zamówieniu.

Nazwałbym to jedwabiście gładkim doświadczeniem. Ci ludzie robili coś dobrze. Każda strona internetowa powinna to robić dla swoich użytkowników. Nazywa się to progresywną aplikacją internetową.

Jak stwierdza Alex Russell w jednym ze swoich postów na blogu:

„Czasem zdarza się, że w sieci pojawiają się potężne technologie bez korzyści działów marketingu czy zgrabnych opakowań. Pozostają i rosną na peryferiach, stając się starymi kapeluszami dla małej grupy, pozostając prawie niewidocznymi dla wszystkich innych. Dopóki ktoś ich nie nazwie.

Jedwabiście płynne działanie w sieci, czasami nazywane progresywną aplikacją internetową

Progresywne aplikacje internetowe (PWA) to bardziej metodologia, która obejmuje połączenie technologii w celu stworzenia potężnych aplikacji internetowych. Dzięki lepszemu doświadczeniu użytkownika ludzie będą spędzać więcej czasu na stronach internetowych i widzieć więcej reklam. Zwykle kupują więcej, a dzięki aktualizacjom powiadomień są bardziej skłonni do częstych odwiedzin. Financial Times porzucił swoje natywne aplikacje w 2011 roku i zbudował aplikację internetową, korzystając z najlepszych dostępnych wówczas technologii. Teraz produkt urósł do pełnoprawnego PWA.

Ale dlaczego po tylu latach miałbyś budować aplikację internetową, skoro natywna aplikacja wykonuje zadanie wystarczająco dobrze?

Przyjrzyjmy się niektórym metrykom udostępnionym w Google IO 17.

Więcej po skoku! Kontynuuj czytanie poniżej ↓

Pięć miliardów urządzeń jest podłączonych do sieci, co czyni ją największą platformą w historii informatyki. W internecie mobilnym 11,4 miliona unikalnych użytkowników miesięcznie przechodzi do 1000 najpopularniejszych usług internetowych, a 4 miliony do tysiąca najpopularniejszych aplikacji. Internet mobilny gromadzi około cztery razy więcej użytkowników niż aplikacje natywne. Ale ta liczba gwałtownie spada, jeśli chodzi o zaangażowanie.

Użytkownik spędza średnio 188,6 minuty w aplikacjach natywnych i tylko 9,3 minuty w sieci mobilnej. Aplikacje natywne wykorzystują moc systemów operacyjnych do wysyłania powiadomień push w celu dostarczenia użytkownikom ważnych aktualizacji. Zapewniają lepsze wrażenia użytkownika i szybciej się uruchamiają niż strony internetowe w przeglądarce. Zamiast wpisywać adres URL w przeglądarce internetowej, wystarczy dotknąć ikony aplikacji na ekranie głównym.

Większość odwiedzających w sieci raczej nie wróci, więc programiści wymyślili obejście polegające na wyświetlaniu im banerów do instalowania natywnych aplikacji, aby utrzymać ich głębokie zaangażowanie. Ale wtedy użytkownicy musieliby przejść przez męczącą procedurę instalowania plików binarnych aplikacji natywnej. Zmuszanie użytkowników do zainstalowania aplikacji jest denerwujące i dodatkowo zmniejsza szanse, że zainstalują ją w pierwszej kolejności. Szansa na sieć jest oczywista.

Zalecana literatura : Native i PWA: wybory, a nie pretendenci!

Jeśli aplikacje internetowe oferują bogate doświadczenie użytkownika, powiadomienia push, obsługę offline i natychmiastowe ładowanie, mogą podbić świat. To właśnie robi progresywna aplikacja internetowa.

PWA zapewnia bogate doświadczenie użytkownika, ponieważ ma kilka mocnych stron:

  • Szybko
    Interfejs użytkownika nie jest niestabilny. Przewijanie jest płynne. A aplikacja szybko reaguje na interakcję użytkownika.

  • Niezawodny
    Normalna strona internetowa zmusza użytkowników do czekania, nic nie robiąc, podczas gdy jest zajęta robieniem przejazdów na serwer. Tymczasem PWA ładuje dane natychmiast z pamięci podręcznej. PWA działa bezproblemowo, nawet przy połączeniu 2G. Każde żądanie sieciowe mające na celu pobranie zasobu lub fragmentu danych przechodzi przez pracownika usługi (więcej o tym później), który najpierw sprawdza, czy odpowiedź na określone żądanie znajduje się już w pamięci podręcznej. Gdy użytkownicy niemal natychmiast uzyskują prawdziwe treści, nawet przy słabym połączeniu, bardziej ufają aplikacji i postrzegają ją jako bardziej niezawodną.

  • Ujmujący
    PWA może zarobić miejsce na ekranie głównym użytkownika. Oferuje natywne wrażenia podobne do aplikacji, zapewniając pełnoekranowy obszar roboczy. Wykorzystuje powiadomienia push, aby utrzymać zaangażowanie użytkowników.

Teraz, gdy wiemy, co wnoszą PWA do stołu, przejdźmy do szczegółów, co daje PWA przewagę nad aplikacjami natywnymi. PWA są zbudowane z wykorzystaniem technologii, takich jak Service Workers, manifesty aplikacji internetowych, powiadomienia push i IndexedDB/lokalna struktura danych do buforowania. Przyjrzyjmy się każdemu szczegółowo.

Pracownicy usług

Service Worker to plik JavaScript działający w tle bez zakłócania interakcji użytkownika. Wszystkie żądania GET kierowane do serwera przechodzą przez pracownika serwisu. Działa jak serwer proxy po stronie klienta. Przechwytując żądania sieciowe, przejmuje pełną kontrolę nad odpowiedzią wysyłaną z powrotem do klienta. PWA ładuje się natychmiast, ponieważ pracownicy usług eliminują zależność od sieci, odpowiadając danymi z pamięci podręcznej.

Service Worker może przechwycić tylko żądanie sieciowe, które znajduje się w jego zakresie. Na przykład, Service Worker o zasięgu głównym może przechwycić wszystkie żądania pobrania pochodzące ze strony internetowej. Pracownik serwisu działa jako system sterowany zdarzeniami. Przechodzi w stan uśpienia, gdy nie jest potrzebny, oszczędzając w ten sposób pamięć. Aby skorzystać z service workera w aplikacji webowej musimy najpierw zarejestrować go na stronie za pomocą 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') } })()

Najpierw sprawdzamy, czy przeglądarka obsługuje pracowników usług. Aby zarejestrować pracownika serwisu w aplikacji webowej, podajemy jego adres URL jako parametr do funkcji register , dostępnej w navigator.serviceWorker ( navigator to webowe API, które pozwala skryptom na samodzielną rejestrację i wykonywanie swoich czynności). Pracownik serwisu rejestruje się tylko raz. Rejestracja nie odbywa się przy każdym załadowaniu strony. Przeglądarka pobiera plik service worker ( ./service-worker.js ) tylko wtedy, gdy istnieje różnica bajtów między istniejącym aktywowanym service workerem a nowszym lub jeśli jego adres URL uległ zmianie.

Powyższy service worker przechwyci wszystkie żądania pochodzące z katalogu głównego ( / ). Aby ograniczyć zakres service workera, jako zakres przekażemy opcjonalny parametr z jednym z kluczy.

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

Powyższy Service Worker przechwyci żądania, które mają /books w adresie URL. Na przykład nie przechwyci żądania za pomocą /products , ale może bardzo dobrze przechwycić żądania za pomocą /books/products .

Jak wspomniano, pracownik serwisu działa jako system sterowany zdarzeniami. Nasłuchuje zdarzeń (instalacja, aktywacja, pobieranie, wypychanie) i odpowiednio wywołuje odpowiedni program obsługi zdarzeń. Niektóre z tych zdarzeń są częścią cyklu życia pracownika usługi, który przechodzi kolejno przez te zdarzenia, aby się aktywować.

Instalacja

Po pomyślnym zarejestrowaniu pracownika serwisu uruchamiane jest zdarzenie instalacji. Jest to dobre miejsce do wykonywania prac inicjalizacji, takich jak konfigurowanie pamięci podręcznej lub tworzenie składnic obiektów w IndexedDB. (IndexedDB nabierze dla Ciebie większego sensu, gdy przejdziemy do jego szczegółów. Na razie możemy po prostu powiedzieć, że jest to struktura pary klucz-wartość).

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

Tutaj buforujemy niektóre pliki, aby następne ładowanie było natychmiastowe. self odnosi się do instancji Service Worker. event.waitUntil powoduje, że Service Worker czeka, aż cały znajdujący się w nim kod zakończy wykonywanie.

Aktywacja

Po zainstalowaniu Service Worker nie może on jeszcze nasłuchiwać żądań pobrania. Wywoływane jest raczej zdarzenie activate . Jeżeli na stronie w tym samym zakresie nie działa żaden aktywny service worker, wówczas zainstalowany service worker zostaje natychmiast aktywowany. Jeśli jednak strona internetowa ma już aktywnego service workera, to aktywacja nowego service workera jest opóźniona do momentu zamknięcia wszystkich zakładek działających na starym service workerze. Ma to sens, ponieważ stary Service Worker może używać instancji pamięci podręcznej, która jest teraz zmodyfikowana w nowszym. Tak więc etap aktywacji jest dobrym miejscem na pozbycie się starych pamięci podręcznych.

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

W powyższym kodzie usuwamy starą pamięć podręczną. Jeśli nazwa pamięci podręcznej nie zgadza się z nazwą cacheWhitelist , zostanie ona usunięta. Aby pominąć fazę oczekiwania i natychmiast aktywować service worker, używamy skip.waiting() .

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

Po aktywacji Service Worker może nasłuchiwać żądań pobierania i zdarzeń wypychania.

Program obsługi zdarzeń pobierania

Za każdym razem, gdy strona internetowa wysyła żądanie pobrania zasobu przez sieć, wywoływane jest zdarzenie pobierania od pracownika serwisu. Program obsługi zdarzeń pobierania najpierw szuka żądanego zasobu w pamięci podręcznej. Jeśli jest obecny w pamięci podręcznej, zwraca odpowiedź z buforowanym zasobem. W przeciwnym razie inicjuje żądanie pobrania do serwera, a gdy serwer odsyła odpowiedź z żądanym zasobem, umieszcza ją w pamięci podręcznej dla kolejnych żądań.

 /* 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 umożliwia pracownikowi serwisu wysłanie dostosowanej odpowiedzi do klienta.

Offline-first jest teraz rzeczą. W przypadku każdego niekrytycznego żądania musimy podać odpowiedź z pamięci podręcznej, zamiast jechać na serwer. Jeśli jakiś zasób nie znajduje się w pamięci podręcznej, pobieramy go z serwera, a następnie buforujemy dla kolejnych żądań.

Service Workery działają tylko w witrynach HTTPS, ponieważ mogą manipulować odpowiedzią na każde żądanie pobrania. Ktoś o złośliwych zamiarach może manipulować odpowiedzią na żądanie w witrynie HTTP. Tak więc hostowanie PWA przez HTTPS jest obowiązkowe. Pracownicy serwisu nie zakłócają normalnego funkcjonowania DOM. Nie mogą komunikować się bezpośrednio ze stroną internetową. Aby wysłać dowolną wiadomość na stronę internetową, korzysta z wiadomości pocztowych.

Powiadomienia Web Push

Załóżmy, że jesteś zajęty graniem w grę na telefonie komórkowym i pojawia się powiadomienie z informacją o 30% zniżce na Twoją ulubioną markę. Bez zbędnych ceregieli klikasz powiadomienie i robisz zakupy. Otrzymywanie na żywo aktualizacji, powiedzmy, meczu krykieta lub piłki nożnej lub otrzymywanie ważnych e-maili i przypomnień jako powiadomień to wielka sprawa, jeśli chodzi o zaangażowanie użytkowników w produkt. Ta funkcja była dostępna tylko w aplikacjach natywnych, dopóki nie pojawiło się PWA. PWA korzysta z powiadomień web push, aby konkurować z tą potężną funkcją, którą natywne aplikacje dostarczają po wyjęciu z pudełka. Użytkownik nadal otrzymywałby powiadomienie web push, nawet jeśli PWA nie jest otwarte w żadnej z zakładek przeglądarki i nawet jeśli przeglądarka nie jest otwarta.

Aplikacja internetowa musi poprosić użytkownika o pozwolenie na wysyłanie powiadomień push.

Monit przeglądarki o zgodę na powiadomienia Web Push
Pytaj przeglądarki o zgodę na powiadomienia Web Push. (duży podgląd)

Gdy użytkownik potwierdzi, klikając przycisk „Zezwól”, przeglądarka generuje unikalny token subskrypcji. Ten token jest unikalny dla tego urządzenia. Format tokena subskrypcji generowanego przez Chrome jest następujący:

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

Punkt endpoint zawarty w powyższym tokenie będzie unikalny dla każdej subskrypcji. Na przeciętnej stronie internetowej tysiące użytkowników zgodziłoby się na otrzymywanie powiadomień push, a dla każdego z nich ten endpoint byłby unikalny. Dzięki temu endpoint aplikacja jest w stanie dotrzeć do tych użytkowników w przyszłości, wysyłając im powiadomienia push. ExpirationTime to czas expirationTime subskrypcji dla określonego urządzenia. Jeśli expirationTime wynosi 20 dni, oznacza to, że subskrypcja wypychana użytkownika wygaśnie po 20 dniach i użytkownik nie będzie mógł otrzymywać powiadomień wypychanych w starszej subskrypcji. W takim przypadku przeglądarka wygeneruje nowy token subskrypcji dla tego urządzenia. Do szyfrowania używane są klucze auth i p256dh .

Teraz, aby w przyszłości wysyłać powiadomienia push do tych tysięcy użytkowników, musimy najpierw zapisać ich odpowiednie tokeny subskrypcji. Zadaniem serwera aplikacji (serwera zaplecza, może skryptu Node.js) jest wysyłanie powiadomień push do tych użytkowników. Może to brzmieć tak prosto, jak wysłanie żądania POST do adresu URL punktu końcowego z danymi powiadomienia w ładunku żądania. Należy jednak zauważyć, że jeśli użytkownik nie jest online, gdy przeznaczone dla niego powiadomienie jest wyzwalane przez serwer, nadal powinien otrzymać to powiadomienie po powrocie do trybu online. Serwer musiałby zadbać o takie scenariusze, wraz z wysyłaniem tysięcy żądań do użytkowników. Serwer śledzący połączenie użytkownika brzmi skomplikowanie. A więc coś pośrodku odpowiadałoby za kierowanie powiadomień web push z serwera do klienta. Nazywa się to usługą push, a każda przeglądarka ma własną implementację usługi push. Przeglądarka musi przekazać usłudze push następujące informacje, aby wysłać jakiekolwiek powiadomienie:

  1. Czas na życie
    Jest to czas, przez jaki wiadomość powinna być umieszczona w kolejce na wypadek, gdyby nie została dostarczona użytkownikowi. Po upływie tego czasu wiadomość zostanie usunięta z kolejki.
  2. Pilność wiadomości
    Dzieje się tak, aby usługa push chroniła baterię użytkownika, wysyłając tylko wiadomości o wysokim priorytecie.

Usługa push kieruje komunikaty do klienta. Ponieważ wypychanie musi zostać odebrane przez klienta, nawet jeśli jego odpowiednia aplikacja internetowa nie jest otwarta w przeglądarce, zdarzenia wypychania muszą być nasłuchiwane przez coś, co stale monitoruje w tle. Zgadłeś: to zadanie pracownika serwisu. Pracownik serwisu nasłuchuje zdarzeń wypychania i wykonuje zadanie wyświetlania powiadomień użytkownikowi.

Teraz wiemy, że przeglądarka, usługa push, service worker i serwer aplikacji współpracują ze sobą, aby wysyłać użytkownikowi powiadomienia push. Przyjrzyjmy się szczegółom implementacji.

Klient Web Push

Pytanie o zgodę użytkownika jest jednorazową rzeczą. Jeśli użytkownik przyznał już uprawnienia do otrzymywania powiadomień push, nie powinniśmy pytać ponownie. Wartość uprawnień jest zapisywana w 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 } }) } }) }

W powyższej metodzie subscribe przekazujemy userVisibleOnly i applicationServerKey w celu wygenerowania tokena subskrypcji. Właściwość userVisibleOnly powinna zawsze mieć wartość true, ponieważ informuje przeglądarkę, że wszelkie powiadomienia wypychane wysyłane przez serwer będą pokazywane klientowi. Aby zrozumieć cel applicationServerKey , rozważmy scenariusz.

Jeśli jakaś osoba zdobędzie Twoje tysiące tokenów subskrypcji, może bardzo dobrze wysyłać powiadomienia do punktów końcowych zawartych w tych subskrypcjach. Nie ma możliwości, aby punkt końcowy był połączony z Twoją unikalną tożsamością. Aby zapewnić unikalną tożsamość tokenom subskrypcji generowanym w Twojej aplikacji internetowej, korzystamy z protokołu VAPID. Dzięki VAPID serwer aplikacji dobrowolnie identyfikuje się z usługą push podczas wysyłania powiadomień push. Generujemy dwa klucze takie jak:

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

web-push to moduł npm. vapidKeys będzie miał jeden klucz publiczny i jeden klucz prywatny. Użyty powyżej klucz serwera aplikacji jest kluczem publicznym.

Serwer Web Push

Zadanie serwera web push (serwera aplikacji) jest proste. Wysyła ładunek powiadomienia do tokenów subskrypcji.

 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)

Wykorzystuje metodę sendNotification z biblioteki web push.

Pracownicy usług

Pracownik serwisu wyświetla użytkownikowi powiadomienie w następujący sposób:

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

Do tej pory widzieliśmy, jak service worker wykorzystuje pamięć podręczną do przechowywania żądań oraz sprawia, że ​​PWA jest szybkie i niezawodne, a także widzieliśmy, jak powiadomienia web push utrzymują zaangażowanie użytkowników.

Aby przechowywać wiele danych po stronie klienta w celu obsługi offline, potrzebujemy gigantycznej struktury danych. Przyjrzyjmy się PWA Financial Times. Musisz sam przekonać się o sile tej struktury danych. Załaduj adres URL w przeglądarce, a następnie wyłącz połączenie internetowe. Odśwież stronę. Och! Czy nadal działa? To jest. (Tak jak powiedziałem, offline to nowy czarny.) Dane nie pochodzą z przewodów. Jest podawany z domu. Przejdź do zakładki "Aplikacje" w Narzędziach dla programistów Chrome. W sekcji „Pamięć” znajdziesz „IndexedDB”.

IndexedDB przechowuje dane artykułów w Financial Times PWA
IndexedDB w Financial Times PWA. (duży podgląd)

Zajrzyj do sklepu z przedmiotami „Artykuły” i rozwiń dowolny z przedmiotów, aby zobaczyć magię na własne oczy. Financial Times przechowuje te dane w celu wsparcia offline. Ta struktura danych, która pozwala nam przechowywać ogromne ilości danych, nazywa się IndexedDB. IndexedDB to obiektowa baza danych oparta na JavaScript, służąca do przechowywania uporządkowanych danych. W tej bazie danych możemy tworzyć różne składnice obiektów do różnych celów. Na przykład, jak widać na powyższym obrazku, „Resources”, „ArticleImages” i „Artykuły” są nazywane magazynami obiektów. Każdy rekord w składnicy obiektów jest jednoznacznie identyfikowany za pomocą klucza. IndexedDB może być nawet używany do przechowywania plików i obiektów blob.

Spróbujmy zrozumieć IndexedDB, tworząc bazę danych do przechowywania książek.

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

Jeśli baza danych booksdb jeszcze nie istnieje, powyższy kod utworzy bazę danych booksdb . Drugim parametrem metody open jest wersja bazy danych. Określenie wersji uwzględnia zmiany związane ze schematem, które mogą nastąpić w przyszłości. Na przykład booksdb ma teraz tylko jedną tabelę, ale gdy aplikacja się rozrośnie, zamierzamy dodać do niej kolejne dwie tabele. Aby upewnić się, że nasza baza danych jest zsynchronizowana ze zaktualizowanym schematem, określimy wyższą wersję niż poprzednia.

Wywołanie metody open nie powoduje od razu otwarcia bazy danych. Jest to żądanie asynchroniczne, które zwraca obiekt IDBOpenDBRequest . Ten obiekt ma właściwości sukcesu i błędu; będziemy musieli napisać odpowiednie procedury obsługi tych właściwości, aby zarządzać stanem naszego połączenia.

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

W celu zarządzania tworzeniem lub modyfikacją składnic obiektów (magazyny obiektów są analogiczne do tabel opartych na SQL — mają strukturę klucz-wartość), na onupgradeneeded wywoływana jest metoda openIdbRequest . Metoda onupgradeneeded będzie wywoływana przy każdej zmianie wersji. W powyższym fragmencie kodu tworzymy magazyn obiektów książek z unikalnym kluczem jako identyfikatorem.

Załóżmy, że po wdrożeniu tego fragmentu kodu, musimy utworzyć jeszcze jedną składnicę obiektów, nazywaną users . Tak więc teraz wersja naszej bazy danych to 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' }) } }

Zbuforowaliśmy dbInstance w obsłudze zdarzeń sukcesu otwartego żądania. Aby pobrać lub dodać dane w IndexedDB, użyjemy dbInstance . Dodajmy kilka rekordów księgowych w naszym magazynie obiektów książek.

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

Korzystamy z transactions , zwłaszcza przy pisaniu zapisów na obiektach magazynowych. Transakcja jest po prostu opakowaniem wokół operacji w celu zapewnienia integralności danych. Jeśli którakolwiek z akcji w transakcji nie powiedzie się, żadna akcja nie zostanie wykonana w bazie danych.

Zmodyfikujmy rekord księgi metodą put :

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

Pobierzmy rekord księgi metodą 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.') }

Dodawanie ikony na ekranie głównym

Teraz, gdy nie ma prawie żadnego rozróżnienia między PWA a aplikacją natywną, sensowne jest zaoferowanie PWA pierwszorzędnej pozycji. Jeśli Twoja strona internetowa spełnia podstawowe kryteria PWA (hostowana na HTTPS, integruje się z serviceworkerami i posiada manifest.json ) i po spędzeniu przez użytkownika czasu na stronie, przeglądarka wyświetli na dole monit z pytaniem użytkownik, aby dodać aplikację do swojego ekranu głównego, jak pokazano poniżej:

Monit o dodanie Financial Times PWA na ekranie głównym
Pytaj o dodanie Financial Times PWA na ekranie głównym. (duży podgląd)

Gdy użytkownik kliknie „Dodaj FT do ekranu głównego”, aplikacja PWA ustawi swoją stopę na ekranie głównym, a także w szufladzie aplikacji. Gdy użytkownik wyszukuje jakąkolwiek aplikację na swoim telefonie, wszystkie aplikacje PWA pasujące do zapytania zostaną wyświetlone. Będą one również widoczne w ustawieniach systemu, co ułatwia użytkownikom zarządzanie nimi. W tym sensie PWA zachowuje się jak aplikacja natywna.

Programy PWA wykorzystują plik manifest.json , aby zapewnić tę funkcję. Przyjrzyjmy się prostemu plikowi 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 pojawia się na ekranie głównym użytkownika oraz w ustawieniach systemu. name pojawia się w monicie chrome i na ekranie powitalnym. Ekran powitalny jest tym, co widzi użytkownik, gdy aplikacja przygotowuje się do uruchomienia. start_url to główny ekran Twojej aplikacji. To jest to, co użytkownicy otrzymują po dotknięciu ikony na ekranie głównym. background_color jest używany na ekranie powitalnym. theme_color ustawia kolor paska narzędzi. standalone wartość trybu display mówi, że aplikacja ma działać w trybie pełnoekranowym (ukrywając pasek narzędzi przeglądarki). Gdy użytkownik instaluje PWA, jego rozmiar jest wyrażony jedynie w kilobajtach, a nie w megabajtach aplikacji natywnych.

Service workery, powiadomienia web push, IndexedDB i pozycja na ekranie głównym to wszystko, co zapewnia wsparcie offline, niezawodność i zaangażowanie. Należy zauważyć, że serwisant nie budzi się do życia i nie wykonuje swojej pracy już przy pierwszym obciążeniu. Pierwsze ładowanie będzie nadal powolne, dopóki wszystkie statyczne zasoby i inne zasoby nie zostaną zbuforowane. Możemy wdrożyć kilka strategii, aby zoptymalizować pierwsze ładowanie.

Łączenie zasobów

Wszystkie zasoby, w tym HTML, arkusze stylów, obrazy i JavaScript, należy pobrać z serwera. Im więcej plików, tym więcej żądań HTTPS potrzeba do ich pobrania. Możemy użyć pakietów, takich jak WebPack, aby łączyć nasze statyczne zasoby, zmniejszając w ten sposób liczbę żądań HTTP do serwera. WebPack świetnie radzi sobie z dalszą optymalizacją pakietu za pomocą technik takich jak dzielenie kodu (tj. łączenie tylko tych plików, które są wymagane do bieżącego ładowania strony, zamiast łączenia ich wszystkich razem) i wstrząsanie drzewa (tj. usuwanie zduplikowanych zależności lub zależności, które są importowane, ale nie są używane w kodzie).

Zmniejszenie liczby podróży w obie strony

Jedną z głównych przyczyn powolnego działania sieci jest opóźnienie w sieci. Czas potrzebny na przebycie bajtu z punktu A do B różni się w zależności od połączenia sieciowego. Na przykład szczególna podróż w obie strony przez Wi-Fi zajmuje 50 milisekund i 500 milisekund w przypadku połączenia 3G, ale 2500 milisekund w przypadku połączenia 2G. Żądania te są przesyłane przy użyciu protokołu HTTP, co oznacza, że ​​o ile dane połączenie jest wykorzystywane do realizacji żądania, nie może ono być wykorzystywane do innych żądań do czasu obsłużenia odpowiedzi na poprzednie żądanie. Witryna internetowa może jednocześnie wysyłać sześć asynchronicznych żądań HTTP, ponieważ do witryny internetowej dostępnych jest sześć połączeń umożliwiających wysyłanie żądań HTTP. Przeciętna strona internetowa składa około 100 żądań; więc przy maksymalnie sześciu dostępnych połączeniach użytkownik może spędzić około 833 milisekund podczas jednej podróży w obie strony. (Obliczenia wynoszą 833 milisekundy - 1006 = 1666 . Musimy podzielić 1666 przez 2, ponieważ obliczamy czas spędzony na podróży w obie strony.) Po wdrożeniu protokołu HTTP2 czas realizacji jest drastycznie skrócony. HTTP2 nie blokuje nagłówka połączenia, więc wiele żądań może być wysyłanych jednocześnie.

Większość odpowiedzi HTTP zawiera nagłówki last-modified i etag . Nagłówek last-modified to data ostatniej modyfikacji pliku, a etag to unikalna wartość oparta na zawartości pliku. Zostanie on zmieniony tylko wtedy, gdy zmieni się zawartość pliku. Oba te nagłówki można wykorzystać, aby uniknąć ponownego pobierania pliku, jeśli wersja w pamięci podręcznej jest już dostępna lokalnie. Jeśli przeglądarka ma lokalnie dostępną wersję tego pliku, może dodać do żądania dowolny z tych dwóch nagłówków:

Dodaj ETag i nagłówki Last-Modified, aby zapobiec pobieraniu prawidłowych zasobów z pamięci podręcznej
ETag i nagłówki ostatniej modyfikacji. (duży podgląd)

Serwer może sprawdzić, czy zawartość pliku uległa zmianie. 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. (duży podgląd)

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

Wniosek

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!