An Express-like layer on top of Service Workers to provide a way to easily plug functionality.
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
demo
dist
lib
testing
.gitignore
.jshintrc
README.md
bower.json
debugging.md
gulpfile.js
index.js
package.json
sw-tests.js

README.md

ServiceWorkerWare

An Express-like layer on top of ServiceWorkers to provide a way to easily plug functionality.

Compatibility

Currently working in:

Philosophy

ServiceWorkers are sold as the replacement for AppCache. But you can do more things than just cache network requests! They have a lifecycle and are able to listen to events (via postMessage), so you can write advanced caches, routers, and a lot more of things and have them run on the ServiceWorker.

This library follows the same pattern as the Express framework, allowing developers to write individual middleware pieces that can be layered in order to augment the default functionality.

Usage

Our syntax is pretty similar to Express'. The first step is to load the basic library:

importScripts('../path/to/ServiceWorkerWare.js');

Then you can load as many additional layers as you might need. In this example we will just import one:

 // Defines a `myMiddleware` variable
importScripts('../myMiddleware.js');

Once myMiddleware is loaded, you can use it with ServiceWorkerWare instances:

var worker = new self.ServiceWorkerWare();
worker.use(myMiddleware);

And that will route the requests through myMiddleware.

You can also specify paths and HTTP verbs, so only requests that match the path or the verb will be handled by that middleware. For example:

worker.post('/event/*', analyticsMiddleware);

More than one middleware can be registered with one worker. You just need to keep in mind that the request and response objects will be passed to each middleware in the same order that they were registered.

Specifying routes

While registering your middlewares, you are not limited just to predefined paths, you have the choice to specify your routes by using placeholders. You could use placeholders as wildcards that match several different routes and handle them in your middleware registration. These placeholders are loosely based on Express' path strings.

Currently supported placeholders include:

  • Anonymous placeholder: *
    Can accommodate any number of characters (including the empty string):
    • "*" is the universal route -- it will match any path

worker.get('*'); // matches any path

  * `"/foo*"` will match `/foo` and any path downstream (like `/foo/bar/baz`)  
  ```javascript
worker.get('/foo*'); // will match /foo and any subpaths
  • Named placeholder: :<placeholder-name>
    Can accommodate any substring, but doesn't allow the empty string (matches minimum 1 character).
    The placeholder name could be any number of alphanumeric characters:
    • "/:path"' will match /fooand/foo/bar/baz, but won't match /`

worker.get('/:path'); // won't match / as :path must not be empty


You could use the backslash character to escape special placeholders (and specify literal asterisks and/or colon characters in your code). *Note: in JavaScript string literals you must double the backslash to achieve the intended effect.*

```javascript
worker.get('/:param\\:42'); // will match /x:42 and /answer/is:42

Writing a middleware layer

Each middleware instance is an object implementing one callback per ServiceWorker event type to be handled:

{
    onInstall: fn1,
    onActivate: fn2,
    onFetch: fn3,
    onMessage: fn4
}

You don't need to respond to all the events--you can opt to only implement a subset of methods you care about. For example, if you only want to handle requests, you just need to implement the onFetch method. But in that case you might be missing out on the full ServiceWorker potential: you can do things such as preloading and caching resources during installation, clear the caches during activation (if the worker has been updated) or even just control the worker's behaviour by sending it a message.

Also, in order to make it even more Express-like, you can write middleware in the following way if you just want to handle fetch events and don't care about the ServiceWorkers life cycle:

var worker = new self.ServiceWorkerWare();
worker.get('/hello.html', function(request, response) {
  return Promise.resolve(new Response('Hello world!', { headers: {'Content-Type': 'text/html'} }));
}
worker.init();

Advanced middleware pipelining

Middlewares are tied to one or several URLs and executed in the same order you register them. The first middleware is passed with the Request from the client code and null as response. Next middlewares receive their parameters from the previous ones according to:

  • If returning a pair [Request, Response], these will be the values for next request and response parameters.
  • If returning only a Request, this will be the next request and the response remains the same as for the previous middleware.
  • The same happens if returning only a Response.
  • For backward-compatibility, returning null will set the next Response to null.
  • Returning any other thing from a middleware will fail and cause a rejection.

Finally, the answer from the service worker will be the response returned by the last middleware.

If you need to perform asynchronous operations, instead of returning one of the previous values, you can return a Promise resolving in one of the previous values.

Interrupting the middleware pipeline

If you want to respond from a middleware immediately and prevent other middlewares to be executed, return the response you want to use wrapped inside the endWith() callback. The callback is received as the third parameter of a middleware and expects a mandatory parameter to be the final response.

var worker = new self.ServiceWorkerWare();
worker.use(function (req, res, endWith) {
  return endWith(
    new Response('Hello world!', { headers: {'Content-Type': 'text/html'} });
  );
});
worker.use(function (req, res, endWith) {
  console.log('Hello!'); // this will be never printed
});
worker.init();

Remember you can use promises resolving to the wrapped response for your asynchronous stuff.

Events

onInstall

It will be called during ServiceWorker installation. This happens just once.

onActivate

It will be called just after onInstall, or each time that you update your ServiceWorker. It's useful to update caches or any part of your application logic.

onFetch(request, response) returns Promise

Will be called whenever the browser requests a resource when the worker has been installed.

You need to .clone() the request before using it as some fields are one use only.

After the whole process of iterating over the different middleware you need to return a Response object. Each middleware can use the previous response object to perform operations over the content, headers or anything.

The request object provides details that define which resource was requested. This callback might (or not) use those details when building the Response object that it must return.

The following example will handle URLs that start with virtual/ with the simplest version of onFetch: a callback! None of the requested resources need to physically exist, they will be programmatically created by the handler on demand:

worker.get('virtual/.', function(request, response) {

  var url = request.clone().url;

  var content = '<html><body>'
      + url + ' ' + Math.random()
      + '<br/><a href="/demo/index.html">index</a></body></html>';

  return Promise.resolve(new Response(content, {
    headers: {
      'Content-Type': 'text/html',
      'x-powered-by': 'ServiceWorkerWare'
    }
  }));
});

The output for any request that hits this URL will be the original address of the URL, and a randomly generated number. Note how we can even specify the headers for our response!

Remember that the handler must always return a Promise.

onMessage(?)

We will receive this event if an actor (window, iframe, another worker or shared worker) performs a postMessage on the ServiceWorker to communicate with it.

Middleware examples

This package already incorporates two simple middlewares. They are available by default when you import it:

TODO: I would suggest refactoring them away from this package

StaticCacher

This will let you preload and cache static content during the ServiceWorker installation.

TODO: where does self come from? in which context is this being executed?

For example:

worker.use(new self.StaticCacher(['a.html', 'b.html' ...]));

Upon installation, this worker will load a.html and b.html and store their content in the default cache.

SimpleOfflineCache

This will serve contents stored in the default cache. If it cannot find a resource in the cache, it will perform a fetch and save it to the cache.

ZipCacher

This is not built-in with this library. It enables you to specify a ZIP file to cache your resources from.

importScripts('./sww.js');
importScripts('./zipcacher.js');

var worker = new self.ServiceWorkerWare();

worker.use(new ZipCacher('http://localhost:8000/demo/resources.zip'));
worker.use(new self.SimpleOfflineCache());
worker.init();

Running the demo

Clone the repository, and then cd to the directory and run:

npm install
gulp webserver

TODO: use npm scripts to run gulp, avoid global gulp installation

And go to http://localhost:8000/demo/index.html using any of the browsers where ServiceWorkers are supported.

When you visit index.html the ServiceWorker will be installed and /demo/a.html will be preloaded and cached. In contrast, /demo/b.html will be cached only once it is visited (i.e. only once the user navigates to it).

For an example of more advanced programmatic functionality, you can also navigate to any URL of the form http://localhost:8000/demo/virtual/<anything> (where anything is any content you want to enter) and you'll receive an answer by the installed virtual URL handler middleware.

And what about testing?

We are working on an you can see the specs under lib/spec folder.

Please, launch:

$ gulp tests

Once tests are complete, Karma, the testing framework, keeps monitoring your files to relaunch the tests when something is modified. Do not try to close the browsers. If you want to stop testing, kill the Karma process.

This should be straightforward for Windows and iOS. As usual, if your are on Linux or you're having problems with binary routes, try setting some environment variables:

$ FIREFOX_NIGHTLY_BIN=/path/to/nightly-bin CHROME_BIN=/path/to/chrome-bin gulp tests

Thanks!

A lot of this code has been inspired by different projects:

License

Mozilla Public License 2.0

http://mozilla.org/MPL/2.0/