CMPUT 404

Web Applications and Architecture

Progressive Web Applications

Created by
Hazel Campbell (hazel.campbell@ualberta.ca).
Copyright 2023.

Progressive Web Apps

  • Run on multiple platforms and devices from one codebase
    • Can run both as a website and a mobile app
    • Can even run on some refrigerators
  • Can perform tasks while main app is not running
  • Modern examples include Uber, Spotify, Google Maps, and Pinterest

Progressive Web Apps

  • Work offline
  • Manage caching
    • & pre-loading!
  • Notifications

PWA parts

Installer script

  • Loaded by HTML (directly or indirectly) <script> tag
  • Should be in all the main pages of the app
  • Uses the navigator.serviceWorker object, a ServiceWorkerContainer
  • Has access to the page and everything like a normal script

Installer script

  • First, check if serviceWorker is supported
if ("serviceWorker" in navigator) {
    navigator.serviceWorker... // do something with the ServiceWorkerContainer API
}

ServiceWorker

  • Intended to improve performance of website/app
    • caching - different from HTTP cache
    • Event based
    • Works in the background- does not interrupt main app
  • Optional

ServiceWorker API

  • Basically everything returns promises
  • async / await is your friend!
const registerServiceWorker = async () => {
    if ("serviceWorker" in navigator) {
        await navigator.serviceWorker... // do something with the ServiceWorkerContainer API
    }
}

// just start the function and proceed with the rest of the script
registerServiceWorker(); 

ServiceWorker script

  • Loaded by the browser after being registered by the installer script
  • Must be at the root of all the pages it's going to work with
    • It includes sub-folders and pages in the same folder
    • Excludes other domains and other parent folders!

ServiceWorker script

  • Must be loaded over HTTPS
  • Cannot access the page, window, and most JS APIs
    • console.log messages will not show up in the tab!
  • Runs in a seperate thread

ServiceWorker script

  • Mainly accesses the API through the ServiceWorkerGlobalScope
  • Difficult to debug...
  • Can communicate to pages via
    const sendToAllTabs async (data) => {
       (await self.clients.matchAll()).map((client) => {
            // data is some structured-cloneable object
            await client.postMessage(data);
        });
    }
    

Communication

  • Pages can receive messages from ServiceWorker:
    if ("serviceWorker" in navigator) {
        navigator.serviceWorker.addEventListener("message", (event) => {
            if ("data" in event) {
                // data.event is the structured-cloneable message from the serviceworker
            }
        })
        navigator.serviceWorker.startMessages();
    }
    

Structured Cloneable

  • MDN Documentation
  • Objects that can be easily cloned
    • Similar to Python copy.deepcopy
  • Used for thread-to-thread Communication
    • Web Workers
    • Shared Workers
    • Service Workers

Structured Cloneable

  • Can have:
    • Plain Objects
    • String
    • Number
    • Array
    • ArrayBuffer
    • Boolean
    • DataView
    • Date
    • Error (kinda)
    • Map
    • Number
    • null
    • undefined
    • BigInt
    • RegExp (kinda)
    • Set
    • String
    • TypedArray

Structured Cloneable

  • Can't have:
    • Functions
    • DOM Nodes
    • Complicated objects (e.g. made by a class)
    • Event
    • ...

Events + ServiceWorker Code

  • Browser considers the event finished when all the handlers exit unless...
  • The event is an ExtendableEvent
  • Then we can use event.waitUntil(somePromise)
  • This prevents the thread from being killed by the browser

ServiceWorker Script

  • Can (and will!) be killed by the browser whenever it feels like it
  • Use event.waitUntil() to ask browser for more time
  • They are not always running!

ServiceWorker Lifecycle

  • Registered by page JS
  • Browser goes and gets the ServiceWorker JS
  • Browser installs the ServiceWorker JS
  • If there was a previous ServiceWorker JS with the same URL, it waits for the tabs to close
  • Browser removes the old version and activates the new version
  • New tabs use the new version of the service worker

ServiceWorker Lifecycle

  • Default: Allow all tabs to close before new version of the SW (and the App)
  • Optionally: Service worker can take over ASAP
    • self.skipWaiting() in install event handler
    • clients.claim() in activate event handler
    • Tell clients (tabs) there's a new version
      • Just reload
      • Popup for the user

ServiceWorker Script

  • Only runs when first registered and some events:
    • install - service worker is first installed
    • activate - service worker becomes the active service worker version
    • message - a tab (client) sent the serviceworker a message
    • fetch - page makes a fetch request
    • push... - Push API (Notifications)

Making it Offline

  • After the service worker is activated it can intercept all requests made by the app
  • HTML, JS, CSS, fetch(), everything
  • self.addEventListener("fetch", (event) => { ... });
  • event.respondWith(promise for response) to take over handling the request
  • return without calling respondWith to let the browser handle as normal

Making it Offline

  • Fetch handler
    • Examine Request and decide if it should be handled by SW
    • Look for valid data in the cache
    • Give a promise for the response if its in the cache
    • If its not in the cache, use normal fetch() to supply result
    • Optionally add result to cache

Cache Management

  • Once SW is installed: fill the cache
    • install event handler
  • Optionally we can also add other things to the cache later
const fillCache = async () => {
    const cache = await caches.open(cache_version);
    await cache.addAll(['./', 'index.html', 'somepage.html', 'somecode.js', 'somestyle.css', 'somepicture.avif']);
}

Cache Management

  • Cache is separated into multiple versions
  • Make sure to remove the old versions!
const cacheKeys = await caches.keys();
for (const cacheKey of cacheKeys) {
    if (cacheKey !== cache_version) {
        await caches.delete(cacheKey);
    }
}

Resources and References

  • Adobe Blog Progressive web apps
  • Native, React-native or PWA
  • What Is A Progressive Web App Youtube
  • mdn Progressive web apps
  • Service worker overview

License

Copyright 2014-2023 ⓒ Abram Hindle

Copyright 2019-2023 ⓒ Hazel Victoria Campbell and contributors

Creative Commons Licence
The textual components and original images of this slide deck are placed under the Creative Commons is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Other images used under fair use and copyright their copyright holders.

License


Copyright (C) 2019-2023 Hazel Victoria Campbell
Copyright (C) 2014-2023 Abram Hindle and contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN.

01234567890123456789012345678901234567890123456789012345678901234567890123456789