How do make a POST request to a server using Background Sync and Service Workers

How do make a POST request to a server using Background Sync and Service Workers

This guide will help you create a web page that can function offline using POST requests. Making an offline web app isn't that hard and allows you to update your web apps offline. The main challenge is the Service Workers doesn't allow POST requests yet. This guide will allow you to store it and send it when the user gets signal again. How cool is that. You can also follow it here Github Page.Firstly Background Sync is only working on Chrome Browsers currently (Feb 2019) follow the progress here.

Start

Create a folder that can be accessed using a URL. Using XAMPP or something similar. In the root folder create an app.html file. In here add a button and some JS to run it.
<!DOCTYPE html>
<html>
    <head>
        <title>Your Page</title>
        
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.0/css/bootstrap.min.css">
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.0/js/bootstrap.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/7.33.1/sweetalert2.all.js"></script>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/7.33.1/sweetalert2.css" />
        <script type="text/javascript" src="idb.js"></script>
        <script type="text/javascript" src="store.js"></script>

        <script>
            if('serviceWorker' in navigator) {
            navigator.serviceWorker.register('sw.js', { scope: '' })
                .then(function(registration) {
                
                });
            navigator.serviceWorker.ready.then(function(registration) {           
                
            });      
        } else {
        }
    </script>
    </head>
    <body>
        <h1>Button</h1>
        <button class="imageButton">Add Image</button>
    </body>

    <script>
        $(document).ready(function () {
            $('.imageButton').on({
                'touchstart': function () {
                    var data = this;
                    setTimeout(function () { openImageTaker(data); }, 500);
                }
            });
        });

        function openImageTaker(self) {
            (async function getImage() {
                const { value: file } = await swal({
                    title: 'Please take a picture of the damage',
                    input: 'file',
                    inputAttributes: {
                        'accept': 'image/*',
                        'html': "<img src='' id='imageDamagePreview'>",
                        'aria-label': 'Damage Area'
                    }
                })
                if (file) {
                    var fileBase64 = '';
                    const reader = new FileReader
                    reader.onload = (e) => {
                        fileBase64 = e.target.result;
                        //Check if the document has already loaded. If not then add a service wroked
                        if (document.readyState !== "loading") {
                            runTheImageToTheJobServiceWorker(fileBase64);
                        } else {
                            document.addEventListener("DOMContentLoaded",
                                runTheImageToTheJobServiceWorker(fileBase64));
                        }
                    }
                    reader.readAsDataURL(file)
                }
            })()
        }





        function runTheImageToTheJobServiceWorker(fileBase64) 
        {
            //Check if we can use service workers
            if ('serviceWorker' in navigator) {
                //Register the sw file
                navigator.serviceWorker.register('sw.js').then(function (reg) {
                    if ('sync' in reg) {
                        //Create an array of the Data to be posted later
                        var message = {
                            "type": "storeTheDamage",
                            "id": 1234,
                            "image": fileBase64,
                        };

                        //Use the store file (store.js) to ssave the data
                        store.image('readwrite').then(function (image) {
                            return image.put(message);
                        }).then(function () {
                            return reg.sync.register('image');
                        }).catch(function (err) {
                            // something went wrong with the database or the sync registration, log and submit the form
                            console.error(err);
                            sendTheImageToTheJob(fileBase64);
                        });
                    };
                }).catch(function (err) {
                    sendTheImageToTheJob(fileBase64);
                    console.error(err); // the Service Worker didn't install correctly
                });
            } else {
                sendTheImageToTheJob(fileBase64);
            }
        }



        function sendTheImageToTheJob(fileBase64) {
            //Use this if it fails to send it direct
            $.post('app.html', {
                "type": "storeTheDamage",
                "id": 1234,
                "image": fileBase64,
            }, function (data) {

            });
        }

    </script>
</html>

Create a Service Worker

Download idbs and add to the folder. Create a file called sw.js in the same folder. Copy this code in.
Var version = 'v1::';
ImportScripts('idb.js');
ImportScripts('store.js');

self.addEventListener('sync', function(event) {
    //Image Upload
    event.waitUntil(
        store.image('readonly')
        .then(function(image) {
            return image.getAll();
        })
        .then(function(uploads) {
            //Send the message out to the POST
            return Promise.all(uploads.map(function(upload) {
                return fetch('app.html', {
                    method: 'POST',
                    body: JSON.stringify(upload),
                    headers: {
                    'Content-Type': 'application/json'
                    }
                })
                .then(function(response) {  
                    console.log(response);
                    //Get the info from the Store
                    return store.image('readwrite').then(function(image) {
                        //Delete the job
                        image.delete(upload.id);
                    });
                })
            }))
        })
        .catch(function(err) { 
            //console.error(err); 
        })
    ) 
});

self.addEventListener("install", function(event) {
    //console.log('WORKER: install event in progress.');
    event.waitUntil(
      /* The caches built-in is a promise-based API that helps you cache responses,
         as well as finding and deleting them.
      */
      caches
        /* You can open a cache by name, and this method returns a promise. We use
           a versioned cache name here so that we can remove old cache entries in
           one fell swoop later, when phasing out an older service worker.
        */
        .open(version + 'fundamentals')
        .then(function(cache) {
          /* After the cache is opened, we can fill it with the offline fundamentals.
             The method below will add all resources we've indicated to the cache,
             after making HTTP requests for each of them.
          */
          return cache.addAll([
            '',
            'app.html'
          ]);
        })
        .then(function() {
          //console.log('WORKER: install completed');
        })
    );
});

self.addEventListener("fetch", function(event) {
    //console.log('WORKER: fetch event in progress.');
  
    /* We should only cache GET requests, and deal with the rest of method in the
       client-side, by handling failed POST,PUT,PATCH,etc. requests.
    */
    if (event.request.method !== 'GET') {
      /* If we don't block the event as shown below, then the request will go to
         the network as usual.
      */
      //console.log('WORKER: fetch event ignored.', event.request.method, event.request.url);
      return;
    }
    /* Similar to event.waitUntil in that it blocks the fetch event on a promise.
       Fulfillment result will be used as the response, and rejection will end in a
       HTTP response indicating failure.
    */
    event.respondWith(
      caches
        /* This method returns a promise that resolves to a cache entry matching
           the request. Once the promise is settled, we can then provide a response
           to the fetch request.
        */
        .match(event.request)
        .then(function(cached) {
          /* Even if the response is in our cache, we go to the network as well.
             This pattern is known for producing "eventually fresh" responses,
             where we return cached responses immediately, and meanwhile pull
             a network response and store that in the cache.
             Read more:
             https://ponyfoo.com/articles/progressive-networking-serviceworker
          */
          var networked = fetch(event.request)
            // We handle the network request with success and failure scenarios.
            .then(fetchedFromNetwork, unableToResolve)
            // We should catch errors on the fetchedFromNetwork handler as well.
            .catch(unableToResolve);
  
          /* We return the cached response immediately if there is one, and fall
             back to waiting on the network as usual.
          */
          //console.log('WORKER: fetch event', cached ? '(cached)' : '(network)', event.request.url);
          return cached || networked;
  
          function fetchedFromNetwork(response) {
            /* We copy the response before replying to the network request.
               This is the response that will be stored on the ServiceWorker cache.
            */
            var cacheCopy = response.clone();
  
            //console.log('WORKER: fetch response from network.', event.request.url);
  
            caches
              // We open a cache to store the response for this request.
              .open(version + 'pages')
              .then(function add(cache) {
                /* We store the response for this request. It'll later become
                   available to caches.match(event.request) calls, when looking
                   for cached responses.
                */
                cache.put(event.request, cacheCopy);
              })
              .then(function() {
                //console.log('WORKER: fetch response stored in cache.', event.request.url);
              });
  
            // Return the response so that the promise is settled in fulfillment.
            return response;
          }
  
          /* When this method is called, it means we were unable to produce a response
             from either the cache or the network. This is our opportunity to produce
             a meaningful response even when all else fails. It's the last chance, so
             you probably want to display a "Service Unavailable" view or a generic
             error response.
          */
          function unableToResolve () {
            /* There's a couple of things we can do here.
               - Test the Accept header and then return one of the `offlineFundamentals`
                 e.g: `return caches.match('/some/cached/image.png')`
               - You should also consider the origin. It's easier to decide what
                 "unavailable" means for requests against your origins than for requests
                 against a third party, such as an ad provider
               - Generate a Response programmaticaly, as shown below, and return that
            */
  
            //console.log('WORKER: fetch request failed in both cache and network.');
  
            /* Here we're creating a response programmatically. The first parameter is the
               response body, and the second one defines the options for the response.
            */
            return new Response('<h1>Service Unavailable</h1>', {
              status: 503,
              statusText: 'Service Unavailable',
              headers: new Headers({
                'Content-Type': 'text/html'
              })
            });
          }
        })
    );
  });

  self.addEventListener("activate", function(event) {
    /* Just like with the install event, event.waitUntil blocks activate on a promise.
       Activation will fail unless the promise is fulfilled.
    */
    //console.log('WORKER: activate event in progress.');
  
    event.waitUntil(
      caches
        /* This method returns a promise which will resolve to an array of available
           cache keys.
        */
        .keys()
        .then(function (keys) {
          // We return a promise that settles when all outdated caches are deleted.
          return Promise.all(
            keys
              .filter(function (key) {
                // Filter by keys that don't start with the latest version prefix.
                return !key.startsWith(version);
              })
              .map(function (key) {
                /* Return a promise that's fulfilled
                   when each outdated cache is deleted.
                */
                return caches.delete(key);
              })
          );
        })
        .then(function() {
          //console.log('WORKER: activate completed.');
        })
    );
  });

Store the data

Create a file called store.js and add this code
var store = {
    dbImages: null,

    //Used for sending images
    initImages: function(name, fileStorageName) {
        if (store.dbImages) { return Promise.resolve(store.dbImages); }
        return idb.open(name, 1, function(upgradeDb) {
          upgradeDb.createObjectStore(fileStorageName, { autoIncrement : true, keyPath: 'id' });
        }).then(function(dbImages) {
          return store.dbImages = dbImages;
        });
      },

    image: function(mode) {
        return store.initImages('uploads', 'image').then(function(db) {
          return db.transaction('image', mode).objectStore('image');
        })
    }
}

App Shell

Making an app shell that allows for app-style gestures and navigation's you need to add a manifest, a great site is here that will build it for you. You need to make sure
"display": "standalone",
is set. You should now, on iPhone be able to load the page with network. Add to Home Screen, put in airplane mode and the app will launch in a non browser window. Here is a check list of what should be used.
  • Site is served over HTTPS
  • Pages are responsive on tablets & mobile devices
  • Metadata provided for Add to Home screen
  • First load fast even on 3G
  • Site works cross-browser
  • Page transitions don't feel like they block on the network
  • Each page has a URL

Testing

Load the page i.e. localhost. Open developer tools and in the tab hit Service Workers and you should see your application. At the top of the list there is a button called offline tick this and refresh the page and it should all be visible still. Cool. Turn the app back on line. Under the storage headings look for Application and on the left find the IndexedDB. Click the add image button on the webpage and upload an image. You should now see a new drop down in the IndexedDB called uploads with a sub heading image. Click on this and you should see all the data form the request. Now click on the Network tab and at the bottom you should see app.html with a 200 request for the send of the image. Now at the top of the Network page click the offline tick box and refresh the page. Firstly you should see the page as normal. Now add an image. You will notice there is nothing appearing in the Network tab. Now the fun bit, untick the offline tick box and you should see the app.html request happen. Amazing. You now have an offline app.

Categories: Posts