XF vs PWA: Exploring PWA Service Workers

PWA Getting Started

I am not finished with my work on the Xamarin Forms portion of the series yet, but I had to start some PWA work in the middle, and I thought it would be good to write about my first lessons in PWA land.

Service Workers

I think the biggest lesson I have learned so far as that the service worker is the most important component of a PWA, or the piece that separates a PWA from a regular responsive web app.
One of the really interesting things about a service worker is that it runs in the background of the browser itself and can run even if your app isn’t currently loaded. How cool is that? I am looking forward to trying that one out.

The service worker has a number of capabilities.
Cache Content: It can cache your content, giving you the ability to have an app presence even when you are offline.
Push Notifications: It can subscribe to a push service and receive messages. The service worker can take these messages and update the state of the app or display a notification of some kind.
Notifications: It can send notifications to the user using the host operating system notification mechanism.
Channel Messaging: It can communicate with other service workers and the host application.

The first thing I am going to do is make sure that my app shows up even if it is offline. Even if your app needs to be online to do anything useful, it is still a nicer experience for the user if your app loads and presents an explanation of what is happening.

Setting Things Up

First we will make a simple web page. There won’t be much of anything going on. We will just pull in some bootstrap resources from their CDN and add in some of our own styles and javascript.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <title>service workers</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
    <link rel="stylesheet" href="my.css" >
</head>
<body>
    <h1>hello from pwa land</h1>

    <a href="https://code.jquery.com/jquery-3.3.1.slim.min.js">https://code.jquery.com/jquery-3.3.1.slim.min.js</a>
    <a href="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js">https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js</a>
    <a href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js">https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js</a>
    <a href="http://my.js">http://my.js</a>
</body>
</html>

and some custom css

body {
    margin: 20px;
}

javascript

console.log('loaded');

For serving up your content, I am using a simple http web server called ‘http-server’. You do require node which you can download from here.

You can install the http-server using the following command:
npm install http-server -g
The -g installs it so that it installed globally and you can use it anywhere.

At the command line, change to your content directory and execute the following command:
http-server -p 8080 -a localhost -c 0
This starts up an http server servicing the content in the current direcctory from http://localhost:8080.
You should be able to open your web browser and go the above address to http://localhost:8080/index.html and serve up the page from above.

Create our Service Worker file

It’s important that we create this file at the root of our web app. Service workers have scope where they apply to the current folder and below. They can’t travel up. I should also note that service workers only work over https connections, localhost being the only exception to that. If you need an ssl certificate, letsencrypt.org is a place where you can get free SSL certificates for your site.

// service worker file

Our file will be empty, but that’s ok, we just need to have a file to handle the registration.

Register our Service Worker

The last thing we do in our index.html file is perform the registration of our service worker. In a script tag just before the closing body tag, add the following code.

    <!-- our code from above -->



        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.register('service-worker.js')
            .then(function(registration) {
                console.log('Service worker registration successful with scope: ', registration.scope);
            })
            .catch(function(error) {
                console.log('Service worker registration failed: ', error);
            });
        }


</body>

This is important. We need to check if our browser supports service workers and that is checked from the navigator object in javascript. If it is, we can do the actual registration. If it isn’t, we just ignore it and keep our app as is. That is a key point to PWA’s: start at the basic functionality and then add on the capabilities as the host browser supports them.

Filling out the Service Worker

Alright, we have a page that is pulling in various resources, some that we own, some via external. Let’s take a look at how we can make the app function offline.
First we need to make a list of the resources that we want to cache. This is pretty simple. Obviously we want the home page, our js and css files, and our external bootstrap and jquery files.

// service-worker.js


(function () {
    'use strict';

    // our list of cache'd items. We can fill this in more later if we want.
    var filesToCache = [
        '.',
        'index.html',
        'my.css',
        'my.js',
        'https://code.jquery.com/jquery-3.3.1.slim.min.js',
        'https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js',
        'https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js'
    ];

    var staticCacheShell = 'app-shell';

})();

Next we are going to register an event handler for when the service worker is installed. The purpose of this is to setup the cache.

    // this is just below the var staticCacheShell declaration and before the closing })();

    self.addEventListener('install', function (event) {
        console.log('installing sw and caching statics');
        event.waitUntil(
            caches.open(staticCacheShell)
                .then(function (cache) {
                    console.log('adding static shel cache');
                    cache.addAll(filesToCache);
                    return;
                }).catch(function(error) {
                    console.log(error);
                })
        );
    });

What is happening above is that we are going to create the cache using our key name and then instruct the cache to add all of our pages into it. Pretty straightforward. But, how do we get pages served from the cache when we are offline? That is the next thing we are going to add to our service worker.
We add another event handler intercepting the fetch event. This sits between your app and the internet and allows you to manipulate the request. In our case, we are going to check if the request is in the cache, and if it is, return it back to your app instead of going out on to the internet. If the network request returns a page not found (404) error, we can serve up the app’s not found page (remember to add it to the list of pages). If the request is returned, we make a clone of the response and stuff it in the cache. Finally, if there is any other error, return the app’s app offline page (remember to stick that in the cache as well).

    self.addEventListener('fetch', function(event) {
        console.log('fetch event for ', event.request.url);
        event.respondWith(
            caches.match(event.request).then(function(response) {
                if (response) {
                    console.log('found ', event.request.url, ' in cache');
                    return response;
                }

                console.log('network request for ', event.request.url);
                return fetch(event.request).then(function(response) {
                    if (response.status === 404) {
                        return caches.match('page404.html');
                    }

                    return caches.open(staticCacheShell).then(function(cache) {
                        if (event.request.url.indexOf('test') < 0) {
                            cache.put(event.request.url, response.clone());
                        }

                        return response;
                    });
                });
            }).catch(function(error) {
                console.log('Error ', error);
                return caches.match('pageoffline.html');
            })
        );
    });

Now let’s run our app. I used the http-server from above and loaded the index.html in Edge. I just had my windows update and Edge was upgraded again. In there you can see that the developer tools are showing that the service worker has been registered.
pwagettingstartedservicework

Next we can look at the cache and we can see that all the pages that registered have been added to the cache.
pwagettingstartedcache

I didn’t see a way in the Edge developer tools to simulate taking the page offline, so I went over to Chrome and loaded up my page. I opened up the developer tools, went to the network tab and set the mode to be offline.

pwagettingstartedoffline
If we refresh the page from here, it will still show up, even though there is no network access for the browser. If we try to load some random page, we will get the pageoffline page.

Conclusion

So that is the quick and dirty for getting started with PWA’s, service workers and offline mode. It’s pretty cool tech and I am looking forward to exploring it some more. You can explore this code in Github.

Xamarin Forms vs PWA

I have been pretty interested in PWA’s lately. In case you don’t know, PWA is a progressive web app. The progressive part is about how the app reacts with more or less capability in the host browser. It also gives the web app some native capabilities such as notifications so that the app appears almost like a native app.

As a bit of a fun side project, I thought I would take a transit status app that I have in UWP and rewrite for Xamarin Forms across Android, iOS and UWP. Then I would rewrite it as a PWA. Fun!

I think this one will take a while to complete though. We shall see. Hopefully what comes out of this are some cool things I learn with each approach and some informative blog posts. On the Xamarin Forms side, I am going to use the latest release of Prism for Xamarin Forms. The Prism team has been doing some amazing work and have recently set themselves up on Patreon for funding.  I signed up for funding them, if you use their libraries, please consider doing the same.

On the PWA side, I will use Angular as a framework. I know some people don’t like any frameworks (too much weight), but I think it is worth it, at least for my target. Perhaps if your target is a local app in a region that doesn’t have very good connectivity, Angular or React is not your choice. For this app, it is only useful in Metro Vancouver and the service is good across multiple providers. But that is your choice to make. I will also find a template that I can use that is ready for my business logic. I don’t have an eye for colors and site design, so I prefer buying a template whenever I make any kind of web app.

At the end of this, I hope that I have learned lots of new things and that I have a number of functioning apps. If they turn out ok, I will submit them to the store for publishing.

Time to start work!

p.s. this will take a while to complete!