Sonrasında Zip dosyasını bilgisayara indirip proje klasörüne açmamız gerekiyor. Manifest.json dosyası ile birlikte images isimli bir klasör de gelecektir. Images klasöründe kendi eklediğimiz active.png dosyasının farklı cihazlar için standartlaştırılmış boyutları yer alır. Bu bilgiler manifest.json dosyasına da konur.
İşimiz henüz bitmedi! Uygulama için Push Notification özelliğini de etkinleştirmek gerekiyor. Bunun için
Firebase Console arabirimine gidip yeni bir proje oluşturmalı ve ardından proje ayarlarına
(Project Overview -> Project Settings) ulaşıp Cloud Messaging sekmesine gelinmeli
(Ben "basketin-cepte-project'" isimli bir proje oluşturdum :P Hayaller başka tabii ama eldeki malzeme şimdilik bu)
Bu bölümde proje için oluşturulan Server Key ve Sender ID değerleri yer alır. Az önce bahsedildiği gibi Sender ID değerinin manifest.json dosyasına eklenmesi gerekiyor (gcm_sender_id yazan kısma bakınız)
PWA Kodları
Artık basketkolik klasöründeki dosyalarımızı kodlamaya başlayabiliriz. index.html sayfası aslında haber kaynağına aboneliği başlatıp durdurabileceğimiz bir test sahası gibi. Tek dikkat edilmesi gereken nokta manifest dosyası ile bağ kurulmuş olması. Tasarım son derece aptalca yapıldı ama esas amacımız elbette şukela bir görsellik sunmanın dışında push notification kabiliyletlerini deneyimlemek. Index sayfası ile işe başlayalım.
<!DOCTYPE html>
<html>
<head>
<title>Basketin Cepte</title>
<link rel="manifest" href="manifest.json">
<!--Bunu eklemeyi unutmamak lazım-->
</head>
<body>
<h3>Euroleague Haberleri</h3>
<p><i>Düğmeye bas ve ne olacağına bakalım</i></p>
<div id="divPush">
<img id="buttonPush" width="50px" src="active.png" />
</div>
<script src="main.js"></script>
</body>
</html>
efes_barca.html isim dosya da şimdilik bildirim alındığında gösterilecek içeriği barındırıyor. Bunun dinamik olduğunu bir düşünsenize. Abone olduktan sonra yeni bir haber geldiğinde efes_barcha benzeri dinamik HTML içerikleri kullanılacak. Tüm uygulamayı tamamladıktan sonra bu tip bir özellikle örneğinizi daha da zenginleştirebilirsiniz.
<!DOCTYPE html>
<html>
<head>
<title>Euroleague'de Efes Farkı</title>
</head>
<body>
<p>Anadolu Efes, kritik maçta Barcelona'yı kendi evinde farklı yenerek...
</p>
</body>
</html>
Sworker modülü aslında Service Worker görevini üstlenmekte. Kodlar arasına katmaya çalıştığım yorumlarla mümkün mertebe neler olduğunu anlamaya ve anlatmaya çalıştım.
/*
main.js içerisinde Service Worker olarak bildirimi yapılan dosyamız.
İki olay metodu içerir.
Uygulama herhangibir bildirim aldığında push metodu tetiklenir.
Bildirim almasını sağlamak için yazdığımız PusherAPI servisine talep göndermemiz yeterlidir. http://localhost:8080/news/push gibi.
Bu metod içerisinde showNotification ile bir bildirim penceresi gösterilir.
Pencereye tıklandığındaysa notificationclick isimli olay tetiklenir.
Bu olayın ele alındığı fonksiyonda ise temsilen bir web sayfası içeriği açtırılır.
*/
self.addEventListener('push', function (event) {
console.log('push olayı tetiklendi');
var title = 'Euroleague Haberleri';
var body = {
'body': 'Güncel skor bilgileri için tıklayın',
'tag': 'pwa',
'icon': './images/48x48.png'
};
event.waitUntil(
self.registration.showNotification(title, body)
);
});
self.addEventListener('notificationclick', function (event) {
//TODO: Burada statik bir sayfa içeriği gösterilmesi yerine haber bilgisini servisten alacak kodu ekleyebilirsiniz
console.log('Şimdi bildirime ait sayfa açılacak');
var url = './efes_barca.html';
event.notification.close();
event.waitUntil(clients.openWindow(url));
});
Bu taraf için son olarak main içeriğini de aşağıdaki gibi geliştirebiliriz.
// load aşamasında Service Worker için register işlemi yapılır
window.addEventListener('load', e => {
if (!('serviceWorker' in navigator)) {
console.log('Service worker desteklenmiyor');
return;
}
navigator.serviceWorker.register('sworker.js')
.then(function () {
console.log('Servis Worker kaydoldu');
})
.catch(function (err) {
console.log('Hımm...Sanırım bir hata oluştu : ', err);
});
});
// Push Notification durumunu günceller
// Aktifse active.png, değilse passive.png
function updateStatus(status) {
divPush.dataset.checked = status; // statu bilgisini set et
if (status) { //true ise aktif
buttonPush.src = "./assets/active.png";
}
else { // değilse pasif olan ikonu göster
buttonPush.src = "./assets/passive.png";
}
}
function isPushNotifyEnabled() {
if (Notification.permission === 'denied') {
alert('Kullanıcı Push Notification hizmetini bloklamış');
return;
}
if (!('PushManager' in window)) {
alert('Üzgünüm ama Push Notification bu tarayıcı modelinde desteklenmiyor');
return;
}
/*
Kullanıcı Push Notification'ı engellememiş ve tarayıcı da bunu destekliyorsa
buraya geliriz.
Eğer Service Worker hazır ve kaydolmuşsa abonelik yönetimine başlanabilir.
*/
navigator.serviceWorker.ready.then(function (reg) {
reg.pushManager.getSubscription() //abone ol
.then(function (subs) { // abonelik durumuna göre statüleri güncelle
if (subs) {
updateStatus(true);
}
else {
updateStatus(false);
}
})
.catch(function (err) { // bir hata oluştuysa tarayıcı console'una hata basıyoruz.
console.error('Bir hata oluştu : ', err);
});
});
}
// Uygulamayı Push Notification hizmetine abone etmek için
function subscribe() {
navigator.serviceWorker.ready
.then(function (reg) {
if (!reg.pushManager) {
alert('Tarayıcı Push Notification hizmetini desteklemiyor');
return false;
}
reg.pushManager.subscribe(
{ userVisibleOnly: true }
).then(function (subs) {
console.log('Abonelik başladı.');
console.log(subs);
sendSubsID(subs); // Abonelik IDsini REST servise gönderen metodu çağırdık
updateStatus(true);
}).catch(function (err) {
updateStatus(false);
console.error('Abone olma işlemi sırasında hata oluştu: ', err);
});
});
}
// Abonelikten çıkartmak için
function unsubscribe() {
navigator.serviceWorker.ready
.then(function (reg) {
reg.pushManager.getSubscription()
.then(function (subs) {
if (!subs) {
alert('Abonelikten çıkılamıyor yahu :S');
return;
}
subs.unsubscribe()
.then(function () {
console.log('Abonelikten çıkıldı');
console.log(subs);
deleteSubsID(subs); // Aboneliği silerken ID'yi çıkartacak olan REST servis metodunu çağırdık
updateStatus(false);
})
.catch(function (err) {
console.error(err);
});
})
.catch(function (err) {
console.error('Abonelikten çıkarken bir hata oluştu : ', err);
});
})
}
// Sunucuya Subscription ID bilgisini göndermek için kullanıyoruz
// Bunun için yazdığımız PusherAPI node servisini kullanıyoruz
function sendSubsID(subscription) {
var id = subscription.endpoint.split('gcm/send/')[1];
fetch('http://localhost:8080/subscribers', {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ subscriptionid: id })
});
}
function deleteSubsID(subscription) {
var id = subscription.endpoint.split('gcm/send/')[1];
fetch('http://localhost:8080/subscribers/' +
id, {
method: 'delete',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
}
// div ve button elementlerini alıyoruz
var divPush = document.getElementById('divPush');
var buttonPush = document.getElementById('buttonPush');
/*
div elementinde mouse ile tıklandığı zaman gerçekleşecek olayı
dinleyecek bir listener bildirimi yaptık
*/
divPush.addEventListener('click', function () {
//console.log('Click');
if (divPush.dataset.checked === 'true') {
//console.log('Unsubscribe');
unsubscribe();
}
else {
//console.log('Subscribe');
subscribe();
}
});
isPushNotifyEnabled();
İlk Test
Kodları tamamladıktan sonra kısa bir test ile push notification hizmetinin çalışıp çalışmadığı hemen kontrol edilebilir. Bunun için terminalden
serve
komutunu verip uygulamayı ayağa kaldırmamız yeterli. Eğer aşağıdaki ekran görüntülerindekine benzer sonuçlar elde edebiliyorsak REST API uygulamasını yazmaya başlayabiliriz.
Uygulama, Push Notification hizmeti için abone olunurken benzersiz bir ID değeri alır. Firebase Cloud Messaging sistem bu değeri kullanarak kime bildirim yapılacağını bilebilir.
REST API Uygulamasının Yazılması
Abone olan uygulamaların ID bilgilerini yönetmek için Node.js tabanlı bir REST servisi yazabiliriz. Servis temel olarak PWA'nın FCM ile olan iletişiminde devreye girmektedir. Hem abonelik yönetimi hem de istemcilere bildirim yapılması ki bunu tek başına yapmayacaktır. Service Worker dolaylı olarak FCM üzerinden bu servisle yakın ilişki içerisindedir.
Bu servisi ayrı bir klasörde projelendirmek iyi olur. Pek tabii node.js tarafında REST Servisi yazımını kolaylaştırmak için bazı paketlerden destek alınabilir. express dışında HTTP mesajlarındaki gövdeleri kolayca ele almak için body-parser, servisin Firebase Cloud Messaging ile konuşabilmesini sağlamak amacıyla da fcm-node paketi kullanılablir. morgan modülünü ise sunucu tarafındaki HTTP trafiğini loglamak için değerlendirebiliriz. Aşağıdaki terminal komutları ile klasör ağacını oluşturup server dosyasını kodlayarak derlememize devam edelim.
mkdir PusherAPI
cd PusherAPI
touch server.js
sudo npm install express body-parser fcm-node morgan
server modülü
var express = require('express');
var app = express();
var bodyParser = require('body-parser');
var FCM = require('fcm-node');
var morgan = require('morgan');
app.use(morgan('combined'));
// Firebase Cloud Messaging nesnesini örneklerken
// bizim için üretilen server key değerini veriyoruz
var fcm = new FCM('bu kod sizde var...');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(function (req, res, next) {
// CORS problemi yaşamamak için gerekli header tanımlamaları yapılır
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
res.setHeader('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
next();
})
//abonelerin ID bilgilerini tutacağımız array.
// Bunun yerine daha kalıcı bir repository tercih edilebilir
var subscribers = []
// Web uygulaması HTTP Post ile subscriptionID gönderdiğinde çalışan metod
app.post('/subscribers/', function (req, res) {
// Mesaj gövdesinden abonelik bilgisini almaya çalışr
if (!req.body.hasOwnProperty('subscriptionid')) {
res.statusCode = 400;
res.send('Gönderilen bilgilerde eksik veya hata var');
return;
}
// bulduysan diziye ekle ve başarılı olduğu bilgisini ilet
subscribers.push(req.body.subscriptionid)
res.statusCode = 200; //HTTP 200 Ok basıyoruz
res.send('ID alındı');
});
// Web uygulaması abonelikten çıkarken HTTP Delete ile çağırdığı metod
app.delete('/subscribers/:id', function (req, res) {
// dizideki indis değerini URLden gelen id değerine göre bul
const index = subscribers.indexOf(req.params.id)
// varsa diziden çıkart
if (index !== -1) {
subscribers.splice(index, 1)
}
res.statusCode = 200;
res.send('ID silindi');
});
/*
Abonelere bildirim göndermek için tetiklenen REST metodu
localhost:8080/news/push çağrısı geldiğinde çalışır.
*/
app.get('/news/push', function (req, res) {
// Aboneler için mesaj hazırlanır
var message = {
registration_ids: subscribers,
collapse_key: 'i_love_this_game',
};
// Firebase Cloud Messaging üzerinden mesaj gönderilir
fcm.send(message, function (err, response) {
if (err) {
console.log(err)
} else {
console.log("Mesaj başarılı bir şekilde gönderildi: ", response);
}
});
res.sendStatus(200);
});
app.listen(8080);
console.log('Pusher API servisi 8080 üstünden dinlemede');
Çalışma Dinamikleri
Uygulamanın çalışma dinamiklerini anlamak oldukça önemli. Index.html olarak düşündüğümüz web uygulamamız çalıştırıldığında iki aksiyonumuz var. Basketbol topuna basıp bir abonelik başlatmak veya tekrar basarak aboneliği durdurmak.
Abonelik başlatıldığında FCM benzersiz bir ID değeri üretir ve bunu PusherAPI servisi kendisine gelen çağrı ile kayıt altına alır(diziye eklediğimiz yer) Sonraki herhangi bir t zamanında PusherAPI servisinin abonelere bildirim gönderen HTTP metodu tetiklenirse, Firebase Cloud Messaging devreye girer ve dizideki ID bilgilerini kullanarak abonelerine bildirimde bulunur. Bildirimler web uygulaması tarafındaki Service Worker(sworker.js) tarafından push olayıyla yakalanır. Push olayı şimdilik sadece statik bir sayfa gösterimi yapmakta ki aslında asıl içeriği yine servis üstünden veya web aracılığıyla başka bir adresten aldırabiliriz.
Çalışma Zamanı(Development Ortamı)
Testler için PWA ve servis tarafını ayrı ayrı çalıştırmalıyız.
serve
terminal komutu ile web uygulamasını
node server.js
ile de REST servisini başlatabiliriz. Aboneliği başlattıktan sonra http://localhost:8080/news/push adresine talepte bulunursak bir bildirim mesajı ile karşılaşırız
(sworker daki push olayı tetiklenir) Aynen aşağıdaki ekran görüntüsünde olduğu gibi.
Bildirim kutusuna tıklarsak statik olarak belirlediğimiz sayfa açılacaktır(yani notificationclick olayı tetiklenir)
PWA ve Service Uygulamalarının Firebase Hosting'e Alınması
Her iki uygulamada local geliştirme ortamında gayet güzel çalışıyor. Ancak bunu anlamlı hale getirmek için her iki ürünü de Firebase üzerine alıp genel kullanıma açmamız lazım. Web uygulamasını Firebase Hosting ile REST servisini de Firebase Function ile yayınlamalıyız. Bu işlemler için firebase-tools aracına ihtiyacımız olacak. Terminalden aşağıdaki komutu kullanarak ilgili aracı sisteme yükleyebiliriz.
sudo npm install -g firebase-tools
Basketkolik'in Dağıtımı
Yeni bir dağıtım klasörü oluşturmalı, initializion işlemini gerçekleştirip basketkolik uygulama kodlarını oluşan public klasörü içerisine atmalıyız. Ardından üzerinde çalışacağımız projeyi seçip deploy işlemini yapabiliriz. Bu işlemler için aşağıdaki terminal komutlarından yararlanılabilir.
mkdir dist
cd dist
firebase init
cp -a ../basketkolik/. ./public/
firebase use --add basketin-cepte-project
firebase deploy
firebase init işleminde bize bazı seçenekler sunulacaktır. Burada aşağıdaki görüntüde yer alan seçimlerle ilerleyebiliriz. En azından ben öyle yaptım.
Eğer dağıtım işlemi başarılı olursa aşağıdaki ekran görüntüsündekine benzer sonuçlar elde edilmelidir.
PusherAPI servisinin Dağıtımı
Hatırlanacağı üzere web uygulaması bir REST Servisi yardımıyla FCM sistemini kullanıyordu. PusherAPI isimli uygulama, Fireabase tarafı için bir Function anlamına gelmektedir(Serverless App olarak düşünelim) Ölçeklenebilirliği, HTTPS güvenliği, otomatik olarak ayağa kalkması gibi bir çok iş Google Cloud ortamı tarafından ele alınır. Şimdi aşağıdaki terminal komutu ile fonksiyon klasörünü oluşturalım (dist klasörü içerisinde çalıştığımıza dikkat edelim)
Yine bazı seçenekler karşımıza gelecektir. Burada gelen sorulara şöyle cevaplar verebiliriz;
- Dil olarak Javascript seçelim.
- ESLint kullanımına Yes diyelim.
- npm dependency'lerin kurulmasına da Yes diyelim ki uygulamanın gereksinim duyduğu node paketleri de yüklensin.
Devam eden adımda functions klasöründeki index dosyasının içeriğini PusherAPI'deki server içeriği ile değiştirmeliyiz. Ancak bu kez express'in firebase-functions ile kullanılması gerekiyor. İhtiyacımız olan express, body-parser ve fcm-node paketlerini üzerinde çalıştığımız functions klasörü içinede de yüklemeliyiz. Son olarak dist klasöründeki firebase.json dosyasına rewrites isimli yeni bir bölüm ekleyip fonksiyonumuzu deploy edebiliriz.
cd functions
sudo npm install express body-parser fcm-node
firebase deploy
Yapmamız gereken bir şey daha var. Web uygulamasının kullandığı main dosyasının içeriğini, yeni eklediğimiz google functions ile uyumlu hale getirmek. Tahmin edileceği üzere gidilen servis adreslerini, oluşturulan firebase proje adresleri ile değiştirmemiz lazım (dist/public/main.js içeriğini kontrol edin) Web uygulamasındaki bu değişikliği Cloud ortamına taşımak içinse public klasöründeyken yeniden bir deploy işlemi başlatmamız yeterli olacaktır.
firebase deploy
Çalışma Zamanı(Production Ortamı)
Uygulama artık https://basketin-cepte-project.firebaseapp.com/ adresinden yayında (En azından bir süre için yayındaydı ki aşağıdaki ekran görüntüsü de bunun kanıtıdır)
Ben Neler Öğrendim?
Bu çalışmanın da bana kattığı bir çok şey oldu elbette. Özellikle bir uygulamaya uzak sunuculardan bildirim yollanması ve bunun abonelik temelli yapılması merak edip öğrenmek istediğim konulardan birisiydi. İşin içerisine basit bir PWA modeli de ekleyince çalışma ilgi çekici bir hal almıştı. Yapılan hazırlıklar düşünüldüğünde aslında bizi çok fazla yormayacak bir geliştirme süreci olduğunu ifade edebiliriz. Derlemeyi sonlandırmacan önce yanıma kar kalanların neler olduğunu aşağıdaki maddeler ile özetleyebilirim.
- Firebase Cloud Messaging(FCM) sisteminin kabaca ne işe yaradığını
- PWA uygulamasının FCM ile nasıl haberleşebileceğini
- Abone olan istemciye bildirimlerin nasıl gönderilebileceğini
- Service Worker üzerindeki push ve notificationclick olaylarının ne anlama geldiğini
- serve paketinin kullanımını
- firebase terminal aracı ile deployment işlemlerinin nasıl yapıldığını
- Web uygulaması ve Functions'ın Google Cloud tarafından bakıldığında farklılıklarını
Bu arada proje büyük ihtimalle Google platformundan kaldırılmıştır. Malum istenmeyen bir yüklenme sonrası yüklü bir fatura ile karşılaşmamak adına küçük bir tedbir olduğunu ifade edebilirim. O nedenle kendiniz başarmaya çalışırsanız daha kıymetli olacaktır. Konseptin basketbol olması önemli değil. Abonelerinize bildirimlerde bulunacağınız herhangi bir senaryo olması yeterli olur. Bildirim yapan servisi de planlanmış bir düzeneğe bağlayabiliriz. Söz gelimi günün belirli anlarında bir konu ile ilgili bildirimlerin yönlendirilmesi işini üstlenebilir. Böylece geldik bir
saturday-night-works macerasının daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.