Truly progressive WebVR apps are available offline!

Posted on Sun 19 February 2017 in Virtual Reality

I've been dabbling with the A-Frame framework for creating WebVR experiences for the past couple of months, ever since Patrick Trottier gave a lightning talk at the GDG Sudbury DevFest in November and a hands-on session with AFrame in January. The @AFrameVR Twitter feed regularly highlights cool new WebVR apps, and one that caught my attention was ForestVR - a peaceful forest scene with birds tweeting in the distance. "How nice would it be", I thought, "if I could just escape into that little scene wherever I am, without worrying about connectivity or how long it would take to download?"

Then I realized that WebVR apps are a great use case for Progressive Web App (PWA) techniques that allow web apps to be as fast, reliable, and engaging as native Android apps. With the source code for ForestVR at my disposal, I set out to add offline support. And it turned out to be surprisingly easy to make this work on Android in both the Firefox and Chrome browsers.

If you just want to see the required changes for this specific example, you can find the relevant two commits at the tip of my branch. The live demo is at https://stuff.coffeecode.net/forestvr/.

Firefox menu for "Add to Home Screen"

ForestVR with "Add to Home Screen" menu on Firefox for Android 51.0.3

Chrome "Add" prompt

ForestVR with "Add" prompt on Chrome for Android 57

In the following sections I've written an overview of the steps you have to take to turn your web app into a PWA:

Describe your app with a Web App Manifest

ForestVR already had a working Web App Manifest (Mozilla docs / Google docs), a simple JSON file that defines metadata about your web app such as the app name and icon to use when it is added to your home screen, the URL to launch, the splash screen to show when it is loading, and other elements that enable it to integrate with the Android environment.

The web app manifest for ForestVR is named manifest.json and contains the following code:

{
  "name": "Forest VR",
  "icons": [
    {
      "src": "./assets/images/icons/android-chrome-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    }
  ],
  "theme_color": "#ffffff",
  "background_color": "#ffffff",
  "start_url": "./index.html",
  "display": "standalone",
  "orientation": "landscape"
}

You associate the manifest with your web app through a simple <link> element in the <head> of your HTML:

<link rel="manifest" href="manifest.json">

Create a service worker to handle offline requests

A service worker is a special chunk of JavaScript that runs independently from a given web page, and can perform special tasks such as intercepting and changing browser fetch requests, sending notifications, and synchronizing data in the background (Google docs / Mozilla docs). While implementing the required networking code for offline support would be painstaking, bug-prone work, Google has fortunately made the sw-precache node module available to support generating a service worker from a simple configuration file and any static files in your deployment directory.

The configuration I added to the existing gulp build system gulpfile uses runtime caching for assets that are hosted at a different hostname or, in the case of the background soundtrack, is not essential for the experience at launch and can thus be loaded and cached after the main experience has been prepared. The staticFileGlobs list, on the other hand, defines all of the assets that must be cached before the app can launch.

swConfig = {
  cacheId: packageJson.name,
  runtimeCaching: [{
    urlPattern: /^https:\/\/cdn\.rawgit\.com\//,
    handler: 'cacheFirst'
  },{
    urlPattern: /^https:\/\/aframe\.io\//,
    handler: 'cacheFirst'
  },{
    urlPattern: /\/assets\/sounds\//,
    handler: 'cacheFirst'
  }],
  staticFileGlobs: [
    'assets/fonts/fabrica-webfont.woff',
    'assets/images/bg.jpg',
    'assets/images/tree_icon.png',
    'assets/models/**.dae',
    'bundle.css',
    'bundle.js',
    'index.html'
  ]
}

I defined the configuration inside a new writeServiceWorkerFile() function so that I could add it as a build task to the gulpfile:

function writeServiceWorkerFile(callback) {
  swConfig = {...}
  swPrecache.write('service-worker.js', swConfig, callback);
}

In that gulp task, I declared the 'scripts' and 'styles' tasks as prerequisites for generating the service worker, as those tasks generate the bundle.js and bundle.css files. If the files are not present in the build directory when sw-precache runs, then it will simply ignore their corresponding entry in the configuration, and they will not be available for offline use.

gulp.task('generate-service-worker', ['scripts', 'styles'], function(callback) {
  writeServiceWorkerFile(callback);
});

I added the generate-service-worker task to the deploy task so that the service worker will be generated every time we build the app:

gulp.task('deploy',['scripts','styles','generate-service-worker'])

Register the service worker

Just like the Web App Manifest, you need to register your service worker--but it's a little more complex. I chose Google's boilerplate service worker registration script because it contains self-documenting comments and hooks for adding more interactivity, and added it in a <script> element in the <head> of the HTML page.

Host your app with HTTPS

PWAs--specifically service workers--require the web app to be hosted on an HTTPS-enabled site due to the potential for mischief that service workers could cause if replaced by a man-in-the-middle attack that would be trivial with a non-secure site. Fortunately, my personal VPS already runs HTTPS thanks to free TLS certificates generated by Let's Encrypt.

Check for success with Lighthouse

Google has made Lighthouse, their PWA auditing tool, available as both a command-line oriented node module and a Chrome extension for grading the quality of your efforts. It runs a separate instance of Chrome to check for offline support, responsiveness, and many other required and optional attributes and generates succinct reports with helpful links for more information on any less-than-stellar results you might receive.

Check for success with your mobile web browser

Once you have satisfied Lighthouse's minimum requirements, load the URL in Firefox or Chrome on Android and try adding it to your home screen.

  • In Firefox, you will find the Add to Home Screen option in the browser menu under the Page entry.
  • In Chrome, the Add button (Chrome 57) or Add to Home Screen button (Chrome 56) will appear at the bottom of the page when you have visited it a few times over a span of five minutes or more; a corresponding entry may also appear in your browser menu.

Put your phone in airplane mode and launch the app from your shiny new home screen button. If everything has gone well, it should launch and run successfully even though you have no network connection at all!

Conclusion

As a relative newbie to node projects, I spent most of my time in figuring out how to integrate the sw-precache build steps nicely into the existing gulp build, and in making the app relocatable on different hosts and paths for testing purposes. The actual service worker itself was straightforward. While I used ForestVR as my proof of concept, the process should be similar for turning any other WebVR app into a Progressive WebVR App. I look forward to seeing broader adoption of this approach for a better WebVR experience on mobile!

As an aside for my friends in the library world, I plan to apply the same principles to making the My Account portion of the Evergreen library catalogue a PWA in time for the 2017 Evergreen International Conference. Here's hoping more library software creators are thinking about improving their mobile experience as well...