Skip to content
This repository has been archived by the owner on Aug 31, 2021. It is now read-only.

Commit

Permalink
Merge pull request #67 from GoogleChrome/regex-routes
Browse files Browse the repository at this point in the history
RegExp routes
  • Loading branch information
wibblymat committed Jan 5, 2016
2 parents 4f19cca + ed65678 commit f04c671
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 80 deletions.
91 changes: 80 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Service Worker Toolbox provides some simple helpers for use in creating your own

## Install

Service Worker Toolbox is available through Bower, npm or direct from github:
Service Worker Toolbox is available through Bower, npm or direct from GitHub:

`bower install --save sw-toolbox`

Expand Down Expand Up @@ -40,21 +40,47 @@ importScripts('bower_components/sw-toolbox/sw-toolbox.js'); // Update path to ma

## Usage

### Defining Routes
### Basic Routes

A _route_ is a URL pattern and request method associated with a handler. It defines the behaviour for a section of the site. _Routing_ is the process of matching an incoming request with the most appropriate route. To define a route you call the appropriate method on `toolbox.router`.
A _route_ is a URL pattern and request method associated with a handler.
It defines the behaviour for a section of the site.
_Routing_ is the process of matching an incoming request with the most
appropriate route. To define a route you call the appropriate method on
`toolbox.router`.

For example, to send `GET` requests for the URL `'/myapp/index.html'` to a handler called `mainHandler()` you would write the following in your service worker file:
For example, to send `GET` requests for the URL `'/myapp/index.html'` to the
built-in `toolbox.networkFirst` handler, you would write the following in your
service worker file:

`toolbox.router.get('/myapp/index.html', mainHandler);`
`toolbox.router.get('/myapp/index.html', toolbox.networkFirst);`

Some other examples follow.
If you don't need wildcards in your route, and your route applies to the same
domain as your main site, then you can use a string like `'/myapp/index.html'`.
However, if you need wildcards (e.g. match _any_ URL that begins with
`/myapp/`), or if you need to match URLs that belong to different domains (e.g.
match `https://othersite.com/api/`), `sw-toolbox` has two options for
configuring your routes.

```javascript
// For some common cases Service Worker Toolbox provides a built-in handler
toolbox.router.get('/', toolbox.networkFirst);
### Express-style Routes

For developers familiar with [Express routing](http://expressjs.com/en/guide/routing.html),
`sw-toolbox` offers support for similar named wildcards, via the
[`path-to-regexp`](https://github.com/pillarjs/path-to-regexp) library.

If you use a `String` to define your route, it's assumed you're using
Express-style routes.

By default, a route will only match URLs on the same origin as the service
worker. If you'd like your Express-style routes to match URLs on different
origins, you need to pass in a value for the `origin` option. The value could be
either a `String` (which is checked for an exact match) or a `RegExp` object.
In both cases, it's matched against the full origin of the URL
(e.g. `'https://example.com'`).

Some examples of using Express-style routing include:

// URL patterns are the same syntax as ExpressJS routes
```javascript
// URL patterns are the same syntax as Express routes
// (http://expressjs.com/guide/routing.html)
toolbox.router.get(':foo/index.html', function(request, values) {
return new Response('Handled a request for ' + request.url +
Expand All @@ -63,7 +89,45 @@ toolbox.router.get(':foo/index.html', function(request, values) {

// For requests to other origins, specify the origin as an option
toolbox.router.post('/(.*)', apiHandler, {origin: 'https://api.example.com'});
```

### Regular Expression Routes

Developers who are more comfortable using [regular expressions](https://regex101.com/)
can use an alternative syntax to define routes, passing in a [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions)
object as the first parameter. This `RegExp` will be matched against the full
request URL when determining whether the route applies, including the origin and
path. This can lead to simpler cross-origin routing vs. Express-style routes,
since both the origin and the path are matched simultaneously, without having
to specify a separate `origin` option.

Note that while Express-style routes allow you to name path fragment
parameters that will be passed to your handler (see `values.foo` in the previous
example), that functionality is not supported while using regular expression
routes.

Some examples of using Regular Expression routing include:

```javascript
// Match URLs that end in index.html
toolbox.router.get(/index.html$/, function(request) {
return new Response('Handled a request for ' + request.url);
});

// Match URLs that begin with https://api.example.com
toolbox.router.post(/^https:\/\/api.example.com\//, apiHandler);
```

### The Default Route

`sw-toolbox` supports defining an optional "default" route via
`toolbox.router.default` that is used whenever there is no alternative route for
a given URL. If `toolbox.router.default` is not set, then `sw-toolbox` will
just ignore requests for URLs that don't match any alternative routes, and the
requests will potentially be handled by the browser as if there were no
service worker involvement.

```javascript
// Provide a default handler for GET requests
toolbox.router.default = myDefaultRequestHandler;
```
Expand All @@ -86,7 +150,12 @@ var myHandler = function(request, values, options) {
```

- `request` - The [Request](https://fetch.spec.whatwg.org/#request) object that triggered the `fetch` event
- `values` - Object whose keys are the placeholder names in the URL pattern, with the values being the corresponding part of the request URL. For example, with a URL pattern of `'/images/:size/:name.jpg'` and an actual URL of `'/images/large/unicorns.jpg'`, `values` would be `{size: 'large', name: 'unicorns'}`
- `values` - When using Express-style routing paths, this will be an object
whose keys are the placeholder names in the URL pattern, with the values being
the corresponding part of the request URL. For example, with a URL pattern of
`'/images/:size/:name.jpg'` and an actual URL of `'/images/large/unicorns.jpg'`,
`values` would be `{size: 'large', name: 'unicorns'}`.
When using a RegExp for the path, `values` will not be set.
- `options` - the [options](#options) passed to one of the [router methods](#methods).

The return value should be a [Response](https://fetch.spec.whatwg.org/#response), or a [Promise](http://www.html5rocks.com/en/tutorials/es6/promises/) that resolves with a Response. If another value is returned, or if the returned Promise is rejected, the Request will fail which will appear to be a [NetworkError](https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-NetworkError) to the page that made the request.
Expand Down
1 change: 0 additions & 1 deletion lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ function openCache(options) {
}
cacheName = cacheName || globalOptions.cache.name;

debug('Opening cache "' + cacheName + '"', options);
return caches.open(cacheName);
}

Expand Down
36 changes: 22 additions & 14 deletions lib/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,36 @@ var basePath = url.pathname;
var pathRegexp = require('path-to-regexp');

var Route = function(method, path, handler, options) {
// The URL() constructor can't parse express-style routes as they are not
// valid urls. This means we have to manually manipulate relative urls into
// absolute ones. This check is extremely naive but implementing a tweaked
// version of the full algorithm seems like overkill
// (https://url.spec.whatwg.org/#concept-basic-url-parser)
if (path.indexOf('/') !== 0) {
path = basePath + path;
if (path instanceof RegExp) {
this.fullUrlRegExp = path;
} else {
// The URL() constructor can't parse express-style routes as they are not
// valid urls. This means we have to manually manipulate relative urls into
// absolute ones. This check is extremely naive but implementing a tweaked
// version of the full algorithm seems like overkill
// (https://url.spec.whatwg.org/#concept-basic-url-parser)
if (path.indexOf('/') !== 0) {
path = basePath + path;
}

this.keys = [];
this.regexp = pathRegexp(path, this.keys);
}

this.method = method;
this.keys = [];
this.regexp = pathRegexp(path, this.keys);
this.options = options;
this.handler = handler;
};

Route.prototype.makeHandler = function(url) {
var match = this.regexp.exec(url);
var values = {};
this.keys.forEach(function(key, index) {
values[key.name] = match[index + 1];
});
if (this.regexp) {
var match = this.regexp.exec(url);
var values = {};
this.keys.forEach(function(key, index) {
values[key.name] = match[index + 1];
});
}

return function(request) {
return this.handler(request, values, this.options);
}.bind(this);
Expand Down
60 changes: 37 additions & 23 deletions lib/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,22 @@ var Router = function() {

Router.prototype.add = function(method, path, handler, options) {
options = options || {};
var origin = options.origin || self.location.origin;
if (origin instanceof RegExp) {
origin = origin.source;
var origin;

if (path instanceof RegExp) {
// We need a unique key to use in the Map to distinguish RegExp paths
// from Express-style paths + origins. Since we can use any object as the
// key in a Map, let's use the RegExp constructor!
origin = RegExp;
} else {
origin = regexEscape(origin);
origin = options.origin || self.location.origin;
if (origin instanceof RegExp) {
origin = origin.source;
} else {
origin = regexEscape(origin);
}
}

method = method.toLowerCase();

var route = new Route(method, path, handler, options);
Expand All @@ -69,29 +79,33 @@ Router.prototype.add = function(method, path, handler, options) {
}

var routeMap = methodMap.get(method);
routeMap.set(route.regexp.source, route);
var regExp = route.regexp || route.fullUrlRegExp;
routeMap.set(regExp.source, route);
};

Router.prototype.matchMethod = function(method, url) {
url = new URL(url);
var origin = url.origin;
var path = url.pathname;
method = method.toLowerCase();

var methods = keyMatch(this.routes, origin);
if (!methods) {
return null;
}

var routes = methods.get(method);
if (!routes) {
return null;
}

var route = keyMatch(routes, path);
var urlObject = new URL(url);
var origin = urlObject.origin;
var path = urlObject.pathname;

// We want to first check to see if there's a match against any
// "Express-style" routes (string for the path, RegExp for the origin).
// Checking for Express-style matches first maintains the legacy behavior.
// If there's no match, we next check for a match against any RegExp routes,
// where the RegExp in question matches the full URL (both origin and path).
return this._match(method, keyMatch(this.routes, origin), path) ||
this._match(method, this.routes.get(RegExp), url);
};

if (route) {
return route.makeHandler(path);
Router.prototype._match = function(method, methodMap, pathOrUrl) {
if (methodMap) {
var routeMap = methodMap.get(method.toLowerCase());
if (routeMap) {
var route = keyMatch(routeMap, pathOrUrl);
if (route) {
return route.makeHandler(pathOrUrl);
}
}
}

return null;
Expand Down
12 changes: 5 additions & 7 deletions recipes/cache-expiration-options/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,20 @@
global.toolbox.options.debug = true;

// Set up a handler for HTTP GET requests:
// - '/(.*)' means any URL pathname will be matched.
// - /\.ytimg\.com\// will match any requests whose URL contains 'ytimg.com'.
// A narrower RegExp could be used, but just checking for ytimg.com anywhere
// in the URL should be fine for this sample.
// - toolbox.cacheFirst let us to use the predefined cache strategy for those
// requests.
global.toolbox.router.get('/(.*)', global.toolbox.cacheFirst, {
global.toolbox.router.get(/\.ytimg\.com\//, global.toolbox.cacheFirst, {
// Use a dedicated cache for the responses, separate from the default cache.
cache: {
name: 'youtube-thumbnails',
// Store up to 10 entries in that cache.
maxEntries: 10,
// Expire any entries that are older than 30 seconds.
maxAgeSeconds: 30
},
// origin allows us to restrict the handler to requests whose origin matches
// a regexp. In this case, we want to match anything that ends in
// 'ytimg.com'.
origin: /\.ytimg\.com$/
}
});

// By default, all requests that don't match our custom handler will use the
Expand Down
Loading

0 comments on commit f04c671

Please sign in to comment.