Progressive Web App (PWA)

Was ist eine PWA

Bei einer Progressive Web App handelt es sich um eine Symbiose aus einer responsiven Website und einer nativen App. Es wird versucht die jeweiligen Vorteile der beiden Arten zu verbinden und die Nachteile zu verhindern. Eine PWA verhält sich wie eine herkömmliche App und wird ebenfalls über ein Icon auf dem Homescreen geöffnet.

Merkmale einer PWA

  • Werden wie eine normale Website mittels HTML, CSS und JavaScript erstellt
  • Service Worker ermöglichen durch optimales Caching eine offline Verwendung
  • Kann wie eine Website über eine URL aufgerufen werden und anschließend mit einer Ad-to-Homescreen Fukntion auf dem Homescreen verknüpft werden
  • setzt HTTPS voraus
  • ähnlich hohe Kundenbindung wie eine native App, da ebenfalls auf dem Homescreen mit Logo hinterlegt

Vorteile einer PWA

  • Macht eine doppelte bzw. dreifache Entwicklung unnötig, da eine PWA gleichzeitig als normale Website, mobile Website und mobile App verwendet werden kann
  • geringere Hürde, um die PWA auf den Homescreen zu installieren als eine App über den Umweg über den Appstore
  • unabhängig von den Richtlinien der Appstores und deren Gebühren
  • Benötigt deutlich weniger Speicherplatz auf dem Endgerät als eine native App
  • ermöglicht Push-Notifications, somit können Nutzer auf Neuheiten aufmerksam gemacht werden, dies zu einer Steigerung der Interaktionsrate führen kann
  • Offline First Ansatz, das heißt, bereits abgerufene Inhalte stehen auch offline zur Verfügung, fall gerade keine Verbindung mit dem Internet besteht
  • PWA kann somit dank den sogenannten Service Workern auch offline verwendet werden

Service Worker

  • JavaScript, dass vom Webserver im Hintergrund ausgeführt wird
  • Sobald die Website einmal aufgerufen wurde, wird sie gecached und kann nun auch ohne Internetverbindung erneut aufgerufen werden (Offline Betrieb)
  • Service Worker müssen zuerst ganz normal als JavaScript programmiert, anschließend registriert und installiert werden
  • Frameworks wie Angular mit Mobile Toolkit stellen Service Workers bereit
  • Hintergrundaktualisierungen ermöglichen eine Sicherstellung, dass die Aktionen auch wirklich ausgeführt werden sobald eine stabile Internetverbindung besteht, auch wenn die PWA bereits verlassen oder geschlossen wurde

Code-Snippets zur Erstellung einer PWA

Notwendige Informationen für den head-Bereich

 
<script src="https://dev-grades.de/skripte/service-worker.js"></script>
<script src="https://dev-grades.de/skripte/app.js"></script>
<link rel="manifest" href="https://dev-grades.de/skripte/manifest.json"></link>
<!-- Add to home screen for Safari on iOS -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="First PWA">
<link rel="apple-touch-icon" href="https://dev-grades.de/Master/IMG_eigen/seoAndMore_App.png">
<meta name="msapplication-TileImage" content="https://dev-grades.de/Master/IMG_eigen/seoAndMore_App.png">
<meta name="msapplication-TileColor" content="#6aa0cc">
                

Grundsätzlicher Aufbau einer Service-Worker JavaScript-Datei

 
var dataCacheName = 'appData-v1';
var cacheName = 'PWA-1';
var filesToCache = [
  '/',
  "https://dev-grades.de/",
  "https://dev-grades.de/Development/pwa.html",
  "https://dev-grades.de/Games/wuerfel.html",
  "https://dev-grades.de/Master/bootstrap.css",
  "https://dev-grades.de/Master/StyleSheet.css",
  "https://dev-grades.de/Master/IMG_eigen/favicon.ico",
  "https://dev-grades.de/skripte/ajax_googleAPI.js",
  "https://dev-grades.de/skripte/bootstrapcdn.js",
  "https://dev-grades.de/skripte/service-worker.js",
  "https://dev-grades.de/skripte/manifest.json",
  "https://dev-grades.de/masterSkript.js"
];
    

self.addEventListener('install', function(e) {
  console.log('[ServiceWorker] Install');
  e.waitUntil(
    caches.open(cacheName).then(function(cache) {
      console.log('[ServiceWorker] Caching app shell');
      return cache.addAll(filesToCache);
    })
  );
});

self.addEventListener('activate', function(e) {
  console.log('[ServiceWorker] Activate');
  e.waitUntil(
    caches.keys().then(function(keyList) {
      return Promise.all(keyList.map(function(key) {
        if (key !== cacheName && key !== dataCacheName) {
          console.log('[ServiceWorker] Removing old cache', key);
          return caches.delete(key);
        }
      }));
    })
  );
  /*
   * Fixes a corner case in which the app wasn't returning the latest data.
   * You can reproduce the corner case by commenting out the line below and
   * then doing the following steps: 1) load app for first time so that the
   * initial New York City data is shown 2) press the refresh button on the
   * app 3) go offline 4) reload the app. You expect to see the newer NYC
   * data, but you actually see the initial data. This happens because the
   * service worker is not yet activated. The code below essentially lets
   * you activate the service worker faster.
   */
  return self.clients.claim();
});

self.addEventListener('fetch', function(e) {
  console.log('[Service Worker] Fetch', e.request.url);
  var dataUrl = 'https://query.yahooapis.com/v1/public/yql';
  if (e.request.url.indexOf(dataUrl) > -1) {
    /*
     * When the request URL contains dataUrl, the app is asking for fresh
     * weather data. In this case, the service worker always goes to the
     * network and then caches the response. This is called the "Cache then
     * network" strategy:
     * https://jakearchibald.com/2014/offline-cookbook/#cache-then-network
     */
    e.respondWith(
      caches.open(dataCacheName).then(function(cache) {
        return fetch(e.request).then(function(response){
          cache.put(e.request.url, response.clone());
          return response;
        });
      })
    );
  } else {
    /*
     * The app is asking for app shell files. In this scenario the app uses the
     * "Cache, falling back to the network" offline strategy:
     * https://jakearchibald.com/2014/offline-cookbook/#cache-falling-back-to-network
     */
    e.respondWith(
      caches.match(e.request).then(function(response) {
        return response || fetch(e.request);
      })
    );
  }
});
//This is a event that can be fired from your page to tell the SW to update the offline page
self.addEventListener('refreshOffline', function(response) {
  return caches.open('pwabuilder-offline').then(function(cache) {
    console.log('[PWA Builder] Offline page updated from refreshOffline event: '+ response.url);
    return cache.put(offlinePage, response);
  });
});                
                

Aufbau eines Manifests

In einem Manifest werden die grundsätzlichen Rahmenbedingungen festgelegt

 
{
  "name": "SEO Development & more",
  "short_name": "SEO&more",
  "icons": [{
    "src": "ihttps://dev-grades.de/Master/IMG_eigen/seoAndMore_App.png",
      "sizes": "128x128",
      "type": "image/png"
    }, {
      "src": "https://dev-grades.de/Master/IMG_eigen/seoAndMore_App.png",
      "sizes": "144x144",
      "type": "image/png"
    }, {
      "src": "https://dev-grades.de/Master/IMG_eigen/seoAndMore_App.png",
      "sizes": "152x152",
      "type": "image/png"
    }],
  "start_url": "https://dev-grades.de/Games/wuerfel.html",
  "display": "standalone",
  "background_color": "#6aa0cc",
  "theme_color": "#2F3BA2"
}
            

Aufbau einer app.js Datei

 
/*****************************************************************************
   *
   * Event listeners for UI elements
   *
****************************************************************************/

  document.getElementById('butRefresh').addEventListener('click', function() {
    // Refresh all of the forecasts
    app.updateForecasts();
  });

/*****************************************************************************
   *
   * Methods to update/refresh the UI
   *
****************************************************************************/

    // Toggles the visibility of the add new city dialog.
    app.toggleAddDialog = function(visible) {
      if (visible) {
        app.addDialog.classList.add('dialog-container--visible');
      } else {
        app.addDialog.classList.remove('dialog-container--visible');
      }
    };