ASP.NET Core 2.2 - PWA Service Worker


Ken Haggerty
Created 05/18/2019 - Updated 08/29/2019 15:24

This article will demonstrate creating an editable service worker using the default service worker provided by the WebEssentials.AspNetCore.PWA NuGet package as a base. I will assume you have created a new ASP.NET Core 2.2 Razor Pages project and have successfully installed the WebEssentials.AspNetCore.PWA package. I won't use Identity or Individual User Accounts.

The default service worker provided by the WebEssentials.AspNetCore.PWA NuGet package implements listeners for the install, activate and fetch events. If you want to extend the service worker, a copy of the default service worker is a great place to start. Build, run and test, Open Chrome DevTools, on the Application tab, select Service Workers on the left. Click the link for the serviceworker to view the code which WebEssentials.AspNetCore.PWA dynamically created.

DevTools Default Service Worker

The service worker code should execute from the wwwroot folder to provide full scope for the site. Give the file a unique name to differentiate it from the default service worker. Add a new file named pwa-serviceworker.js to the wwwroot folder and copy the default service worker code.

Default Service Worker Code
New pwa-serviceworker.js file in the wwwroot folder:
(function () {
    'use strict';

    // Update 'version' if you need to refresh the cache
    var version = 'v1.0::CacheFirstSafe';
    var offlineUrl = "/offline.html";

    // Store core files in a cache (including a page to display when offline)
    function updateStaticCache() {
        return caches.open(version)
            .then(function (cache) {
                return cache.addAll([
                    offlineUrl,

                ]);
            });
    }

    function addToCache(request, response) {
        if (!response.ok && response.type !== 'opaque')
            return;

        var copy = response.clone();
        caches.open(version)
            .then(function (cache) {
                cache.put(request, copy);
            });
    }

    function serveOfflineImage(request) {
        if (request.headers.get('Accept').indexOf('image') !== -1) {
            return new Response('<svg role="img" aria-labelledby="offline-title" viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg"><title id="offline-title">Offline</title><g fill="none" fill-rule="evenodd"><path fill="#D8D8D8" d="M0 0h400v300H0z"/><text fill="#9B9B9B" font-family="Helvetica Neue,Arial,Helvetica,sans-serif" font-size="72" font-weight="bold"><tspan x="93" y="172">offline</tspan></text></g></svg>', { headers: { 'Content-Type': 'image/svg+xml' } });
        }
    }

    self.addEventListener('install', function (event) {
        event.waitUntil(updateStaticCache());
    });

    self.addEventListener('activate', function (event) {
        event.waitUntil(
            caches.keys()
                .then(function (keys) {
                    // Remove caches whose name is no longer valid
                    return Promise.all(keys
                        .filter(function (key) {
                            return key.indexOf(version) !== 0;
                        })
                        .map(function (key) {
                            return caches.delete(key);
                        })
                    );
                })
        );
    });

    self.addEventListener('fetch', function (event) {
        var request = event.request;

        // Always fetch non-GET requests from the network
        if (request.method !== 'GET' || request.url.match(/\/browserLink/ig)) {
            event.respondWith(
                fetch(request)
                    .catch(function () {
                        return caches.match(offlineUrl);
                    })
            );
            return;
        }

        // For HTML requests, try the network first, fall back to the cache, finally the offline page
        if (request.headers.get('Accept').indexOf('text/html') !== -1) {
            event.respondWith(
                fetch(request)
                    .then(function (response) {
                        // Stash a copy of this page in the cache
                        addToCache(request, response);
                        return response;
                    })
                    .catch(function () {
                        return caches.match(request)
                            .then(function (response) {
                                return response || caches.match(offlineUrl);
                            });
                    })
            );
            return;
        }

        // cache first for fingerprinted resources
        if (request.url.match(/(\?|&)v=/ig)) {
            event.respondWith(
                caches.match(request)
                    .then(function (response) {
                        return response || fetch(request)
                            .then(function (response) {
                                addToCache(request, response);
                                return response || serveOfflineImage(request);
                            })
                            .catch(function () {
                                return serveOfflineImage(request);
                            });
                    })
            );

            return;
        }

        // network first for non-fingerprinted resources
        event.respondWith(
            fetch(request)
                .then(function (response) {
                    // Stash a copy of this page in the cache
                    addToCache(request, response);
                    return response;
                })
                .catch(function () {
                    return caches.match(request)
                        .then(function (response) {
                            return response || serveOfflineImage(request);
                        })
                        .catch(function () {
                            return serveOfflineImage(request);
                        });
                })
        );
    });

})();

Next, you need to configure WebEssentials.AspNetCore.PWA to not register the default service worker.

Edit Startup.cs > ConfigureServices, add the RegisterServiceWorker PwaOptions to services.AddProgressiveWebApp:
services.AddProgressiveWebApp(new PwaOptions()
{
    RegisterServiceWorker = false
});

Then you can register the editable service worker from site.js.

Add to site.js in the wwwroot/js folder:
document.addEventListener('DOMContentLoaded', function () {

    if ('serviceWorker' in navigator) {
        console.log('Service Worker is supported');
        navigator.serviceWorker.register('/pwa-serviceworker.js')
            .then(function (swReg) {
                console.log('Service Worker is registered from site.js', swReg);
            })
            .catch(function (error) {
                console.error('Service Worker Error from site.js', error);
            });
    }
    else {
        console.error('Service Worker Not Supported');
    }

}, false);

Build, run and test. Open Chrome DevTools, on the Application tab, select Service Workers on the left. Verify the new service worker is registered.

DevTools PWA Service Worker
Update 08/29/2019

Moved update notices towards the bottom of the article.

Update 07/28/2019

Added spaces to the link text in the article references. Fixes a responsive width issue on small screens.

Update 05/25/2019

The WebEssentials.AspNetCore.PWA NuGet package provides a unique serviceworker.js file based on the caching strategy. This article references the default CacheFirstSafe strategy. See Caching strategies. WebEssentials .AspNetCore .ServiceWorker #caching-strategies


Article Tags:

JavaScript PWA

1 Following


Comment Count = 0

Please log in to comment or follow.

Login Register