Zeig mir Deine

Offline Seite

(Progessive Web App)

Twitter: @mittlmedien

Email: info@mittl-medien.de

Über mich

  • Robert Mittl

  • Stuttgart, Germany

  • selbständig seit 2012

  • Webentwickler

  • JUG Stuttgart

Twitter: @mittlmedien

Email: info@mittl-medien.de

Internet

  • Geschwindigkeit oft langsam 😪

  • schlechte Verbindung

Webseiten offline speichern

  • nur wenige werden das tun

  • zumindest nicht manuell

  • wir werden das zusammen tun 👏

Progressive Web App

  • als Ansatz zur Lösung

  • ist eine Möglichkeit Webseiten offline verfügbar zu machen und eine native App funktionell nachzubilden, die installiert werden können

Vorteile - man könnte

  • bei einer Webseite mit den Kontaktdaten anzeigen, wenn offline

  • oder speichere eine bereits besuchte Webseite, so dass Sie erneut ohne Internetverbindung aufrufbar wäre

Zauberwort Serviceworker

regelt unser Vorhaben

Server

Serviceworker

Browser

Theorie am Praxisbeispiel

Grundlage eine Joomla Webseite

so "hübsch" wie wir Sie kennen 🤭

Voraussetzungen👆

  • Browserunterstützung (jeder Browser ist auf unterschiedlichem Stand der Implementierung)

  • SSL (oder localhost)

Serviceworker Lifecylcle 🔄

-> Register

-> Download

-> Installation

-> Warten

-> Aktivierung

Register Serviceworker im Template

window.addEventListener('load', async() => {
	if("serviceWorker" in window.navigator) {
		try {

		await window.navigator.serviceWorker.register('/serviceworker.js')

		} catch (error) {
			console.error(error)
		}
	}
})

fetch Event in serviceworker.js

self.addEventListener('fetch', e => {
    console.log(e.request)
})

Meine erste Offline Nachricht - Promise


self.addEventListener('fetch', e => {
    const request = e.request
    e.respondWith(
        fetch(request)
            .then(responseFromFetch => {
                // console.log(responseFromFetch)
                return responseFromFetch
            })
            .catch(error => {
                console.log(error)
                return new Response('<h1>Meine Joomla Seite ist offline</h1>',
                    {
                        headers: {
                            'Content-type': 'text/html; charset=utf-8'
                        }
                    })
            }
            )

    )
})

Meine erste Offline Nachricht - Async Await


const fetchRequest = async (request) => {
  try {
      const responseFromFetch = await fetch(request)
      return responseFromFetch
    } catch (error) {
      console.log(error)
      return new Response('<h1>Meine Joomla Seite ist offline</h1>',
          {
            headers: {
                  'Content-type': 'text/html; charset=utf-8'
                }
          })
    }
}

self.addEventListener('fetch', e => {
  e.respondWith(
        fetchRequest(e.request)
    )
})

💪 Offline Nachricht 👏

-> Offline Seite 🤔

Zauberwort Cache

Versionierung und Festlegen des Cache


const version = 'V0.01'
const staticCacheName = version + 'staticfiles'


self.addEventListener('install', e => {
    e.waitUntil(

    )
})

Event.waitUntil() -> wartet mit der Installation des Service Workers, bis der Code innerhalb ausgeführt wird

Cache Files


const cacheFiles = async () => {
  const cache = await caches.open(staticCacheName)
  const files = await cache.addAll([
      '/templates/protostar/css/template.css',
      '/media/jui/js/jquery.min.js',
      '/media/jui/js/jquery-noconflict.js',
      '/media/jui/js/jquery-migrate.min.js',
      '/media/system/js/caption.js',
      '/media/jui/js/bootstrap.min.js',
      '/templates/protostar/js/template.js',
      '/media/system/js/core.js',
      '/media/system/js/keepalive.js'
    ])
  return files
}

self.addEventListener('install', e => {
  e.waitUntil(
        cacheFiles()
    )
})

Cached Files

laden aus dem Cache

const cacheMatch = async (request) => {
  try {
      const url = request.url
      const cleanUrl = url.split('?')
      const cacheMatch = await caches.match(cleanUrl[0])
      if (cacheMatch) {
          console.log('cacheMatch', cacheMatch)
          return cacheMatch
        }
      return fetch(request)
    } catch (error) {
      console.log(error)
    }
}

self.addEventListener('fetch', e => {
  e.respondWith(
        cacheMatch(e.request)
    )
})

url.split() trennen, wegen Hash

serviceworker.js darf selbst nicht gecached werden

<IfModule mod_expires.c>
  <FilesMatch "serviceworker.js">
    ExpiresDefault "access plus 0 seconds"
  </FilesMatch>
</IfModule>”

Cache säubern


addEventListener('activate', e => {
    e.waitUntil(
        caches.keys()
            .then(cacheNames => {
                return Promise.all(
                    cacheNames.map(cacheName => {
                        if (cacheName != staticCacheName) {
                            return caches.delete(cacheName)
                        }
                    })
                )
            })
            .then(() => {
                return clients.claim()
            })
    )
})

clients.claim sorgt dafür, dass es gleich ausgführt wird, ohne dass ein Browser Refresh nötig ist

Offline Page

const files = await cache.addAll([
      '...,
      '/templates/protostar/offline.html',
      ...
    ])

im statischen Cache hinzufügen und im Template anlegen

try{
    //....
    const fetchRequest = await fetch(request)
      return fetchRequest
    } catch (error) {
      return caches.match('/templates/protostar/offline.html')
    }

und wenn Fehler Request --> offline.html

Cache

if (request.headers.get('Accept').includes('text/html')) {
    if (request.url.endsWith('/joomla-doc')
    /....
    
  }

if (request.url.match(/\.(jpe?g|png|gif|svg)$/)) {
    if (request.url.includes('piwik')) return

}

Bilder, HTML und nach URL filtern

Cache von Seiten

const responseFrom = async (request) => {
    try {
        const url = request.url
        const cleanUrl = url.split('?')
        const cacheMatch = await caches.match(cleanUrl[0])
        if (cacheMatch) {
            return cacheMatch
        } else {
            const fetchRequest = await fetch(request)
            if (fetchRequest.ok) {
                const cache = await self.caches.open(pageCacheName)
                cache.put(request, fetchRequest.clone())
            }
            return fetchRequest
        }
    } catch (error) {
        return caches.match('/templates/protostar/offline.html')
    }
}

Offline Seite ergänzen

<p>Folgende Seiten kannst Du offline besuchen:</p>
<ul id="history"></ul>
<script>
    
    caches.open('pages')
        .then(pagesCache => {
            pagesCache.keys()
                .then(keys => {
                    let markup = ''
                    keys.forEach(request => {
                       
                        markup += `<li><a href="${request.url}">${request.url}</a></li>`
                            
                    })
                    
                    document.querySelector('#history').innerHTML = markup
                })
        })
</script>

Cache säubern auf Anzahl der Items beschränken - Funktion

const trimCacheAsync = async (cacheName, maxItems) => {
  try {
    const responseFromCache = await caches.open(cacheName)
    const cacheKeys = await responseFromCache.keys()
    const items = await cacheKeys

    if (items.length > maxItems) {
      cache.delete(items[0])
        .then(
          trimCacheAsync(cacheName, maxItems)
        )
    }
  } catch (error) {
    //console.log('error is', error)
  }
}

Cache säubern auf Anzahl der Items beschränken - Message Event

self.addEventListener('message', messageEvent => {
  if (messageEvent.data === 'clean up caches') {
    trimCacheAsync(imageCacheName, 50)
    trimCacheAsync(pagesCacheName, 30)
  }
})

Cache säubern auf Anzahl der Items beschränken - beim registrieren

 if (navigator.serviceWorker) {
    navigator.serviceWorker.register('/serviceworker.js')

    if (navigator.serviceWorker.controller) {
      window.addEventListener('load', _ => {
        navigator.serviceWorker.controller.postMessage('clean up caches')
      })
    }
  }

PWA Manifest

<link rel="manifest" href="/manifest.json">

 https://app-manifest.firebaseapp.com/

{
    "lang": "de",
    "name": "Joomla",
    "short_name": "Joomla",
    "description": "Meine Joomla PWA",
    "theme_color": "#99ccff",
    "background_color": "#99ccff",
    "display": "standalone",
    "Scope": "/",
    "start_url": "/",
    "icons": [
        {
            "src": "images/icons/icon-72x72.png",
            "sizes": "72x72",
            "type": "image/png"
        },
        {

Weitere 😎 Features:

  • Push Notifications

  • Background Sync

Push Notifications

  • Background Push

  • Active Push

☝️ Achtung kein Support in iOS oder Safari

Push Notifications

pushButton.addEventListener('click', async () => {
  const permission = await Notification.requestPermission()
  if (permission === 'granted') {
    const notifyObj = new Notification('Joomla Day Austria', {
      body: 'Was für eine schöner Tag 😀',
      image: './images/icons/icon-512x512.png',
      icon: './images/icons/icon-152x152.png',
      badge: './images/icons/icon-72x72.png'

    })
  }
})

Push Notifications

mögliche Beispiele:

  • bei Aktualisierung des Serviceworkers (Updatebenachrichtigung)
  • Bei Auslösung bestimmter Ereignisse

  • Versand über einen Dienst der dann den Serviceworker anspricht (Firebase Cloud Messaging, Cleverpush, OneSignal)

Beispiele

Twitter

Trivago

 

oder schaut wieviel Service Worker bei Euch im Browser bereits installiert sind

Tools für PWA

Workbox von Google

zum testen:

Lighthouse in Chrome für Manifest


Google PWA Training:

https://developers.google.com/web/ilt/pwa/


Buch: Jeremy Keith. “Going Offline”


Fragen?

Twitter: @mittlmedien

Email: info@mittl-medien.de

Danke 👏

Quellen

Buch: Jeremy Keith. “Going Offline”

 

Peter Kröner: PWA Workshop

 

MDN: Service Worker

Made with Slides.com