Aşamalı Web Uygulamaları İçin Kapsamlı Bir Kılavuz

Yayınlanan: 2022-03-10
Kısa özet ↬ Bu makalede, eski PWA olmayan web sitelerine göz atan kullanıcıların sıkıntılı noktalarına ve PWA'ların web'i harika hale getirme vaadine bakacağız. Servis çalışanları, web push bildirimleri ve IndexedDB gibi harika PWA'lar oluşturan önemli teknolojilerin çoğunu öğreneceksiniz.

Babamın doğum günüydü ve ona çikolatalı kek ve gömlek sipariş etmek istedim. Çikolatalı kek aramak için Google'a gittim ve arama sonuçlarındaki ilk bağlantıya tıkladım. Birkaç saniyeliğine boş bir ekran vardı; Ne olduğunu anlamadım. Birkaç saniye sabırla baktıktan sonra mobil ekranım lezzetli görünen pastalarla doldu. Ayrıntılarını kontrol etmek için birine tıkladığımda, çirkin bir şişman pop-up aldım ve bir pasta sipariş ederken ipeksi pürüzsüz bir deneyim yaşayabilmem için bir Android uygulaması yüklememi istedi.

Bu hayal kırıklığıydı. Vicdanım “Yükle” düğmesine basmama izin vermedi. Tek yapmak istediğim küçük bir pasta ısmarlamak ve yoluma devam etmekti.

Olabildiğince çabuk çıkmak için açılır pencerenin en sağındaki çarpı simgesine tıkladım. Ancak daha sonra kurulum açılır penceresi ekranın alt kısmına oturdu ve alanın dörtte birini kapladı. Ve lapa lapa kullanıcı arayüzü ile aşağı kaydırmak zordu. Bir şekilde Hollanda pastası sipariş etmeyi başardım.

Bu korkunç deneyimden sonra bir sonraki görevim babam için bir gömlek sipariş etmekti. Daha önce olduğu gibi, gömlekler için Google'da arama yapıyorum. İlk bağlantıya tıkladım ve göz açıp kapayıncaya kadar tüm içerik tam önümdeydi. Kaydırma pürüzsüzdü. Kurulum afişi yok. Yerel bir uygulamaya göz atıyormuş gibi hissettim. Korkunç internet bağlantımın kesildiği bir an oldu ama yine de bir dinozor oyunu yerine içeriği görebildim. Bozuk internetim olmasına rağmen babam için bir gömlek ve kot pantolon sipariş etmeyi başardım. En şaşırtıcı olanı, siparişim hakkında bildirimler alıyordum.

Buna ipeksi pürüzsüz bir deneyim derdim. Bu insanlar doğru bir şey yapıyorlardı. Her web sitesi bunu kullanıcıları için yapmalıdır. Buna aşamalı bir web uygulaması denir.

Alex Russell'ın blog yazılarından birinde belirttiği gibi:

"Zaman zaman web'de, pazarlama departmanlarının veya şık ambalajların yararı olmadan güçlü teknolojilerin ortaya çıktığı oluyor. Çevrede oyalanıp büyürler, diğerlerine neredeyse görünmez kalırken küçücük bir grubun eski şapkası haline gelirler. Biri isimlerini verene kadar."

Bazen Aşamalı Bir Web Uygulaması Olarak Bilinen Web'de İpeksi Pürüzsüz Bir Deneyim

Aşamalı web uygulamaları (PWA'lar), daha çok güçlü web uygulamaları yapmak için teknolojilerin bir kombinasyonunu içeren bir metodolojidir. İyileştirilmiş bir kullanıcı deneyimi ile insanlar web sitelerinde daha fazla zaman geçirecek ve daha fazla reklam görecektir. Daha fazla satın alma eğilimindedirler ve bildirim güncellemeleriyle daha sık ziyaret etme olasılıkları daha yüksektir. Financial Times, 2011'de yerel uygulamalarını terk etti ve o sırada mevcut olan en iyi teknolojileri kullanarak bir web uygulaması oluşturdu. Şimdi, ürün tam teşekküllü bir PWA'ya dönüştü.

Ancak, bunca zamandan sonra, yerel bir uygulama işi yeterince iyi yaptığında neden bir web uygulaması oluşturasınız?

Google IO 17'de paylaşılan bazı metriklere göz atalım.

Atlamadan sonra daha fazlası! Aşağıdan okumaya devam edin ↓

Web'e bağlı beş milyar cihaz, web'i bilgi işlem tarihindeki en büyük platform haline getiriyor. Mobil web'de aylık 11,4 milyon tekil ziyaretçi ilk 1000 web mülküne, 4 milyon ise ilk bin uygulamaya gidiyor. Mobil web, yerel uygulamalardan yaklaşık dört kat daha fazla kullanıcı toplar. Ancak iş angajmana gelince bu sayı keskin bir şekilde düşüyor.

Bir kullanıcı, yerel uygulamalarda ortalama 188,6 dakika ve mobil web'de yalnızca 9,3 dakika harcıyor. Yerel uygulamalar, kullanıcılara önemli güncellemeler vermek için anında iletme bildirimleri göndermek için işletim sistemlerinin gücünden yararlanır. Bir tarayıcıdaki web sitelerinden daha iyi bir kullanıcı deneyimi sunar ve daha hızlı açılırlar. Kullanıcıların web tarayıcısına bir URL yazmak yerine ana ekranda bir uygulamanın simgesine dokunması yeterlidir.

Web'deki çoğu ziyaretçinin geri gelme olasılığı düşüktür, bu nedenle geliştiriciler, onları derinlemesine meşgul etmek amacıyla yerel uygulamaları yüklemeleri için onlara banner'lar göstermenin geçici bir çözümünü bulmuşlardır. Ancak daha sonra, kullanıcıların yerel bir uygulamanın ikili dosyasını yüklemenin yorucu prosedüründen geçmeleri gerekecekti. Kullanıcıları bir uygulamayı yüklemeye zorlamak can sıkıcıdır ve ilk etapta uygulamayı yükleme şanslarını daha da azaltır. Web için fırsat açıktır.

Önerilen okuma : Yerli ve PWA: Meydan Okuyanlar Değil, Seçenekler!

Web uygulamaları zengin bir kullanıcı deneyimi, push bildirimleri, çevrimdışı destek ve anında yükleme ile gelirse dünyayı fethedebilirler. Aşamalı bir web uygulamasının yaptığı budur.

Bir PWA, çeşitli güçlü yönleri olduğu için zengin bir kullanıcı deneyimi sunar:

  • Hızlı
    UI lapa lapa değil. Kaydırma pürüzsüz. Ve uygulama, kullanıcı etkileşimine hızla yanıt verir.

  • Güvenilir
    Normal bir web sitesi, kullanıcıları sunucuya gitmekle meşgulken hiçbir şey yapmadan beklemeye zorlar. Bu arada bir PWA, verileri önbellekten anında yükler. PWA, 2G bağlantısında bile sorunsuz çalışır. Bir varlığı veya veri parçasını almak için yapılan her ağ isteği, önce belirli bir istek için yanıtın zaten önbellekte olup olmadığını doğrulayan bir hizmet çalışanı (daha sonra hakkında daha fazla bilgi) üzerinden geçer. Kullanıcılar, zayıf bir bağlantıda bile gerçek içeriği neredeyse anında aldıklarında, uygulamaya daha fazla güvenirler ve uygulamayı daha güvenilir olarak görürler.

  • ilgi çekici
    Bir PWA, kullanıcının ana ekranında bir yer kazanabilir. Tam ekran bir çalışma alanı sağlayarak yerel uygulama benzeri bir deneyim sunar. Kullanıcıları meşgul etmek için push bildirimlerini kullanır.

Artık PWA'ların masaya ne getirdiğini bildiğimize göre, PWA'lara yerel uygulamalara göre üstünlük sağlayan şeyin ayrıntılarına girelim. PWA'lar, hizmet çalışanları, web uygulaması bildirimleri, anında iletme bildirimleri ve önbelleğe alma için IndexedDB/yerel veri yapısı gibi teknolojilerle oluşturulmuştur. Her birine ayrıntılı olarak bakalım.

Servis Çalışanları

Hizmet çalışanı, kullanıcının etkileşimlerine müdahale etmeden arka planda çalışan bir JavaScript dosyasıdır. Sunucuya yapılan tüm GET istekleri bir hizmet görevlisinden geçer. İstemci tarafı proxy gibi davranır. Ağ isteklerini yakalayarak, istemciye geri gönderilen yanıt üzerinde tam kontrol sahibi olur. PWA anında yüklenir, çünkü hizmet çalışanları önbellekten gelen verilerle yanıt vererek ağ bağımlılığını ortadan kaldırır.

Bir hizmet çalışanı, yalnızca kendi kapsamındaki bir ağ isteğine müdahale edebilir. Örneğin, kök kapsamlı bir hizmet çalışanı, bir web sayfasından gelen tüm getirme isteklerini durdurabilir. Servis çalışanı, olaya dayalı bir sistem olarak çalışır. Gerekmediğinde uyku durumuna geçer, böylece hafızayı korur. Bir web uygulamasında bir servis çalışanı kullanmak için, önce JavaScript ile sayfaya kaydettirmemiz gerekir.

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

Önce tarayıcının servis çalışanlarını destekleyip desteklemediğini kontrol ederiz. Bir hizmet çalışanını bir web uygulamasına kaydetmek için, URL'sini navigator.serviceWorker bulunan register işlevine bir parametre olarak sağlarız ( navigator , komut dosyalarının kendilerini kaydetmelerine ve etkinliklerini gerçekleştirmelerine izin veren bir web API'sidir). Bir servis çalışanı yalnızca bir kez kaydedilir. Kayıt her sayfa yüklemesinde gerçekleşmez. Tarayıcı, hizmet çalışanı dosyasını ( ./service-worker.js ) yalnızca mevcut etkinleştirilmiş hizmet çalışanı ile yenisi arasında bir bayt farkı varsa veya URL'si değişmişse indirir.

Yukarıdaki hizmet çalışanı, kökten ( / ) gelen tüm istekleri durduracaktır. Bir servis çalışanının kapsamını sınırlamak için, kapsam olarak anahtarlardan biriyle isteğe bağlı bir parametre geçiririz.

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

Yukarıdaki hizmet çalışanı, URL'de /books olan istekleri durduracaktır. Örneğin, /products ile istekleri engellemez, ancak /books/products ile istekleri çok iyi kesebilir.

Belirtildiği gibi, bir hizmet çalışanı olaya dayalı bir sistem olarak çalışır. Olayları dinler (kurulum, etkinleştirme, getirme, gönderme) ve buna göre ilgili olay işleyicisini çağırır. Bu olaylardan bazıları, etkinleştirilmek için bu olaylardan sırayla geçen bir hizmet çalışanının yaşam döngüsünün bir parçasıdır.

Kurulum

Bir hizmet çalışanı başarıyla kaydedildikten sonra bir yükleme olayı başlatılır. Bu, önbelleği ayarlamak veya IndexedDB'de nesne depoları oluşturmak gibi başlatma işlerini yapmak için iyi bir yerdir. (IndexedDB detaylarına indiğimizde size daha mantıklı gelecektir. Şimdilik sadece bir anahtar/değer çifti yapısı olduğunu söyleyebiliriz.)

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

Burada, bir sonraki yüklemenin anında gerçekleşmesi için bazı dosyaları önbelleğe alıyoruz. self , hizmet çalışanı örneğini ifade eder. event.waitUntil , hizmet çalışanının içindeki tüm kodun yürütülmesini bitirmesini beklemesini sağlar.

aktivasyon

Bir hizmet çalışanı yüklendikten sonra, henüz getirme isteklerini dinleyemez. Bunun yerine, bir activate olayı başlatılır. Web sitesinde aynı kapsamda aktif bir hizmet çalışanı çalışmıyorsa, kurulu hizmet çalışanı hemen etkinleştirilir. Ancak, bir web sitesinde zaten aktif bir hizmet çalışanı varsa, yeni bir hizmet çalışanının etkinleştirilmesi, eski hizmet çalışanı üzerinde çalışan tüm sekmeler kapatılana kadar ertelenir. Bu mantıklıdır çünkü eski hizmet çalışanı şimdi yenisinde değiştirilmiş olan önbellek örneğini kullanıyor olabilir. Bu nedenle, etkinleştirme adımı eski önbelleklerden kurtulmak için iyi bir yerdir.

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

Yukarıdaki kodda eski önbelleği siliyoruz. Bir önbelleğin adı cacheWhitelist ile eşleşmiyorsa silinir. Bekleme aşamasını atlamak ve servis çalışanını hemen etkinleştirmek için skip.waiting() kullanıyoruz.

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

Servis çalışanı etkinleştirildiğinde, getirme isteklerini ve push olaylarını dinleyebilir.

Olay İşleyicisini Getir

Bir web sayfası ağ üzerinden bir kaynak için bir getirme isteği başlattığında, hizmet çalışanından getirme olayı çağrılır. Fetch olay işleyicisi önce önbellekte istenen kaynağı arar. Önbellekte varsa, yanıtı önbelleğe alınmış kaynakla döndürür. Aksi takdirde, sunucuya bir getirme isteği başlatır ve sunucu, yanıtı istenen kaynakla geri gönderdiğinde, sonraki istekler için önbelleğe alır.

 /* 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 , hizmet çalışanının istemciye özelleştirilmiş bir yanıt göndermesini sağlar.

Çevrimdışı-önce artık bir şey. Kritik olmayan herhangi bir istek için, sunucuya gitmek yerine yanıtı önbellekten sunmalıyız. Önbellekte herhangi bir varlık yoksa, onu sunucudan alır ve sonraki istekler için önbelleğe alırız.

Hizmet çalışanları, herhangi bir getirme isteğinin yanıtını değiştirme gücüne sahip oldukları için yalnızca HTTPS web sitelerinde çalışır. Kötü niyetli biri, bir HTTP web sitesindeki bir isteğin yanıtını değiştirebilir. Bu nedenle, HTTPS üzerinde bir PWA barındırmak zorunludur. Hizmet çalışanları, DOM'nin normal işleyişini kesintiye uğratmaz. Doğrudan web sayfasıyla iletişim kuramazlar. Bir web sayfasına herhangi bir mesaj göndermek için, mesaj mesajlarını kullanır.

Web Push Bildirimleri

Diyelim ki cep telefonunuzda bir oyun oynamakla meşgulsünüz ve en sevdiğiniz markada %30 indirim olduğunu bildiren bir bildirim açılır. Lafı daha fazla uzatmadan bildirime tıklayıp nefesinizi dışarı verin. Örneğin bir kriket veya futbol maçıyla ilgili canlı güncellemeler almak veya bildirim olarak önemli e-postalar ve hatırlatıcılar almak, kullanıcıların bir ürünle etkileşime geçmesi açısından büyük önem taşır. Bu özellik, PWA gelene kadar yalnızca yerel uygulamalarda mevcuttu. Bir PWA, yerel uygulamaların kutudan çıktığı gibi sağladığı bu güçlü özellikle rekabet etmek için web push bildirimlerini kullanır. Bir kullanıcı, tarayıcı sekmelerinin hiçbirinde PWA açık olmasa ve tarayıcı açık olmasa bile bir web push bildirimi almaya devam eder.

Bir web uygulaması, kullanıcıdan push bildirimleri göndermek için izin istemelidir.

Web Push bildirimleri için izin istemek için Tarayıcı İstemi
Web Push bildirimleri için izin istemek için Tarayıcı İstemi. (Büyük önizleme)

Kullanıcı "İzin Ver" düğmesini tıklayarak onayladıktan sonra, tarayıcı tarafından benzersiz bir abonelik jetonu oluşturulur. Bu jeton bu cihaz için benzersizdir. Chrome tarafından oluşturulan abonelik jetonunun biçimi aşağıdaki gibidir:

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

Yukarıdaki belirteçte yer alan endpoint , her abonelik için benzersiz olacaktır. Ortalama bir web sitesinde binlerce kullanıcı anında iletme bildirimleri almayı kabul eder ve her biri için bu endpoint benzersiz olur. Böylece, bu endpoint yardımıyla, uygulama bu kullanıcıları gelecekte onlara push bildirimleri göndererek hedefleyebilir. expirationTime , aboneliğin belirli bir cihaz için geçerli olduğu süredir. expirationTime 20 gün ise, bu, kullanıcının push aboneliğinin 20 gün sonra sona ereceği ve kullanıcının eski abonelikle ilgili push bildirimlerini alamayacağı anlamına gelir. Bu durumda tarayıcı, o cihaz için yeni bir abonelik belirteci oluşturacaktır. p256dh auth şifreleme için kullanılır.

Şimdi, gelecekte bu binlerce kullanıcıya anında iletme bildirimleri göndermek için öncelikle ilgili abonelik belirteçlerini kaydetmemiz gerekiyor. Bu kullanıcılara anında iletme bildirimleri göndermek uygulama sunucusunun (arka uç sunucusu, belki bir Node.js betiği) görevidir. Bu, istek yükündeki bildirim verileriyle uç nokta URL'sine bir POST isteği yapmak kadar basit gelebilir. Ancak, bir kullanıcı kendisine yönelik bir push bildirimi sunucu tarafından tetiklendiğinde çevrimiçi değilse, tekrar çevrimiçi olduklarında yine de bu bildirimi alması gerektiği unutulmamalıdır. Sunucu, kullanıcılara binlerce istek göndermenin yanı sıra bu tür senaryolarla ilgilenmek zorunda kalacaktı. Kullanıcının bağlantısını takip eden bir sunucu kulağa karmaşık geliyor. Bu nedenle, web push bildirimlerini sunucudan istemciye yönlendirmekten ortadaki bir şey sorumlu olacaktır. Buna push hizmeti denir ve her tarayıcının kendi push hizmeti uygulaması vardır. Herhangi bir bildirim göndermek için tarayıcının push hizmetine aşağıdaki bilgileri iletmesi gerekir:

  1. yaşama zamanı
    Bu, bir mesajın kullanıcıya teslim edilmemesi durumunda ne kadar süreyle kuyruğa alınması gerektiğidir. Bu süre dolduğunda, mesaj kuyruktan kaldırılacaktır.
  2. mesajın aciliyeti
    Bu, push hizmetinin yalnızca yüksek öncelikli mesajlar göndererek kullanıcının pilini korumasını sağlar.

Push hizmeti, mesajları istemciye yönlendirir. İlgili web uygulaması tarayıcıda açık olmasa bile push'un istemci tarafından alınması gerektiğinden, push olaylarının arka planda sürekli olarak izleyen bir şey tarafından dinlenmesi gerekir. Tahmin ettiniz: Bu, hizmet görevlisinin işidir. Servis çalışanı, push olaylarını dinler ve kullanıcıya bildirim gösterme işini yapar.

Artık tarayıcı, push servisi, servis çalışanı ve uygulama sunucusunun kullanıcıya push bildirimleri göndermek için uyum içinde çalıştığını biliyoruz. Uygulama detaylarına bakalım.

Web Push İstemcisi

Kullanıcıdan izin istemek tek seferlik bir şeydir. Bir kullanıcı push bildirimleri almak için zaten izin vermişse, tekrar sormamalıyız. İzin değeri Notification.permission içine kaydedilir.

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

Yukarıdaki subscribe olma yönteminde, bir abonelik belirteci oluşturmak için userVisibleOnly ve applicationServerKey geçiyoruz. userVisibleOnly özelliği her zaman doğru olmalıdır, çünkü tarayıcıya sunucu tarafından gönderilen herhangi bir anında iletme bildiriminin istemciye gösterileceğini söyler. applicationServerKey amacını anlamak için bir senaryo düşünelim.

Birileri binlerce abonelik jetonunuzu ele geçirirse, bu aboneliklerde bulunan uç noktalara çok iyi bildirim gönderebilirler. Uç noktanın benzersiz kimliğinize bağlanmasının hiçbir yolu yoktur. Web uygulamanızda oluşturulan abonelik belirteçlerine benzersiz bir kimlik sağlamak için VAPID protokolünü kullanıyoruz. VAPID ile uygulama sunucusu, push bildirimleri gönderirken kendini push hizmetine gönüllü olarak tanımlar. Bunun gibi iki anahtar üretiyoruz:

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

web-push bir npm modülüdür. vapidKeys bir genel anahtarı ve bir özel anahtarı olacaktır. Yukarıda kullanılan uygulama sunucusu anahtarı, genel anahtardır.

Web İtme Sunucusu

Web push sunucusunun (uygulama sunucusu) işi basittir. Abonelik belirteçlerine bir bildirim yükü gönderir.

 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)

Web push kitaplığındaki sendNotification yöntemini kullanır.

Servis Çalışanları

Servis çalışanı, bildirimi kullanıcıya şu şekilde gösterir:

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

Şimdiye kadar, bir hizmet çalışanının istekleri depolamak için önbelleği nasıl kullandığını ve bir PWA'yı nasıl hızlı ve güvenilir hale getirdiğini ve web push bildirimlerinin kullanıcıları nasıl etkileşimde tuttuğunu gördük.

Çevrimdışı destek için istemci tarafında bir sürü veri depolamak için dev bir veri yapısına ihtiyacımız var. Financial Times PWA'ya bakalım. Bu veri yapısının gücüne kendiniz tanık olmalısınız. URL'yi tarayıcınıza yükleyin ve ardından internet bağlantınızı kapatın. Sayfayı yenile. Ah! Hala çalışıyor mu? Bu. (Dediğim gibi, çevrimdışı yeni siyahtır.) Veriler kablolardan gelmiyor. Evden servis ediliyor. Chrome Geliştirici Araçları'nın "Uygulamalar" sekmesine gidin. “Depolama” altında “IndexedDB”yi bulacaksınız.

IndexedDB, makale verilerini Financial Times PWA'da saklar
Financial Times PWA'da IndexedDB. (Büyük önizleme)

"Makaleler" nesne deposuna göz atın ve sihri kendiniz görmek için öğelerden herhangi birini genişletin. Financial Times, bu verileri çevrimdışı destek için sakladı. Muazzam miktarda veri depolamamızı sağlayan bu veri yapısına IndexedDB adı verilir. IndexedDB, yapılandırılmış verileri depolamak için JavaScript tabanlı nesne yönelimli bir veritabanıdır. Bu veritabanında çeşitli amaçlar için farklı nesne depoları oluşturabiliriz. Örneğin yukarıdaki resimde gördüğümüz gibi “Kaynaklar”, “ArticleImages” ve “Makaleler” nesne depoları olarak adlandırılmaktadır. Bir nesne deposundaki her kayıt, benzersiz bir şekilde bir anahtarla tanımlanır. IndexedDB, dosyaları ve blobları depolamak için bile kullanılabilir.

Kitapları depolamak için bir veritabanı oluşturarak IndexedDB'yi anlamaya çalışalım.

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

booksdb veritabanı zaten mevcut değilse, yukarıdaki kod bir booksdb veritabanı oluşturacaktır. Açık yöntemin ikinci parametresi veritabanının sürümüdür. Bir sürümün belirtilmesi, gelecekte meydana gelebilecek şemayla ilgili değişikliklerle ilgilenir. Örneğin, booksdb artık sadece bir tablosu var ama uygulama büyüdüğünde ona iki tablo daha eklemeyi düşünüyoruz. Veritabanımızın güncellenmiş şema ile senkronize olduğundan emin olmak için öncekinden daha yüksek bir sürüm belirleyeceğiz.

open yöntemini çağırmak, veritabanını hemen açmaz. Bir IDBOpenDBRequest nesnesi döndüren zaman uyumsuz bir istektir. Bu nesnenin başarı ve hata özellikleri vardır; bağlantımızın durumunu yönetmek için bu özelliklere uygun işleyiciler yazmamız gerekecek.

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

Nesne depolarının oluşturulmasını veya değiştirilmesini yönetmek için (nesne depoları SQL tabanlı tablolara benzerdir - bir anahtar/değer yapısına sahiptirler), openIdbRequest nesnesinde onupgradeneeded yöntemi çağrılır. Sürüm değiştiğinde onupgradeneeded yöntemi çağrılır. Yukarıdaki kod parçacığında, kimlik olarak benzersiz anahtarı olan bir kitap nesne deposu oluşturuyoruz.

Diyelim ki, bu kod parçasını dağıttıktan sonra, users adlı bir nesne deposu daha oluşturmamız gerekiyor. Yani şimdi veritabanımızın versiyonu 2 olacak.

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

Açık isteğin başarı olay işleyicisinde dbInstance önbelleğe aldık. IndexedDB'de veri almak veya eklemek için dbInstance . Kitaplar nesne mağazamıza bazı kitap kayıtları ekleyelim.

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

Özellikle nesne mağazalarında kayıt yazarken transactions yararlanıyoruz. Bir işlem, veri bütünlüğünü sağlamak için bir işlemin etrafındaki bir sarmalayıcıdır. Bir işlemdeki işlemlerden herhangi biri başarısız olursa, veritabanı üzerinde herhangi bir işlem yapılmaz.

put yöntemiyle bir kitap kaydını değiştirelim:

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

Get yöntemiyle bir kitap kaydı 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.') }

Ana Ekrana Simge Ekleme

Artık bir PWA ile yerel bir uygulama arasında neredeyse hiç ayrım olmadığı için, PWA'ya birinci sınıf bir konum önermek mantıklıdır. Web siteniz bir PWA'nın temel kriterlerini karşılıyorsa (HTTPS'de barındırılıyor, hizmet çalışanları ile entegre oluyor ve bir manifest.json sahipse) ve kullanıcı web sayfasında biraz zaman geçirdikten sonra, tarayıcı en altta bir istem çağırarak şunu sorar: kullanıcının uygulamayı aşağıda gösterildiği gibi ana ekranına eklemesi için:

Financial Times PWA'yı ana ekrana ekleme istemi
Ana ekranda Financial Times PWA'nın eklenmesini isteyin. (Büyük önizleme)

Bir kullanıcı "Ana ekrana FT ekle"yi tıkladığında, PWA hem ana ekrana hem de uygulama çekmecesine adım atar. Bir kullanıcı telefonunda herhangi bir uygulama aradığında, arama sorgusuyla eşleşen tüm PWA'lar listelenir. Ayrıca, kullanıcıların bunları yönetmesini kolaylaştıran sistem ayarlarında da görüleceklerdir. Bu anlamda, bir PWA yerel bir uygulama gibi davranır.

PWA'lar bu özelliği sağlamak için manifest.json kullanır. Basit bir manifest.json dosyasına bakalım.

 { "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 , kullanıcının ana ekranında ve sistem ayarlarında görünür. name , krom isteminde ve açılış ekranında görünür. Açılış ekranı, uygulama başlatmaya hazırlanırken kullanıcının gördüğü ekrandır. start_url , uygulamanızın ana ekranıdır. Kullanıcıların ana ekranda bir simgeye dokunduklarında aldıkları şey budur. Giriş ekranında background_color kullanılır. theme_color , araç çubuğunun rengini ayarlar. display modu için standalone değer, uygulamanın tam ekran modunda (tarayıcının araç çubuğunu gizleyerek) çalıştırılacağını belirtir. Bir kullanıcı bir PWA kurduğunda, boyutu yerel uygulamaların megabaytları yerine yalnızca kilobayt cinsindendir.

Hizmet çalışanları, web push bildirimleri, IndexedDB ve ana ekran konumu, çevrimdışı destek, güvenilirlik ve etkileşimi oluşturur. Unutulmamalıdır ki bir servis çalışanı canlanıp ilk yükte işini yapmaya başlamaz. İlk yükleme, tüm statik varlıklar ve diğer kaynaklar önbelleğe alınana kadar yavaş olacaktır. İlk yükü optimize etmek için bazı stratejiler uygulayabiliriz.

Varlıkları Paketleme

HTML, stil sayfaları, resimler ve JavaScript dahil tüm kaynaklar sunucudan alınacaktır. Daha fazla dosya, onları getirmek için daha fazla HTTPS isteği gerekir. Statik varlıklarımızı paketlemek için WebPack gibi paketleyicileri kullanabiliriz, böylece sunucuya yapılan HTTP isteklerinin sayısını azaltabiliriz. WebPack, kod bölme (yani, hepsini bir araya toplamak yerine yalnızca geçerli sayfa yüklemesi için gerekli olan dosyaları bir araya getirmek) ve ağaç sallamak (yani, yinelenen bağımlılıkları kaldırmak veya içe aktarılan ancak kodda kullanılmayan bağımlılıklar).

Gidiş-Dönüşlerin Azaltılması

Web'deki yavaşlığın ana nedenlerinden biri ağ gecikmesidir. Bir baytın A'dan B'ye seyahat etmesi için geçen süre ağ bağlantısına göre değişir. Örneğin, Wi-Fi üzerinden belirli bir gidiş-dönüş, 3G bağlantısında 50 milisaniye ve 500 milisaniye, 2G bağlantısında ise 2500 milisaniye sürer. Bu istekler HTTP protokolü kullanılarak gönderilir; bu, bir istek için belirli bir bağlantı kullanılırken, önceki isteğin yanıtı sunulana kadar başka istekler için kullanılamayacağı anlamına gelir. Bir web sitesi aynı anda altı asenkron HTTP isteği yapabilir, çünkü bir web sitesinde HTTP istekleri yapmak için altı bağlantı mevcuttur. Ortalama bir web sitesi yaklaşık 100 istekte bulunur; bu nedenle, maksimum altı bağlantı mevcut olduğunda, bir kullanıcı tek bir gidiş-dönüş yolculuğunda yaklaşık 833 milisaniye harcayabilir. (Hesaplama 833 milisaniye - 1006 = 1666. Bir gidiş-dönüşte harcanan zamanı hesapladığımız için 1666'yı 2'ye bölmemiz gerekiyor.) HTTP2 yerindeyken, geri dönüş süresi büyük ölçüde azalır. HTTP2 bağlantı kafasını engellemez, bu nedenle aynı anda birden fazla istek gönderilebilir.

Çoğu HTTP yanıtı, last-modified ve etag başlıklarını içerir. last-modified başlık, dosyanın en son değiştirildiği tarihtir ve etag , dosyanın içeriğine dayalı benzersiz bir değerdir. Yalnızca bir dosyanın içeriği değiştirildiğinde değiştirilecektir. Önbelleğe alınmış bir sürüm zaten yerel olarak mevcutsa, bu üstbilgilerin her ikisi de dosyanın yeniden indirilmesini önlemek için kullanılabilir. Tarayıcıda bu dosyanın yerel olarak kullanılabilen bir sürümü varsa, bu iki başlıktan herhangi birini isteğe şu şekilde ekleyebilir:

Geçerli önbelleğe alınmış varlıkların indirilmesini önlemek için ETag ve Son Değiştirilen Başlıklar ekleyin
ETag ve Son Değiştirilen Başlıklar. (Büyük önizleme)

Sunucu, dosyanın içeriğinin değişip değişmediğini kontrol edebilir. 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. (Büyük önizleme)

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

Çözüm

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!