Django Web Sitesini Progressive Web App (PWA)'e / Progresif Web Uygulamasına Dönüştürme Nasıl Yapılır?
Yazan: Ali Kasımoğlu_
13 Şubat 2021 18:43

Günümüzde en çok kullanılan iletişim aracı olan mobil cihazlar web teknolojilerinin de gelişimini ciddi oranda etkilemektedir. Kullanıcılar her geçen gün daha fazla oradan mobil cihazlardan web sitelerini ziyaret etmektedirler. Birçok sitenin mobil uygulaması bulunurken, uygulama boyutları, yükleme ihtiyacı ve bunlarla bağlantılı zaman kaybı da giderek daha önemli hale gelmiş durumdadır. İşte bu probleme çözüm olarak geliştirilen Progressive Web App (PWA) ya da Türkçe olarak yazarsak Progresif Web Uygulaması adeta kullanıcıların ve geliştiricilerin imdadına yetişti.

Konuyu teknik açıdan ele almadan önce, bunun neden gerekli olduğu konusunu irdelemek isterim. Düşünün; şirketiniz için web sitesi yaptırdınız ve hatta bunu responsive yani uyarlanabilir olarak mobil uyumlu yaptırdınız. Ancak mobil uygulama da istiyorsunuz. Ama bu size ciddi bir ek maliyet gerektiriyor. Veya diğer açıdan yaklaşalım. Firmanın web sitesine giren bir kullanıcısınız. Siteye sürekli internet tarayıcısı kullanarak girmek yerine, daha hızlı ve pratik olan ve hatta internetsiz de kullanılabilen mobil uygulamasını kullanmak istiyorsunuz. İşte her iki durumda da PWA sizin imdadımıza yetişecektir.

Django Web Sitesi Nasıl Progressive Web App (PWA)'e Dönüştürülür?

İÇERİK:

1. Manifest dosyasını oluşturun
2. HTML'de manifest etiketini tanımlama
3. Hizmet çalıştırıcı (Service Worker) oluşturma
4. Service Worker'ı kayıt ettirme
5. Yükleme bildirimi etkinliği hazırlama

Başlamadan önce bunun bir çok yolunun olduğunu ve hatta django-pwa gibi hazır modüllerin de olduğunu söyleyebilirim. Ancak ben size bunu manuel olarak en doğru şekilde nasıl yapabileceğinizi anlatacağım.

Not 1: Bu yöntemleri Django harici yapılmış bir web sitesi için de uygulayabilirsiniz.

Not 2: PWA'nın çalışabilmesi için sitenizde SSL sertifikası olması gerekmektedir.

Django URL Ayarları:

Başlamadan önce, PWA'yı bir Django projesinde uygulayacaklar için URL yapısını nasıl oluşturabileceğinizi gösterelim.

İlk önce path ve TemlpateView modüllerini import etmleliyiz.

from django.urls import path
from django.views.generic import TemplateView

Ardından PWA için gerekli olan manifest.json, service-worker.js ve offilne.html dosyalarımızı projemizde kullanılabilir hale getirmek için Django'nun bize sunduğu TemplateView yapısından faydalanıyoruz.

    path('manifest.json', TemplateView.as_view(template_name="manifest.json")),
    path('offline.html', TemplateView.as_view(template_name="offline.html", content_type='text/html')),
    path('service-worker.js', TemplateView.as_view(template_name="service-worker.js", content_type='text/javascript')),

Bu kadar. Şimdi PWA için çalışmalara başlayalım.

1. Manifest dosyasını oluşturun: PWA uygulamasının ayarlarını barındırdığı en kritik dosya olan manifest.json dosyasını oluşturarak başlıyoruz. Manifest dosyası Chrome, Edge, Firefox, UC Browser, Opera, ve Samsung browser tarafından tam desteklenir. Safari ise kısmen destek sunar. Gelin tüm özellikleri barındıran bir manifest.json örneğine bakalım.

{
  "name": "Uygulama Adınız Buraya",
  "short_name": "Kısa Ad",
  "description": "Uygulamanız hakkında kısa açıklama yazısı",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#FFFFFF",
  "theme_color": "#2F3B4B",
  "orientation": "any",
  "categories": ["books", "education"],
  "dir": "ltr",
  "lang": "tr-TR",
  "prefer_related_applications": true,
  "related_applications": [
    {
      "platform": "play",
      "url": "https://play.google.com/store/apps/details?id=com.uygulamaadinizburaya.twa",
      "id": "com.uygulamaadinizburaya.twa"
    }, {
      "platform": "itunes",
      "url": "https://itunes.apple.com/app/uygulamaadinizburaya/id123456789"
    }
  ],
  "icons": [
    {
      "src": "/static/img/icons/icon-72x72.png",
      "type": "image/png", "sizes": "72x72", "purpose": "any"
    },
    {
      "src": "/static/img/icons/icon-96x96.png",
      "type": "image/png", "sizes": "96x96", "purpose": "any"
    },
    {
      "src": "/static/img/icons/icon-128x128.png",
      "type": "image/png","sizes": "128x128", "purpose": "any"
    },
    {
      "src": "/static/img/icons/icon-144x144.png",
      "type": "image/png", "sizes": "144x144", "purpose": "any"
    },
    {
      "src": "/static/img/icons/icon-152x152.png",
      "type": "image/png", "sizes": "152x152", "purpose": "any"
    },
    {
      "src": "/static/img/icons/icon-192x192.png",
      "type": "image/png", "sizes": "192x192", "purpose": "any"
    },
    {
      "src": "/static/img/icons/icon-384x384.png",
      "type": "image/png", "sizes": "384x384", "purpose": "any"
    },
    {
      "src": "/static/img/icons/icon-512x512.png",
      "type": "image/png", "sizes": "512x512", "purpose": "any"
    },
    {
      "src": "/static/img/icons/maskable_icon_2_512x512_black.png",
      "type": "image/png", "sizes": "512x512", "purpose": "maskable"
    }
  ],
  "shortcuts": [
    {
      "name": "Projeleri Aç",
      "short_name": "Projeler",
      "description": "Yaptığım Projeler",
      "url": "/projeler",
      "icons": [{ "src": "/static/img/icons/icon-96x96.png", "sizes": "96x96" }]
    },
    {
      "name": "Blog Yazılarını Aç",
      "short_name": "Blog",
      "description": "Son Blog Yazıları",
      "url": "/blog",
      "icons": [{ "src": "/static/img/icons/icon-96x96.png", "sizes": "96x96" }]
    }
  ],
  "screenshots": [
    {
      "src": "/images/screenshot1.png",
      "type": "image/png",
      "sizes": "540x720"
    },
    {
      "src": "/images/screenshot2.jpg",
      "type": "image/jpg",
      "sizes": "540x720"
    }
  ]
}

Yukarıda örnek olarak verdiğim manifest dosyasında birçok ek özellik de belirtilmesine rağmen, aslında minimum manifest dosyası şu şekilde olabilir.

{
  "short_name": "Kısa Ad",
  "display": "standalone",
  "icons": [
    {
      "src": "/static/img/icons/icon-192x192.png",
      "type": "image/png", "sizes": "192x192", "purpose": "any"
    },
    {
      "src": "/static/img/icons/icon-512x512.png",
      "type": "image/png", "sizes": "512x512", "purpose": "any"
    },
  ],
}

Buradaki özellik alanlarının ne işe yaradığına kısaca bir bakalım.

Manifest.json Dosyası Özellikleri

name: Uygulamanızın yükleme ekranında gözükecek tam adı. Eğer belirtmezseniz short_name kullanılır.
short_name: Uygulamanız ana ekranda gözükecek kısa adı.
start_url: Uygulamanın sitenizin hangi alanında başlayacağını belirleyen kısım. "/" veya "." yaparsanız alanadiniz.com adresine girilince çalışması gerektiğini söylemiş oluruz. Bu alanı boş da geçebilirsiniz. Default olarak "." tanımlı oluyor.
display: Uygulamanızın hangi görünüm formatını kullanacağını belirler. "fullscreen" uygulamanın tam ekran çalışması için tanımlanır. "standalone" en çok tercih edilen bu modda adres çubuğu, tarayıcı menüleri gizli olur ama Android durum çubuğu gözükür.

"minimal-ui" standalone gibidir ancak tarayıcı için minimum ek araç çubuğu alanlarını gösterir.

"browser" modu ise tüm tarayıcı alanlarını gösterir. 
background_color: Bu alan sayesinde başlangıç ekranı arkaplan rengi "splash screen" tanımlanır.
theme_color: Bu alan sayesinde tarayıcı araç çubuğu ve Android bildirim çubuğu rengi belirlenir.

scope: Tarayıcının uygulamanızdan ne zaman ayrıldığını tanımlayan kapsam belirleme alanıdır. Bu da zorunlu alan değildir. 
orientation
prefer_related_applications
related_applications
icons: Uygulamanın farklı ortamlarda kullanılacak olan ikonu. "src" ile kaynak belirtilmesi, "type" ile resim formatının belirtilmesi ve "size" ile boyutunun tanımlanması gerekiyor. Android cihazlar için isteğe bağlı olarak "purpose" özelliğini kullanarak "any maskable" tanımlaması yaparak, maskable ikonunun her yerde kullanılmasını sağlayabilirsiniz. Ayrıca Chrome 192x192 ve 512x512 boyutlarında ikona ihtiyaç duyuyor.
shortcuts: Uygulama ikonuna basılı tutarak açılır pencereden hızlı ulaşım seçenekleri eklemeye yarar.

description: Kurulum ekranında uygulamanız hakkında kısa bilgilendirme yazısını tanımlamanızı sağlar.
screenshots: Uygulamanızın yükleme ekranlarında ön izleme resmi için tanımlanır. Genişlik ve yükseklik en az 320 piksel ve en fazla 3840 piksel olmalıdır ve sadece jpeg ve png formatları desteklenir.
categories: Uygulamanızın hangi kategoride olduğunu tanımlamamıza yarar.
lang: Uygulama içeriğinin hangi dilde olduğunu tanımlar.
dir: Uygulama yazı yönünü tanımlamamızı sağlar. Örneğin "ltr" tanımlaması "left to right", yani soldan sağa şeklinde tanımlama yapar. Default olarak kullanıcının cihaz tanımlamasını baz alır.
orientation: Uygulamanızın hangi yönlendirme yerleşimlerini desteklediğini belirlememize yarar. Genellikle "any" kullanılarak tüm yönlendirmelerin desteklenmesi sağlanır.
prefer_related_applications: Web sitenizin Android, iOS veya Windows gibi işletim sistemleri için yazılmış hali hazırda markette bulunan bir uygulaması varsa, tarayıcının kullanıcılara bunu önermesi için gereklidir. "true" tanımlanırsa önerir. Default değeri zaten "false" şeklindedir. related_applications ile kullanılır.
related_applications: Native mobil uygulaması için marketlere yönlendirecek bağlantıları tanımlamamızı sağlar.

2. HTML'de manifest etiketini tanımlama: manifest.json dosyasını oluşturduktan sonra yapmamız gereken şey sitemizin html şablonlarında onu head etiketinin içinde tanımlamak olacaktır.

<!doctype html>
<html lang="tr">
<head>
    ...
    <link rel="manifest" href="manifest.json">
    ...
</head>

Burada dikkat etmemiz gereken kısım manifest.json dosyasının dizinde bulunduğu yolu doğru tanımlamaktır.

3. Hizmet çalıştırıcı (Service Worker) oluşturma: Tanımladığımız PWA fonksiyonlarının tarayıcı tarafından kullanılabilmesi için service-worker.js dosyasını hazırlamamız gerekiyor. Bunun için standartlardan faydalanabiliriz. Örnek service-worker.js dosyası şöyledir;

/*
Copyright 2015, 2019, 2020 Google LLC. All Rights Reserved.
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 http://www.apache.org/licenses/LICENSE-2.0
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
*/

// Incrementing OFFLINE_VERSION will kick off the install event and force
// previously cached resources to be updated from the network.
const OFFLINE_VERSION = 1;
const CACHE_NAME = "offline";
// Customize this with a different URL if needed.
const OFFLINE_URL = "offline.html";

self.addEventListener("install", (event) => {
  event.waitUntil(
    (async () => {
      const cache = await caches.open(CACHE_NAME);
      // Setting {cache: 'reload'} in the new request will ensure that the
      // response isn't fulfilled from the HTTP cache; i.e., it will be from
      // the network.
      await cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
    })()
  );
  // Force the waiting service worker to become the active service worker.
  self.skipWaiting();
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    (async () => {
      // Enable navigation preload if it's supported.
      // See https://developers.google.com/web/updates/2017/02/navigation-preload
      if ("navigationPreload" in self.registration) {
        await self.registration.navigationPreload.enable();
      }
    })()
  );

  // Tell the active service worker to take control of the page immediately.
  self.clients.claim();
});

self.addEventListener("fetch", (event) => {
  // We only want to call event.respondWith() if this is a navigation request
  // for an HTML page.
  if (event.request.mode === "navigate") {
    event.respondWith(
      (async () => {
        try {
          // First, try to use the navigation preload response if it's supported.
          const preloadResponse = await event.preloadResponse;
          if (preloadResponse) {
            return preloadResponse;
          }

          // Always try the network first.
          const networkResponse = await fetch(event.request);
          return networkResponse;
        } catch (error) {
          // catch is only triggered if an exception is thrown, which is likely
          // due to a network error.
          // If fetch() returns a valid HTTP response with a response code in
          // the 4xx or 5xx range, the catch() will NOT be called.
          console.log("Fetch failed; returning offline page instead.", error);

          const cache = await caches.open(CACHE_NAME);
          const cachedResponse = await cache.match(OFFLINE_URL);
          return cachedResponse;
        }
      })()
    );
  }

  // If our if() condition is false, then this fetch handler won't intercept the
  // request. If there are any other fetch handlers registered, they will get a
  // chance to call event.respondWith(). If no fetch handlers call
  // event.respondWith(), the request will be handled by the browser as if there
  // were no service worker involvement.
});

Standart bir servis-worker.js dosyasının yapması gerekenler arasında install, activate ve fetch etkinliklerinin oluşturmak yer alır. Aynı zamanda Offline (çevrimdışı) sayfasını da bu dosyada oluşturabiliriz. Bu sayede internet giderse kullanıcıya istediğimiz çevirim dışı sayfasını göstermiş oluruz. Hatta tüm içeriği önbelleğe alıp sonra çevirim dışı kullandırmak da mümkündür. Biz örneğimizde offline.html sayfası hazırladık. Chrome'da internet kesildiğinde gördüğümüz sayfa benzeri bir yapı yani.

Dikkatinizi const OFFLINE_URL = "offline.html"; koduna ermenizi rica ediyorum. Burada offline.html'nin konumunu doğru girmek önemlidir. Gelin bir de örnek çevrimdışı sayfası inceleyelim:

alikasimoglu.com/offline.html

Gördüğünüz gibi standart bir html sayfasından ibaret.

4. Service Worker'ı kayıt ettirme: Yukarıda hazırladığımız service-worker.js dosyasını html tarafında sitemize kayıt ettirmemiz gerekiyor. Bunun yapmamız gereken aşağıdaki kodu </head> etiketinden hemen önce yerleştiriyoruz.

    <script>
        window.addEventListener("load", () => {
            if ("serviceWorker" in navigator) {
                navigator.serviceWorker.register("service-worker.js");
            }
        });
    </script>

Bu sayede tarayıcı hazırladığımız service-worker.js dosyasını görecek ve çalıştıracaktır. Yapabileceğimiz şeyler bu kadar değil elbette.

5. Yükleme bildirimi etkinliği hazırlama: beforeinstallprompt ile kullanıcılarımıza sitemize girdiklerinde PWA uygulamasını yüklemeleri için tarayıcı tarafından bildirim verdireceğiz. Bunun da iki ana yöntemi mevcuttur. Örneğin tarayıcının kendi dahili bildirim özelliğini kullanmak mümkün. Ancak bu tüm tarayıcılarda çalışmayabilir ve kullanıcıların fark etmesi için yeterli olmayabilir. Veya bizim kendi hazırladığımız, web sitemizin de isteğimiz şekilde gözükmesini sağlayabileceğimiz bir bildirim butonu da oluşturabiliriz. Gelin ikinci ve daha zor olan yöntemi yapalım.

Öncelikle sitemizde bir bildirim elementi oluşturmamız gerekiyor. Buna Material Design kütüphanesinde Snackbar veya Bootstrap Framework'te Toast deniyor. Dilerseniz kendiniz de hazırlayabilirsiniz elbette. Bu standart bir html elementi sonuçta. Gelin bootstrap ile yapalım.

    <div aria-live="polite" aria-atomic="true">
        <div class="butInstall toast" data-autohide="false" aria-atomic="true">
            <div class="toast-header">
                <button type="button" class="btn btn-sm app-download">Uygulamayı İndir</button>
                <button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
                    <span aria-hidden="true" class="badge badge-dark">&times;</span>
                </button>
            </div>
        </div>
    </div>

Güzel gözükmesi için css kodlarını şöyle yazabiliriz:

.butInstall {
    display: inline-block;
}

.toast {
    display: flex;
    position: fixed;
    z-index: 98;
    margin-left: 30px;
    padding: 10px 10px 10px 10px;
    bottom: 20px;
    left: 0;
    border-radius: 10px;
    background-color: #8a8f97;
}

.app-download {
    background-color: #5E60CE;
    color: white;
    -webkit-transition: all 0.3s ease;
    -moz-transition: all 0.3s ease;
    -o-transition: all 0.3s ease;
    transition: all 0.3s ease;
}

.app-download:hover {
    background-color: #4C5D74;
}

.badge {
    vertical-align: middle;
}

Ayrıca </head> etiketinin öncesinde butonumuzun gözükmesi için şu JavaScript kodunu ekliyoruz:

    <script>
        $(document).ready(function () {
            $('.toast').toast('show');
        });
    </script>

Butonumuz şu şekilde gözükecektir:

Şimdi de bu butonun sadece PWA uygulamamızı yüklemeden kullanıcılarda gözükmesi için yükleme kontrol edici ve dinleyici eylem oluşturalım.

    <script>
        let deferredPrompt;
        const addBtn = document.querySelector('.butInstall');
        addBtn.style.display = 'none';

        window.addEventListener('beforeinstallprompt', (e) => {
            // Prevent Chrome 67 and earlier from automatically showing the prompt
            e.preventDefault();
            // Stash the event so it can be triggered later.
            deferredPrompt = e;
            // Update UI to notify the user they can add to home screen
            addBtn.style.display = 'block';

            addBtn.addEventListener('click', (e) => {
                // hide our user interface that shows our A2HS button
                addBtn.style.display = 'none';
                // Show the prompt
                deferredPrompt.prompt();
                // Wait for the user to respond to the prompt
                deferredPrompt.userChoice.then((choiceResult) => {
                    if (choiceResult.outcome === 'accepted') {
                        console.log('User accepted the A2HS prompt');
                    } else {
                        console.log('User dismissed the A2HS prompt');
                    }
                    deferredPrompt = null;
                });
            });
        });
    </script>

Bu etkinlik sayesinde JavaScript ile tarayıcıya kullanıcının PWA uygulamasını indirip indirmediğini sorguladık. Eğer indirmediyse .butInstall class'ının yer aldığı elementi göster dedik. Eğer kullanıcı butona tıklarsa tarayıcı uygulamamızı indirebilmesi için yükleme alanını açacak. Eğer kullanıcı uygulama indirme butonunu kapatırsa tamamen kapanacak. 

Dilersek kullanıcıların uygulamayı yükleyip yüklemediğini, nasıl açıldığını veya hangi cihaz veya tarayıcı modunda kullandığını da takip edebiliriz. Bunun için </head> kodundan önce şu kodları ekliyoruz:

    <script>
        window.addEventListener('appinstalled', (event) => {
            console.log('👍', 'appinstalled', event);
        });
    </script>
    <script>
        window.addEventListener('DOMContentLoaded', () => {
            let displayMode = 'browser tab';
            if (navigator.standalone) {
                displayMode = 'standalone-ios';
            }
            if (window.matchMedia('(display-mode: standalone)').matches) {
                displayMode = 'standalone';
            }
            // Log launch display mode to analytics
            console.log('DISPLAY_MODE_LAUNCH:', displayMode);
        });
    </script>

Bu kadar. Artık web sitemiz 100% mobil uyumlu ve sorunsuz çalışan bir PWA uygulaması var. Bir sonraki yazımızda mevcut PWA uygulamasını nasıl Android için APK dosyasına çevirip Google Play Store'a yükleyerek yayınlayabileceğimizi anlatacağım.

İletişime Geçin_

Benimle iletişime geçmek için adresine e-posta gönderebilirsiniz. Size en kısa sürede geri dönüş yapacağımdan emin olabilirsiniz. Ayrıca dilerseniz kasimoglu.ali Skype kullanıcı adımdan yada linkedin, facebook veya twitter sosyal medya profillerimden bana ulaşabilirsiniz. Gerçekten iletişime geçmek istiyorsanız bunu bir şekilde yapabileceğinize eminim :)