• Stars
    star
    575
  • Rank 77,622 (Top 2 %)
  • Language
    Ruby
  • License
    MIT License
  • Created over 8 years ago
  • Updated almost 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Use Service Worker with the Rails asset pipeline

ServiceWorker::Rails

Build Status Code Climate

Turn your Rails app into a Progressive Web App. Use Service Worker with the Rails asset pipeline or Webpacker

Why?

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.

We want Sprockets or Webpacker to compile service worker JavaScript from ES6/7, CoffeeScript, ERB, etc. but must remove the caching and scoping mechanisms offered by Rails defaults. This is where serviceworker-rails comes in.

Check out the blog post for more background.

Demo

See various examples of using Service Workers in the demo Rails app, Service Worker Rails Sandbox. The source code is also on GitHub.

Features

  • Maps service worker endpoints to Rails assets
  • Adds appropriate response headers to service workers
  • Renders compiled source in production and development

Installation

Add this line to your application's Gemfile:

gem 'serviceworker-rails'

And then execute:

$ bundle

Or install it yourself as:

$ gem install serviceworker-rails

To set up your Rails project for use with a Service Worker, you either use the Rails generator and edit the generated files as needed, or you can follow the manual installation steps.

Automated setup

After bundling the gem in your Rails project, run the generator from the root of your Rails project.

$ rails g serviceworker:install

The generator will create the following files:

  • config/initializers/serviceworker.rb - for configuring your Rails app
  • app/assets/javascripts/serviceworker.js.erb - a blank Service Worker script with some example strategies
  • app/assets/javascripts/serviceworker-companion.js - a snippet of JavaScript necessary to register your Service Worker in the browser
  • app/assets/javascripts/manifest.json.erb - a starter web app manifest pointing to some default app icons provided by the gem
  • public/offline.html - a starter offline page

It will also make the following modifications to existing files:

  • Adds a sprockets directive to application.js to require serviceworker-companion.js
  • Adds serviceworker.js and manifest.json to the list of compiled assets in config/initializers/assets.rb
  • Injects tags into the head of app/views/layouts/application.html.erb for linking to the web app manifest

NOTE Given that Service Worker operates in a separate browser thread, outside the context of your web pages, you don't want to include serviceworker.js script in your application.js. So if you have a line like require_tree . in your application.js file, you'll either need to move your serviceworker.js to another location or replace require_tree with something more explicit.

To learn more about each of the changes or to perform the set up yourself, check out the manual setup section below.

Manual setup

Let's add a ServiceWorker to cache some of your JavaScript and CSS assets. We'll assume you already have a Rails application using the asset pipeline built on Sprockets.

Add a service worker script

Create a JavaScript file called app/assets/javascripts/serviceworker.js.erb:

// app/assets/javascripts/serviceworker.js.erb
console.log('[Service Worker] Hello world!');

var CACHE_NAME = 'v1-cached-assets'

function onInstall(event) {
  event.waitUntil(
    caches.open(CACHE_NAME).then(function prefill(cache) {
      return cache.addAll([
        '<%= asset_path "application.js" %>',
        '<%= asset_path "application.css" %>',
        '/offline.html',
        // you get the idea ...
      ]);
    })
  );
}

function onActivate(event) {
  console.log('[Serviceworker]', "Activating!", event);
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.filter(function(cacheName) {
          // Return true if you want to remove this cache,
          // but remember that caches are shared across
          // the whole origin
           return cacheName.indexOf('v1') !== 0;
        }).map(function(cacheName) {
          return caches.delete(cacheName);
        })
      );
    })
  );
}

self.addEventListener('install', onInstall)
self.addEventListener('activate', onActivate)

For use in production, instruct Sprockets to precompile service worker scripts separately from application.js, as in the following example:

Register the service worker

You'll need to register the service worker with a companion script in your main page JavaScript, like application.js. You can use the following:

// app/assets/javascripts/serviceworker-companion.js

if (navigator.serviceWorker) {
  navigator.serviceWorker.register('/serviceworker.js', { scope: './' })
    .then(function(reg) {
      console.log('[Page] Service worker registered!');
    });
}

// app/assets/javascripts/application.js

// ...
//= require serviceworker-companion

Add a manifest

You may also want to create a manifest.json file to make your web app installable.

// app/assets/javascripts/manifest.json
{
  "name": "My Progressive Rails App",
  "short_name": "Progressive",
  "start_url": "/"
}

You'd then link to your manifest from the application layout:

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

Configure the middleware

Next, add a new initializer as show below to instruct the serviceworker-rails middleware how to route requests for assets by canonical url.

# config/initializers/serviceworker.rb

Rails.application.configure do
  config.serviceworker.routes.draw do
    match "/serviceworker.js"
    match "/manifest.json"
  end
end

Precompile the assets

# config/initializers/assets.rb

Rails.application.configure do
  config.assets.precompile += %w[serviceworker.js manifest.json]
end

Test the setup

At this point, restart your Rails app and reload a page in your app in Chrome or Firefox. Using dev tools, you should be able to determine.

  1. The page requests a service worker at /serviceworker.js
  2. The Rails app responds to the request by compiling and rendering the file in app/assets/javascripts/serviceworker.js.erb.
  3. The console displays messages from the page and the service worker
  4. The application JavaScript and CSS assets are added to the browser's request/response Cache.

Using the cache

So far so good? At this point, all we've done is pre-fetched assets and added them to the cache, but we're not doing anything with them yet.

Now, we can use the service worker to intercept requests and either serve them from the cache if they exist there or fallback to the network response otherwise. In most cases, we can expect responses coming from the local cache to be much faster than those coming from the network.

// app/assets/javascripts/serviceworker.js.erb

function onFetch(event) {
  // Fetch from network, fallback to cached content, then offline.html for same-origin GET requests
  var request = event.request;

  if (!request.url.match(/^https?:\/\/example.com/) ) { return; }
  if (request.method !== 'GET') { return; }

  event.respondWith(
    fetch(request)                                        // first, the network
      .catch(function fallback() {
         caches.match(request).then(function(response) {  // then, the cache
           response || caches.match("/offline.html");     // then, /offline cache
         })
       })
  );

  // See https://jakearchibald.com/2014/offline-cookbook/#on-network-response for more examples
}

self.addEventListener('fetch', onFetch);

Configuration

When serviceworker-rails is required in your Gemfile, it will insert a middleware into the Rails middleware stack. You'll want to configure it by mapping serviceworker routes to Sprockets JavaScript assets in an initializer, like the example below.

# config/initializers/serviceworker.rb

Rails.application.configure do
  config.serviceworker.routes.draw do
    # maps to asset named 'serviceworker.js' implicitly
    match "/serviceworker.js"

    # map to a named asset explicitly
    match "/proxied-serviceworker.js" => "nested/asset/serviceworker.js"
    match "/nested/serviceworker.js" => "another/serviceworker.js"

    # capture named path segments and interpolate to asset name
    match "/captures/*segments/serviceworker.js" => "%{segments}/serviceworker.js"

    # capture named parameter and interpolate to asset name
    match "/parameter/:id/serviceworker.js" => "project/%{id}/serviceworker.js"

    # insert custom headers
    match "/header-serviceworker.js" => "another/serviceworker.js",
      headers: { "X-Resource-Header" => "A resource" }

    # maps to serviceworker "pack" compiled by Webpacker
    match "/webpack-serviceworker.js" => "serviceworker.js", pack: true

    # anonymous glob exposes `paths` variable for interpolation
    match "/*/serviceworker.js" => "%{paths}/serviceworker.js"
  end
end

Serviceworker::Rails will insert a Cache-Control header to instruct browsers not to cache your serviceworkers by default. You can customize the headers for all service worker routes if you'd like, such as adding the experimental Service-Worker-Allowed header to set the allowed scope.

config.serviceworker.headers["Service-Worker-Allowed"] = "/"
config.serviceworker.headers["X-Custom-Header"] = "foobar"

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bin/rake to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/rossta/serviceworker-rails. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

The gem is available as open source under the terms of the MIT License.

More Repositories

1

montrose

Recurring events library for Ruby. Enumerable recurrence objects and convenient chainable interface.
Ruby
743
star
2

vue-pdfjs-demo

A demo PDF viewer implemented with Vue and PDF.js
Vue
417
star
3

serviceworker-rails-sandbox

Service Workers on Rails demo app with the serviceworker-rails gem
Ruby
88
star
4

tacokit.rb

Ruby and Trello: the simplest fully-featured Ruby gem for the Trello API
Ruby
49
star
5

loves-enumerable

Code samples for a presentation on the joy of Ruby Enumerable
Ruby
45
star
6

github_groove

Demo Hanami.rb application for integrating GrooveHQ with GitHub issues
Ruby
33
star
7

seymour

Activity feed audiences, backed by Redis.
Ruby
23
star
8

rossta.github.com

rossta's homepage
HTML
21
star
9

rails6-webpacker-demo

Ruby
15
star
10

local-ssl-demo-rails

Rails 5 demo app with local SSL for development and test environments
Ruby
14
star
11

map_ready

A Rails plugin to convert models into map ready marker objects. Includes support for marker clustering and offsetting. Requires the geokit and geokit-rails.
Ruby
11
star
12

webpackersurvival.guide

Learn how to tame Webpacker on Rails - Foundations, best practices, and in-depth lessons.
JavaScript
10
star
13

form-to-terminal

Fill out Google Forms from the command line
JavaScript
10
star
14

rails-webpacker-bootstrap-demo

Rails 6 with Webpacker 4 and Bootstrap 4 Demo
Ruby
9
star
15

weekly-todo-vue

Weekly TODO list in Vue.js
Vue
7
star
16

lionel_richie

Trello? Export your Trello data to Google Docs
Ruby
7
star
17

non-digest-webpack-plugin

Webpack plugin to emit both both digest and non-digest assets.
JavaScript
7
star
18

fnord-client

A client for sending events to an FnordMetric server over UDP (or TCP)
Ruby
5
star
19

pushkin

Pub/sub messaging through private channels using Faye. Based on PrivatePub.
Ruby
4
star
20

opensesame

Rails engine for authenticating internal applications and private-access products
Ruby
4
star
21

leo

The elementary school art teacher didn't have a classroom, so she wheeled in the art cart. Instead of watercolors and charcoal, the ArtCart library enables creativity via HTML Canvas and Javascript.
JavaScript
4
star
22

heuristics

Complex problems in computer science
Ruby
3
star
23

webpacker-6.0.0.beta-pdfjs-demo

Demo of Rails + Webpacker 6.0.0.beta + PDF.js
Ruby
3
star
24

poll

Mac Dashboard widget displaying candidate poll data.
JavaScript
2
star
25

nyu_topic_fu

Final project: Unix Tools
JavaScript
2
star
26

soapbox

Create and present simple slides through your browser using HTML5, CSS, Javascript.
JavaScript
2
star
27

zenkaffe

A simple API for a sip of inspiration. Built on node.js
JavaScript
2
star
28

whassup

Set up a stand-alone uptime check service for your applications
Ruby
2
star
29

capitan

App to manage builds and deployment
Ruby
2
star
30

dropit

An HTML5 drag&drop media uploader application for weplay.com
JavaScript
2
star
31

montrose-select

a javascript menu for selecting repeating events
JavaScript
2
star
32

optical

A visual history of tweet frequency
Ruby
2
star
33

sudokill

Competitive sudoku: it's more than just a one-player game
JavaScript
2
star
34

afterburn

Cumulative flow diagrams for Trello boards
Ruby
2
star
35

montrose-rails

A gem for adding recurring events to Rails apps
Ruby
2
star
36

moneyball

The only way to play golf.
Ruby
2
star
37

os

Labs for Operating Systems
Ruby
1
star
38

opensesame-github

Company walled-garden authentication via github organizations
Ruby
1
star
39

sudoku-solver

A heuristics approach to solving Sudoku in Ruby
Ruby
1
star
40

static_fm

A static file manager for assets (Javascript|CSS)
Ruby
1
star
41

nyu-ror

Projects for NYU Web Development with Ruby on Rails
Ruby
1
star
42

connect-four-vue

Connect Four frontend in Vue.js
JavaScript
1
star
43

js-practices

A slideshow of recommended best practices for coding javascript in applications
1
star
44

spidey-web-crawlers

Web crawlers in Ruby
Ruby
1
star
45

bottleneck

Visualizing Trello boards in cumulative flow diagrams: an incomplete-work-in-progress-proof-of-concept-side-project.
Ruby
1
star
46

connect-four-elixir

Connect Four game in Elixir and Phoenix
Elixir
1
star
47

cache_cow

Rails cache extensions for models
Ruby
1
star