Skip to content

Commit

Permalink
Merge branch 'feature/service-workers'
Browse files Browse the repository at this point in the history
Enable offline caching/usage of the website
  • Loading branch information
weierophinney committed Nov 18, 2015
2 parents 3653aff + 5d43995 commit abe58e9
Show file tree
Hide file tree
Showing 11 changed files with 436 additions and 4 deletions.
18 changes: 17 additions & 1 deletion bin/mwop.net.php
Expand Up @@ -19,7 +19,7 @@
chdir(__DIR__ . '/../');
require_once 'vendor/autoload.php';

define('VERSION', '0.0.2');
define('VERSION', '0.0.3');

$container = require 'config/container.php';

Expand Down Expand Up @@ -130,6 +130,22 @@
return $handler($route, $console);
},
],
[
'name' => 'prep-offline-pages',
'route' => '[--serviceWorker=]',
'description' => 'Prepare the offline pages list for the service-worker.js file.',
'short_description' => 'Prep offline page cache list',
'options_descriptions' => [
'--serviceWorker' => 'Path to the service-worker.js file',
],
'defaults' => [
'serviceWorker' => realpath(getcwd()) . '/public/service-worker.js',
],
'handler' => function ($route, $console) use ($container) {
$handler = $container->get('Mwop\Console\PrepOfflinePages');
return $handler($route, $console);
},
],
];

$app = new Application(
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -37,7 +37,8 @@
},
"require-dev": {
"zfcampus/zf-deploy": "~1.0@stable",
"filp/whoops": "^1.1"
"filp/whoops": "^1.1",
"herrera-io/version": "^1.1"
},
"autoload": {
"psr-4": {
Expand Down
55 changes: 53 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions config/autoload/dependencies.global.php
Expand Up @@ -23,6 +23,7 @@
Blog\Console\FeedGenerator::class => Blog\Console\FeedGeneratorFactory::class,
Blog\Console\TagCloud::class => Blog\Console\TagCloudFactory::class,
Blog\Mapper::class => Blog\MapperFactory::class,
Console\PrepOfflinePages::class => Factory\PrepOfflinePagesFactory::class,
Github\AtomReader::class => Github\AtomReaderFactory::class,
Github\Console\Fetch::class => Github\Console\FetchFactory::class,
Application::class => ApplicationFactory::class,
Expand Down
7 changes: 7 additions & 0 deletions config/autoload/routes.global.php
Expand Up @@ -31,6 +31,7 @@
HomePage::class => Factory\PageFactory::class,
Job\GithubFeed::class => Job\GithubFeedFactory::class,
ResumePage::class => Factory\PageFactory::class,
'Mwop\OfflinePage' => Factory\PageFactory::class,
],
],

Expand All @@ -47,6 +48,12 @@
'allowed_methods' => ['GET'],
'name' => 'comics',
],
[
'path' => '/offline',
'middleware' => 'Mwop\OfflinePage',
'allowed_methods' => ['GET'],
'name' => 'offline',
],
[
'path' => '/resume',
'middleware' => ResumePage::class,
Expand Down
5 changes: 5 additions & 0 deletions public/js/site.js
@@ -0,0 +1,5 @@
if (navigator.serviceWorker) {
navigator.serviceWorker.register('/service-worker.js', {
scope: '/'
});
}
158 changes: 158 additions & 0 deletions public/service-worker.js
@@ -0,0 +1,158 @@
/* Ends with ':' so it can be used with cache identifiers */
var version = 'v0.0.3:'

/* Pages to cache by default */
var offline = [
"/",
"/blog",
"/offline",
"/resume",
"/blog/2015-09-19-zend-10-year-anniversary.html",
"/blog/2015-09-09-composer-root.html",
"/blog/2015-07-28-on-psr7-headers.html",
"/blog/2015-06-08-php-is-20.html",
"/blog/2015-05-18-psr-7-accepted.html",
"/blog/2015-05-15-splitting-components-with-git.html",
"/blog/2015-01-26-psr-7-by-example.html",
"/blog/2015-01-08-on-http-middleware-and-psr-7.html",
"/blog/2014-11-03-utopic-and-amd.html",
"/blog/2014-09-18-zend-server-deployment-part-8.html"
];

/* Cache up to 25 pages locally */
var pageCacheLimit = 25;

/* Cache up to 10 images locally */
var imageCacheLimit = 10;

/* Update/install the static cache */
var updateStaticCache = function() {
return caches.open(version + 'offline').then(function(cache) {
return Promise.all(offline.map(function(value) {
var request = new Request(value);
var url = new URL(request.url);
if (url.origin != location.origin) {
request = new Request(value, {mode: 'no-cors'});
}
return fetch(request).then(function(response) {
var cachedCopy = response.clone();
return cache.put(request, cachedCopy);
});
}));
});
};

/* Invalidate obsolete cache entries */
var clearOldCache = function() {
return caches.keys().then(function(keys) {
return Promise.all(
keys
.filter(function(key) {
return key.indexOf(version);
})
.map(function(key) {
return caches.delete(key);
})
);
});
};

/* Ensure cache only retains a set number of items */
var limitCache = function(cache, maxItems) {
cache.keys().then(function(items) {
if (items.length > maxItems) {
cache.delete(items[0]);
}
});
};

/* Install the service worker: populate the cache */
self.addEventListener('install', function(event) {
event.waitUntil(updateStaticCache());
});

/* On activation: clear out old cache files */
self.addEventListener('activate', function(event) {
event.waitUntil(clearOldCache());
});

/* Handle fetch events, but only from GET */
self.addEventListener('fetch', function(event) {
/* Fetch from site and cache on completion */
var fetchFromNetwork = function(response) {
var cacheCopy = response.clone();

/* Caching an HTML page */
if (event.request.headers.get('Accept').indexOf('text/html') != -1) {
caches.open(version + 'pages').then(function(cache) {
cache.put(event.request, cacheCopy).then(function() {
limitCache(cache, pageCacheLimit);
});
});
return response;
}

/* Caching an image */
if (event.request.headers.get('Accept').indexOf('image/') != -1) {
caches.open(version + 'images').then(function(cache) {
cache.put(event.request, cacheCopy).then(function() {
limitCache(cache, imageCacheLimit);
});
});
return response;
}

/* All other assets */
caches.open(version + 'assets').then(function(cache) {
cache.put(event.request, cacheCopy);
});
return response;
};

/* Provide a fallback in the event of network failure/going offline */
var fallback = function() {
/* HTML pages; check if in cache, returning that, or /offline if not found */
if (event.request.headers.get('Accept').indexOf('text/html') != -1) {
return caches.match(event.request).then(function(response) {
return response || caches.match('/offline');
});
}

/* Images: placeholder indicating offline */
if (event.request.headers.get('Accept').indexOf('image/') != -1) {
return new Response(
'<svg width="400" height="300" role="img" aria-labelledby="offline-title" viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg"><title id="offline-title">Offline</title><g fill="none" fill-rule="evenodd"><path fill="#D8D8D8" d="M0 0h400v300H0z"/><text fill="#9B9B9B" font-family="Helvetica Neue,Arial,Helvetica,sans-serif" font-size="72" font-weight="bold"><tspan x="93" y="172">offline</tspan></text></g></svg>',
{
headers: {
'Content-Type': 'image/svg+xml'
}
}
);
}
};

/* If not a GET request, we're done; nothing to cache */
if (event.request.method != 'GET') {
return;
}

/* HTML requests: attempt to fetch from network first, falling back to cache */
if (event.request.headers.get('Accept').indexOf('text/html') != -1) {
/* Attempt to fetch from the network; fallback if it cannot be done.
*
* Essentially, fetch() returns a promise, and we're using fetchFromNetwork
* as the resolve callback, and fallback as the reject callback.
*/
event.respondWith(fetch(event.request).then(fetchFromNetwork, fallback));
return;
}

/* Non-HTML requests: look for file in cache first */
event.respondWith(
caches.match(event.request).then(function(cached) {
return cached || fetch(event.request).then(fetchFromNetwork, fallback);
})
);
});

/* See https://brandonrozek.com/2015/11/service-workers/ for full details! */

0 comments on commit abe58e9

Please sign in to comment.