dojo dragon main logo

Progressive web applications

Progressive web apps (PWAs) are made up of a collection of technologies and patterns that improve the user experience and help create a more reliable and usable application. Mobile users in particular will see the application as more integrated into their device similar to an installed app.

The core of a progressive web app is made up of two technologies: Service workers and a manifest. Dojo's build command supports both of these through .dojorc with the pwa object.

Manifest

The manifest describes an application in a JSON file and provides details so it may be installed on a device's homescreen directly from the web.

.dojorc

{
    "build-app": {
        "pwa": {
            "manifest": {
                "name": "Todo MVC",
                "description": "A simple to-do application created with Dojo",
                "icons": [
                    { "src": "./favicon-16x16.png", "sizes": "16x16", "type": "image/png" },
                    { "src": "./favicon-32x32.png", "sizes": "32x32", "type": "image/png" },
                    { "src": "./favicon-48x48.png", "sizes": "48x48", "type": "image/png" },
                    { "src": "./favicon-256x256.png", "sizes": "256x256", "type": "image/png" }
                ]
            }
        }
    }
}

When a manifest is provided dojo build will inject the necessary <meta> tags in the applications index.html.

  • mobile-web-app-capable="yes": indicates to Chrome on Android that the application can be added to the user's homescreen.
  • apple-mobile-web-app-capable="yes": indicates to iOS devices that the application can be added to the user's homescreen.
  • apple-mobile-web-app-status-bar-style="default": indicates to iOS devices that the status bar should use the default appearance.
  • apple-touch-icon="{{icon}}": the equivalent of the manifests' icons since iOS does not currently read icons from the manifest. A separate meta tag is injected for each entry in the icons array.

Service worker

A service worker is a type of web worker that is able to intercept network requests, cache, and provide resources. Dojo's build command can automatically build fully-functional service worker that is activated on startup and complete with precaching and custom route handling from a configuration file.

For instance, we could write a configuration to create a simple service worker that cached all of the application bundles except the admin bundle and cached recent application images and articles.

.dojorc

{
    "build-app": {
        "pwa": {
            "serviceWorker": {
                "cachePrefix": "my-app",
                "excludeBundles": ["admin"],
                "routes": [
                    {
                        "urlPattern": ".*\\.(png|jpg|gif|svg)",
                        "strategy": "cacheFirst",
                        "cacheName": "my-app-images",
                        "expiration": { "maxEntries": 10, "maxAgeSeconds": 604800 }
                    },
                    {
                        "urlPattern": "http://my-app-url.com/api/articles",
                        "strategy": "cacheFirst",
                        "expiration": { "maxEntries": 25, "maxAgeSeconds": 86400 }
                    }
                ]
            }
        }
    }
}

ServiceWorker configuration

Under the hood, the ServicerWorkerPlugin from @dojo/webpack-contrib is used to generate the service worker, and all of its options are valid pwa.serviceWorker properties.

Property Type Optional Description
bundles string[] Yes An array of bundles to include in the precache. Defaults to all bundles.
cachePrefix string Yes The prefix to use for the runtime precache cache.
clientsClaim boolean Yes Whether the service worker should start controlling clients on activation. Defaults to false.
excludeBundles string[] Yes An array of bundles to include in the precache. Defaults to [].
importScripts string[] Yes An array of script paths that should be loaded within the service worker
precache object Yes An object of precache configuration options (see below)
routes object[] Yes An array of runtime caching config objects (see below)
skipWaiting boolean Yes Whether the service worker should skip the waiting lifecycle

Precaching

The precache option can take the following options to control precaching behavior:

Property Type Optional Description
baseDir string Yes The base directory to match include against.
ignore string[] Yes An array of glob pattern string matching files that should be ignored when generating the precache. Defaults to [ 'node_modules/**/*' ].
include string or string[] Yes A glob pattern string or an array of glob pattern strings matching files that should be included in the precache. Defaults to all files in the build pipeline.
index string Yes The index filename that should be checked if a request fails for a URL ending in /. Defaults to 'index.html'.
maxCacheSize number Yes The maximum size in bytes a file must not exceed to be added to the precache. Defaults to 2097152 (2 MB).
strict boolean Yes If true, then the build will fail if an include pattern matches a non-existent directory. Defaults to true.
symlinks boolean Yes Whether to follow symlinks when generating the precache. Defaults to true.

Runtime caching

In addition to precaching, strategies can be provided for specific routes to determine whether and how they can be cached. This routes option is an array of objects with the following properties:

Property Type Optional Description
urlPattern string No A pattern string (which will be converted a regular expression) that matches a specific route.
strategy string No The caching strategy (see below).
options object Yes An object of additional options, each detailed below.
cacheName string Yes The name of the cache to use for the route. Note that the cachePrefix is not prepended to the cache name. Defaults to the main runtime cache (${cachePrefix}-runtime-${domain}).
cacheableResponse object Yes Uses HTTP status codes and or headers to determine whether a response can be cached. This object has two optional properties: statuses and headers. statuses is an array of HTTP status codes that should be considered valid for the cache. headers is an object of HTTP header and value pairs; at least one header must match for the response to be considered valid. Defaults to { statuses: [ 200 ] } when the strategy is 'cacheFirst', and { statuses: [0, 200] } when the strategy is either networkFirst or staleWhileRevalidate.
expiration object Yes Controls how the cache is invalidated. This object has two optional properties. maxEntries is the number of responses that can be cached at any given time. Once this max is exceeded, the oldest entry is removed. maxAgeSeconds is the oldest a cached response can be in seconds before it gets removed.
networkTimeoutSeconds number Yes Used with the networkFirst strategy to specify how long in seconds to wait for a resource to load before falling back on the cache.

Four routing strategies are currently supported:

  • networkFirst attempts to load a resource over the network, falling back on the cache if the request fails or times out. This is a useful strategy for assets that either change frequently or may change frequently (i.e., are not versioned).
  • cacheFirst loads a resource from the cache unless it does not exist, in which case it is fetched over the network. This is best for resources that change infrequently or can be cached for a long time (e.g., versioned assets).
  • networkOnly forces the resource to always be retrieved over the network, and is useful for requests that have no offline equivalent.
  • staleWhileRevalidate requests resources from both the cache and the network simulaneously. The cache is updated with each successful network response. This strategy is best for resources that do not need to be continuously up-to-date, like user avatars. However, when fetching third-party resources that do not send CORS headers, it is not possible to read the contents of the response or verify the status code. As such, it is possible that a bad response could be cached. In such cases, the networkFirst strategy may be a better fit.