Have you heard about Service Worker? I believe this new JavaScript API has the potential to transform the way users interact with the web and how web developers construct websites. Though still in development, Service Worker is already landing in modern browsers.

So far, there hasn't been a good story for adding Service Worker to Rails. Until now!

There's a new Ruby gem, serviceworker-rails, to make it easier to integrate Service Worker with the Rails asset pipeline. To understand why Rails developers might want to use this, let's take a step back.

A brief intro

In its plainest form, service workers are just JavaScript running in a separate thread outside the context of a web page, like any other web worker.

But, service workers are special; they can act as client-side proxies. This means they can hook into the request/response cycle on the user's machine.

Hooking in to the request/response cycle on the client-side means we can improve the user experience in ways that weren't possible (or much more difficult) previously. We could render HTML from a local cache while waiting for a response from the network or we could display another friendly page altogether when the network is offline. With service workers, we'll be able to pre-fetch and sync data in the background, push activity notifications to users and even let them know when new releases have been deployed.

I've been playing with Service Worker a bit lately. Now that you've visited my site, your browser has cached the data for my offline page, so if you lost your network connection, you'd at least see a friendly message instead of the dreaded Chrome dinosaur.

Go ahead and take a look at the source code for the rossta.net service worker to see how I did it. I'm still learning about Service Worker - is it really new after all - so I'm sure there's lots of ways I could improve it!

Let's talk Rails

Next I wondered how I'd add a Service Worker to a Rails application. I'd expect Rails developers would want to be able to develop and deploy their service workers like any other JavaScript assets using the Rails asset pipeline. Not so fast though.

As it turns out, to use Service Workers on Rails, we want some, but not all, of the Rails asset pipeline.

The Rails asset pipeline makes a number of assumptions about what's best for deploying JavaScript, including asset digest fingerprints and long-lived cache headers - mostly to increase "cacheability". Rails also assumes a single parent directory, /public/assets, to make it easier to look up the file path for a given asset.

Service worker assets must play by different rules. Consider these behaviors:

  • Service workers may only be active from within the scope from which they are served. So if you try to register a service worker from a Rails asset pipeline path, like /assets/serviceworker-abcd1234.js, it will only be able to interact with requests and responses within /assets/**. This is not what we want.

  • MDN states browsers check for updated service worker scripts in the background every 24 hours (possibly less). Rails developers wouldn't be able to take advantage of this feature since the fingerprint strategy means assets at a given url are immutable. Beside fingerprintings, the Cache-Control headers used for static files served from Rails also work against browser's treatment of service workers.

**There is an early proposal to use the Service-Worker-Allowed header to change scopes.

What to do?

For now, Rails developers need to work around best practices in the Rails asset pipeline to use service workers.

One approach would be to just place service worker scripts in /public. That could work, but it could mean foregoing the asset pipeline altogether. We lose bundling, transpilation, testing and other features we do want. You could use the pipeline but would then need to add steps to the build process to copy precompiled service workers to the correct paths. In this case, you may want toaugment your web server configuration to change Cache-Control headers for those selected service worker scripts - this may not be possible in certain environments.

Given the constraints around scoping, you could create special controller actions to mount service workers at arbitrary routes. Rails also gives you the ability to set custom headers on controller actions so that's another benefit. From there, you either write your JavaScript in a template where you may lose the advantage of the asset pipeline or expose the contents of a precompiled asset from within the controller.

I like this last option up until the point where a standard Rails controller adds a lot of overhead, e.g. parameter parsing, session and cookie management, CSRF protection, that isn't needed for serving static files. From there, you can drop down to a ActionController::Metal subclass and figure out which extensions to pull in... or put this in a Rack middleware!

Using serviceworker-rails

This is what I've done with serviceworker-rails. It inserts a middleware into the Rails stack that acts as a separate router for service worker scripts. In development, you can edit and recompile your service workers on the fly as with any other asset in the pipeline. In production, the service worker endpoints map to the precompiled asset in /public/assets.

Once the gem is added to your Gemfile, you can add a Rails initializer to set up the service worker middleware router:

# config/initializers/serviceworker.rb
Rails.application.configure do
  config.serviceworker.routes.draw do
    match "/serviceworker.js" => "path/to/precompiled/serviceworker"

By default, the middleware sets the Cache-Control header to avoid aggressive caching. It also gives you the ability to customize headers as desired.

match "/serviceworker.js" => "app/serviceworker", headers: { "X-Custom-Header" => "foobar" }

Use globbing or named parameters in the service worker paths to interpolate asset names.

match "/*segments/serviceworker.js" => "%{segments}/serviceworker"
match "/project/:id/serviceworker.js" => "project/%{id}/serviceworker"

Check out the project README for more info on how to set up and configure the middleware for your Rails app.

Though the project is still young, you can see serviceworker-rails in action in the Service Workers on Rails Sandbox. Inspired by Mozilla's Service Workers Cookbook, it serves as good place to experiment with Service Workers on Rails in an open source setting. Try using the site in Chrome Canary with the advanced service worker debugging tools to play around. I've added just a few examples so far but am interested to explore further with various caching strategies, push notifications, and eventually background sync to name a few.

What do you think of this approach?

Interested in contributing? Fork the serviceworker-rails gem or the Service Workers on Rails Sandbox to get started.

Did you like this post? Do me a favor: share it on Twitter, follow me - @rossta, and sign up for my newsletter. Thanks!

Part of the Service Worker series. Published on May 3, 2016