Како претворити ВебАпп у ПВА помоћу Пусх обавештења

Ovaj tekst objašnjava kako transformisati veb aplikaciju ili sajt u Progresivnu Veb Aplikaciju (PWA), koristeći push notifikacije uz pomoć Firebase Cloud Messaging.

U današnje vreme, mnoge veb aplikacije se konvertuju u PWA jer nude funkcije poput rada van mreže, push notifikacija i sinhronizacije u pozadini. Ove opcije čine veb aplikacije sličnijim izvornim aplikacijama, poboljšavajući korisničko iskustvo.

Velike kompanije, poput Twittera i Amazona, pretvorile su svoje veb aplikacije u PWA kako bi povećale angažman korisnika.

Šta je PWA?

PWA = (veb aplikacija) + (neke izvorne funkcije aplikacije)

PWA je vaša standardna veb aplikacija (HTML+CSS+JS), koja se ponaša isto kao i na svim pregledačima. Međutim, kada se veb sajt učita u modernom pregledaču, PWA može imati izvorne funkcije. Ovo vašu aplikaciju čini moćnijom i skalabilnijom, jer je moguće unapred preuzeti i keširati resurse na frontendu, smanjujući opterećenje backend servera.

Po čemu se PWA razlikuje od veb aplikacije?

  • Mogućnost instalacije: Veb aplikacija se može instalirati kao izvorna aplikacija.
  • Progresivnost: Radi kao standardna veb aplikacija, ali sa dodatnim izvornim funkcijama.
  • Izvorno iskustvo: Jednom instalirana, korisnik se može kretati po aplikaciji kao da je izvorna.
  • Lak pristup: Korisnici ne moraju svaki put unositi veb adresu. Aplikaciju je moguće otvoriti jednim klikom nakon instalacije.
  • Keširanje aplikacije: Pre PWA, keširanje se vršilo isključivo putem HTTP keša pregledača. Sa PWA, keširanje se može implementirati direktno u kôdu na strani klijenta.
  • Objavljivanje u prodavnicama: PWA se može objaviti u Google Play Store i iOS App Store.

Transformacijom u PWA, aplikacija postaje znatno moćnija.

Zašto bi preduzeća trebala razmatrati PWA?

Često klijenti prvo traže razvoj veb aplikacije, a zatim Android i iOS verzije. To znači dupliranje funkcionalnosti, što podiže troškove razvoja i produžava vreme izlaska na tržište.

Međutim, neki klijenti imaju ograničen budžet ili im je prioritet brz izlazak na tržište.

PWA može zadovoljiti većinu zahteva klijenata. Možemo im predložiti PWA, sa mogućnošću konverzije u Android aplikaciju preko Trusted Web Activity (TWA) ako žele da je objave na Play Store.

Ako su zahteve zaista potrebne izvorne funkcije koje PWA ne podržava, klijenti mogu razvijati obe verzije aplikacije. U međuvremenu, PWA se može objaviti na Play Store dok se ne završi razvoj izvorne Android aplikacije.

Primer: Titan Eyeplus

Prvo su razvili PWA aplikaciju i objavili je na Play Store pomoću TWA. Kada je razvoj Android aplikacije završen, objavili su pravu Android aplikaciju. Tako su skratili vreme izlaska na tržište i uštedeli novac.

PWA karakteristike

PWA daje veb aplikacijama izvorne funkcije.

Glavne karakteristike su:

  • Instalacija: Veb aplikacija se instalira kao izvorna aplikacija.
  • Keširanje: Moguće je keširanje aplikacije, što omogućava rad van mreže.
  • Push notifikacije: Moguće je slanje push notifikacija sa servera radi angažovanja korisnika.
  • Geofencing: Aplikacija može biti obaveštena kad god se lokacija uređaja promeni.
  • Zahtev za plaćanje: Omogućava plaćanje unutar aplikacije sa izvornim korisničkim iskustvom.

I mnogo više funkcija koje dolaze u budućnosti.

Dodatne karakteristike su:

  • Prečice: Brze URL adrese dodate u manifest fajl.
  • Web Share API: Omogućava aplikaciji da prima podatke od drugih aplikacija.
  • Badging API: Prikazuje broj obaveštenja u instaliranoj PWA.
  • Periodic Background Sync API: Čuva podatke korisnika dok se ne povežu na mrežu.
  • Contact Picker: Omogućava biranje kontakata sa mobilnog telefona.
  • File Picker: Pristup datotekama na lokalnom sistemu/mobilnom uređaju.

Prednosti PWA u odnosu na izvornu aplikaciju

Izvorna aplikacija obično radi bolje i ima više funkcija od PWA. Ipak, PWA ima određene prednosti:

  • PWA radi na više platformi, uključujući Android, iOS i desktop.
  • Smanjuje troškove razvoja.
  • Jednostavna implementacija funkcija u poređenju sa izvornom aplikacijom.
  • Lako se pronalazi jer je PWA (veb sajt) SEO optimizovan.
  • Bezbedna je jer radi samo preko HTTPS.

Nedostaci PWA u odnosu na izvornu aplikaciju

  • Ograničena funkcionalnost u poređenju sa izvornom aplikacijom.
  • Nije zagarantovana puna podrška PWA funkcija na svim uređajima.
  • Slabiji brending jer PWA nije dostupna direktno u prodavnicama aplikacija.

PWA možete objaviti kao Android aplikaciju na Play Store pomoću Trusted Web Activity (TWA), što pomaže brendiranju.

Potrebni elementi za konverziju veb aplikacije u PWA

Za konverziju veb aplikacije ili sajta u PWA:

  • Service Worker: Osnova svake PWA za keširanje, push notifikacije i proxy za zahteve.
  • Manifest fajl: Sadrži detalje o aplikaciji koji se koriste za instalaciju i prikazivanje na početnom ekranu.
  • Logo aplikacije: Slika visoke rezolucije (512×512 piksela) za ikonu aplikacije. Logo je potreban za prikaz na početnom ekranu, splash screen-u, itd. Potrebno je kreirati više slika u formatu 1:1.
  • Responzivni dizajn: Veb aplikacija mora biti responzivna na različitim veličinama ekrana.

Šta je Service Worker?

Service worker (skripta na strani klijenta) deluje kao proxy između veb aplikacije i spoljnjeg sveta, omogućavajući push notifikacije i keširanje.

Service worker radi nezavisno od glavnog JavaScript koda i nema pristup DOM API-ju. Može pristupiti samo IndexedDB API, Fetch API i Cache Storage API, ali može komunicirati sa glavnom niti putem poruka.

Usluge koje pruža service worker:

  • Presretanje HTTP zahteva sa vašeg domena.
  • Primanje push notifikacija sa servera.
  • Offline pristup aplikaciji.

Service worker kontroliše aplikaciju i može manipulisati zahtevima, ali radi nezavisno. Iz tog razloga, domen mora biti omogućen preko HTTPS-a da bi se sprečio man-in-the-middle napad.

Šta je manifest fajl?

Manifest fajl (manifest.json) sadrži detalje o PWA aplikaciji koje su potrebne pregledaču.

  • name: Ime aplikacije.
  • short_name: Kratko ime aplikacije, opcionalno. Ako su definisani i name i short_name, pregledač će koristiti short_name.
  • description: Opis aplikacije.
  • start_url: Početna stranica aplikacije kada se pokrene.
  • icons: Skup slika za prikaz PWA na početnom ekranu, itd.
  • background_color: Boja pozadine splash screen-a.
  • display: Prilagođavanje korisničkog interfejsa pregledača u PWA.
  • theme_color: Boja teme PWA.
  • scope: URL opseg aplikacije. Podrazumevano je lokacija manifest fajla.
  • shortcuts: Brze veze za PWA aplikaciju.

Konverzija veb aplikacije u PWA

Za demonstraciju, kreirana je struktura direktorijuma sa statičkim datotekama:

  • index.html – početna stranica
  • clanci/
    • index.html – stranica sa člancima
  • autori/
    • index.html – stranica autora
  • alati/
    • index.html – stranica sa alatima
  • ponude/
    • index.html – stranica sa ponudama

Ako već imate veb sajt ili aplikaciju, probajte da je konvertujete u PWA prateći sledeće korake.

Kreiranje potrebnih slika za PWA

Prvo, uzmite logo i isecite ga u razmeri 1:1 u 5 različitih veličina. Korišćen je https://tools.crawlink.com/tools/pwa-icon-generator/ za brzo dobijanje slika različitih veličina. Možete koristiti isti alat.

Kreiranje manifest fajla

Drugo, kreirajte manifest.json fajl sa detaljima o veb aplikaciji. Primer manifest fajla za sajt je prikazan ispod:

{
	"name": "techblog.co.rs",
	"short_name": "techblog.co.rs",
	"description": "techblog.co.rs proizvodi kvalitetne tehnološke i finansijske članke, alate i API-je za pomoć preduzećima i pojedincima u razvoju.",
	"start_url": "/",
	"icons": [{
		"src": "assets/icon/icon-128x128.png",
		"sizes": "128x128",
		"type": "image/png"
	}, {
		"src": "assets/icon/icon-152x152.png",
		"sizes": "152x152",
		"type": "image/png"
	}, {
		"src": "assets/icon/icon-192x192.png",
		"sizes": "192x192",
		"type": "image/png"
	}, {
		"src": "assets/icon/icon-384x384.png",
		"sizes": "384x384",
		"type": "image/png"
	}, {
		"src": "assets/icon/icon-512x512.png",
		"sizes": "512x512",
		"type": "image/png"
	}],
	"background_color": "#EDF2F4",
	"display": "standalone",
	"theme_color": "#B20422",
	"scope": "/",
	"shortcuts": [{
			"name": "Članci",
			"short_name": "Članci",
			"description": "1595 članaka o bezbednosti, sistemskoj administraciji, digitalnom marketingu, računarstvu u oblaku, razvoju i mnogim drugim temama.",
			"url": "https://geekflare.com/articles",
			"icons": [{
				"src": "/assets/icon/icon-152x152.png",
				"sizes": "152x152"
			}]
		},
		{
			"name": "Autori",
			"short_name": "Autori",
			"description": "techblog.co.rs - Autori",
			"url": "/authors",
			"icons": [{
				"src": "/assets/icon/icon-152x152.png",
				"sizes": "152x152"
			}]
		},
		{
			"name": "Alati",
			"short_name": "Alati",
			"description": "techblog.co.rs - Alati",
			"url": "https://techblog.co.rs.com/tools",
			"icons": [{
				"src": "/assets/icon/icon-152x152.png",
				"sizes": "152x152"
			}]
		},
		{
			"name": "Ponude",
			"short_name": "Ponude",
			"description": "techblog.co.rs - Ponude",
			"url": "/deals",
			"icons": [{
				"src": "/assets/icon/icon-152x152.png",
				"sizes": "152x152"
			}]
		}
	]
}

Registracija Service Worker-a

Kreirajte fajlove `register-service-worker.js` i `service-worker.js` u osnovnom direktorijumu.

`register-service-worker.js` je JavaScript datoteka koja se izvršava u glavnoj niti i ima pristup DOM API-ju. `service-worker.js` je skripta koja se izvršava nezavisno od glavne niti i ima kratak životni vek. Pokreće se kad god događaji pozovu service worker i radi dok se ne završi proces.

U glavnom JavaScript fajlu možete proveriti da li je service worker registrovan i, ako nije, registrovati `service-worker.js`.

Dodajte sledeći kod u `register-service-worker.js`:

if ('serviceWorker' in navigator) {
    window.addEventListener('load', function() {
        navigator.serviceWorker.register('/service-worker.js');
    });
}

Dodajte sledeći kod u `service-worker.js`:

self.addEventListener('install', (event) => { // event when service worker install
    console.log( 'install', event);
    self.skipWaiting();
});

self.addEventListener('activate', (event) => { // event when service worker activated
    console.log('activate', event);
    return self.clients.claim();
});

self.addEventListener('fetch', function(event) { // HTTP request interceptor
    event.respondWith(fetch(event.request)); // send all http request without any cache logic
    /*event.respondWith(
        caches.match(event.request).then(function(response) {
            return response || fetch(event. request);
        })
    );*/ // cache new request. if already in cache serves with the cache.
});

Za sada se ne bavimo keširanjem za offline podršku, već samo konverzijom veb aplikacije u PWA.

Dodajte manifest fajl i skriptu u <head> sekciju HTML stranice.

<link rel="manifest" href="https://techblog.co.rs.com/manifest.json">
<script src="/register-service-worker.js"></script>

Nakon dodavanja, osvežite stranicu. Sada možete instalirati aplikaciju kao što je prikazano na mobilnom Chrome-u.

Aplikacija se dodaje na početni ekran.

Ako koristite WordPress, isprobajte neki od postojećih PWA pluginova. Za VueJS ili ReactJS, možete pratiti gornji metod ili koristiti postojeće PWA npm module za ubrzanje razvoja. PWA npm moduli su već konfigurirani sa offline keširanjem, itd.

Omogućavanje push notifikacija

Veb push notifikacije se šalju pregledaču kako bi korisnici bili više angažovani sa aplikacijom. Mogu se omogućiti korišćenjem:

  • Notification API: Za konfigurisanje prikaza push notifikacija korisniku.
  • Push API: Za prijem poruka sa notifikacijama koje se šalju sa servera.

Prvi korak je provera podrške za Notification API i traženje dozvole od korisnika za prikaz notifikacija. Kopirajte sledeći kod u `register-service-worker.js`:

if ('Notification' in window && Notification.permission != 'granted') {
    console.log('Traženje dozvole od korisnika')
    Notification.requestPermission(status => {  
        console.log('Status:'+status)
        displayNotification('Notifikacije omogućene');
    });
}


const displayNotification = notificationTitle => {
    console.log('Prikazivanje notifikacije')
    if (Notification.permission == 'granted') {
        navigator.serviceWorker.getRegistration().then(reg => {
            console.log(reg)
            const options = {
                    body: 'Hvala što ste omogućili push notifikacije!',
                    icon:  '/assets/icons/icon-512x512.png',
                    vibrate: [100, 50, 100],
                    data: {
                      dateOfArrival: Date.now(),
                      primaryKey: 0
                    }
                  };
    
            reg.showNotification(notificationTitle, options);
        });
    }
};

Ako je sve prošlo kako treba, dobićete notifikaciju iz aplikacije.

Provera `window` objekta će potvrditi da li pregledač podržava Notification API. Vrednost `Notification.permission` će ukazati da li je korisnik dozvolio prikazivanje notifikacija. Ako je korisnik dozvolio, vrednost će biti „granted“, u suprotnom će biti „denied“.

Omogućavanje Firebase Cloud Messaging i kreiranje pretplate

Za slanje notifikacija sa servera potrebna je jedinstvena krajnja tačka/pretplata za svakog korisnika. Za to se koristi Firebase Cloud Messaging.

Prvo kreirajte Firebase nalog posetom https://firebase.google.com/.

  1. Kreirajte novi projekat i pritisnite „Nastavi“.
  2. U sledećem koraku, Google analitika je podrazumevano omogućena. Možete je isključiti i pritisnuti „Nastavi“. Možete je uključiti kasnije u Firebase konzoli ako je potrebno.
  3. Kada je projekat kreiran, izgledaće ovako:

Zatim idite na podešavanja projekta, kliknite na razmenu poruka u oblaku i generišite ključeve.

Iz gornjih koraka dobićete 3 ključa:

  • Projektni server ključ
  • Privatni ključ veb push sertifikata
  • Veb push sertifikati sa javnim ključem

Sada dodajte sledeći kod u `register-service-worker.js`:

const updateSubscriptionOnYourServer = subscription => {
    console.log('Napišite svoj AJAX kod ovde da biste sačuvali korisničku pretplatu u bazi podataka', subscription);
    // write your own ajax request method using fetch, jquery, axios to save the subscription in your server for later use.
};

const subscribeUser = async () => {
    const swRegistration = await navigator.serviceWorker.getRegistration();
    const applicationServerPublicKey = 'BOcTIipY07N4Y63Y-9r7NMoJHofmCzn3Pu9g-LMsgIMGH4HVr42_LW9ia0lMr68TsTLKS3UcdkE3IcC52hJDYsY'; // paste your webpush certificate public key
    const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
    swRegistration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey
    })
    .then((subscription) => {
        console.log('Korisnik se novopretplatio:', subscription);
        updateSubscriptionOnServer(subscription);
    })
    .catch((err) => {
        if (Notification.permission === 'denied') {
          console.warn('Dozvola za notifikacije je odbijena')
        } else {
          console.error('Neuspešna pretplata korisnika: ', err)
        }
    });
};
const urlB64ToUint8Array = (base64String) => {
    const padding = '='.repeat((4 - base64String.length % 4) % 4)
    const base64 = (base64String + padding)
        .replace(/-/g, '+')
        .replace(/_/g, '/')

    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);

    for (let i = 0; i < rawData.length; ++i) {
        outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
};

const checkSubscription = async () => {
    const swRegistration = await navigator.serviceWorker.getRegistration();
    swRegistration.pushManager.getSubscription()
    .then(subscription => {
        if (!!subscription) {
            console.log('Korisnik je već pretplaćen.');
            updateSubscriptionOnYourServer(subscription);
        } else {
            console.log('Korisnik NIJE pretplaćen. Nova pretplata korisnika');
            subscribeUser();
        }
    });
};

checkSubscription();

Dodajte sledeći kod u `service-worker.js`:

self.addEventListener('push', (event) => {
  const json = JSON.parse(event.data.text())
  console.log('Push Data', event.data.text())
  self.registration.showNotification(json.header, json.options)
});

Sada je sve podešeno na frontendu. Pomoću pretplate možete slati push notifikacije korisnicima kad god želite, dok god ne odbiju push usluge.

Slanje notifikacija iz Node.js backend-a

Možete koristiti web-push npm modul za lakšu implementaciju.

Primer koda za slanje push notifikacija sa Node.js servera:

const webPush = require('web-push');
    // pushSubscription is nothing but subscription that you sent from your front-end to save it in DB
    const pushSubscription = {"endpoint":"https://updates.push.services.mozilla.com/wpush/v2/gAAAAABh2…E0mTFsHtUqaye8UCoLBq8sHCgo2IC7UaafhjGmVCG_SCdhZ9Z88uGj-uwMcg","keys":{"auth":"qX6AMD5JWbu41cFWE3Lk8w","p256dh":"BLxHw0IMtBMzOHnXgPxxMgSYXxwzJPxpgR8KmAbMMe1-eOudcIcUTVw0QvrC5gWOhZs-yzDa4yKooqSnM3rnx7Y"}};
    //your web certificates public-key
    const vapidPublicKey = 'BOcTIipY07N4Y63Y-9r7NMoJHofmCzn3Pu9g-LMsgIMGH4HVr42_LW9ia0lMr68TsTLKS3UcdkE3IcC52hJDYsY';
    //your web certificates private-key
    const vapidPrivateKey = 'web-certificate private key';

    var payload = JSON.stringify({
      "options": {
        "body": "Testiranje PWA push notifikacije sa backend-a",
        "badge": "/assets/icon/icon-152x152.png",
        "icon": "/assets/icon/icon-152x152.png",
        "vibrate": [100, 50, 100],
        "data": {
          "id": "458",
        },
        "actions": [{
          "action": "view",
          "title": "Prikaži"
        }, {
          "action": "close",
          "title": "Zatvori"
        }]
      },
      "header": "Notifikacija sa techblog.co.rs-PWA Demo"
    });

    var options = {
      vapidDetails: {
        subject: 'mailto:[email protected]',
        publicKey: vapidPublicKey,
        privateKey: vapidPrivateKey
      },
      TTL: 60
    };

    webPush.sendNotification(
      pushSubscription,
      payload,
      options
    ).then(data => {
      return res.json({status : true, message : 'Notifikacija poslata'});
    }).catch(err => {
      return res.json({status : false, message : err });
    });

Gornji kod će poslati push notifikaciju pretplati i pokrenuti `push` događaj u service worker-u.

Slanje notifikacija iz PHP backend-a

Za PHP pozadinu možete koristiti web-push-php Composer paket. Primer koda za slanje push notifikacija:

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

require __DIR__.'/../vendor/autoload.php';
use MinishlinkWebPushWebPush;
use MinishlinkWebPushSubscription;

// subscription stored in DB
$subsrciptionJson = '{"endpoint":"https://updates.push.services.mozilla.com/wpush/v2/gAAAAABh2…E0mTFsHtUqaye8UCoLBq8sHCgo2IC7UaafhjGmVCG_SCdhZ9Z88uGj-uwMcg","keys":{"auth":"qX6AMD5JWbu41cFWE3Lk8w","p256dh":"BLxHw0IMtBMzOHnXgPxxMgSYXxwzJPxpgR8KmAbMMe1-eOudcIcUTVw0QvrC5gWOhZs-yzDa4yKooqSnM3rnx7Y"}}';
$payloadData = array (
'options' =>  array (
                'body' => 'Testiranje PWA push notifikacije sa backend-a',
                'badge' => '/assets/icon/icon-152x152.png',
                'icon' => '/assets/icon/icon-152x152.png',
                'vibrate' => 
                array (
                  0 => 100,
                  1 => 50,
                  2 => 100,
                ),
                'data' => 
                array (
                  'id' => '458',
                ),
                'actions' => 
                array (
                  0 => 
                  array (
                    'action' => 'view',
                    'title' => 'Prikaži',
                  ),
                  1 => 
                  array (
                    'action' => 'close',
                    'title' => 'Zatvori',
                  ),
                ),
),
'header' => 'Notifikacija sa techblog.co.rs-PWA Demo',
);

// auth
$auth = [
    'GCM' => 'your project private-key', // deprecated and optional, it's here only for compatibility reasons
    'VAPID' => [
        'subject' => 'mailto:[email protected]', // can be a mailto: or your website address
        'publicKey' => 'BOcTIipY07N4Y63Y-9r7NMoJHofmCzn3Pu9g-LMsgIMGH4HVr42_LW9ia0lMr68TsTLKS3UcdkE3IcC52hJDYsY', // (recommended) uncompressed public key P-2