├── .gitignore ├── README.md ├── doc-imgs └── internals.png ├── index.html ├── logging.js ├── package.json ├── reset ├── index.html └── sw.js ├── script.js ├── serviceworker-cache-polyfill.js ├── style.css ├── sw.js └── trollface.svg /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is this? 2 | 3 | It's a really simple ServiceWorker example. No build systems, (almost) no dependencies. It's designed to be an interactive introduction to the kinds of things you can do with ServiceWorker. 4 | 5 | ## 1. Get it running locally 6 | 7 | Either clone it via git, or just [grab the zip file](https://github.com/jakearchibald/simple-serviceworker-tutorial/archive/gh-pages.zip). 8 | 9 | If you already run a web server locally, put the files there. Or you can run a web server from the terminal for the current directory by installing [node.js](http://nodejs.org/) and running: 10 | 11 | ```sh 12 | npm install 13 | npm start 14 | ``` 15 | 16 | Visit the site in Chrome (`http://localhost:8080` if you used the script above). Open the dev tools and look at the console. Once you refresh the page, it'll be under the ServiceWorker's control. 17 | 18 | **You can reset the SW & caches at any point** by navigating to `reset/`. That will unregister the ServiceWorker & clear all the caches. 19 | 20 | ## 2. Go offline 21 | 22 | Disable your internet connection & shut down your local web server. 23 | 24 | If you refresh the page, it still works, even through you're offline! Well, we're missing that final JavaScript-added paragraph, but we'll fix that shortly. 25 | 26 | Take a look at the code in `index.html` and `sw.js`, hopefully the comments make it easy to follow. 27 | 28 | ## 3. Fixing that script 29 | 30 | The `install` event in the ServiceWorker is setting up the cache, but it's missing a reference to the script that adds the paragraph to the page. Add it to the array. The URL is `script.js`. It doesn't need to be a no-cors request like the Flickr image because it's on the same origin. 31 | 32 | Make sure you're online, refresh the page & watch the console. The browser checks for updates to the ServiceWorker script, if anything in the file has changed it considers it to be a new version. The new version is been picked up, but it isn't ready to use. 33 | 34 | If you open a new tab and go to `chrome://serviceworker-internals` you'll see both the old & new worker listed. 35 | 36 | ![serviceworker-internals](doc-imgs/internals.png) 37 | 38 | **Not seeing the new worker?** It could be that your server sent the original worker with a far-future `max-age` or similar caching header. Instead, use the node server mentioned in exercise 1 instead. 39 | 40 | Follow the instructions in the page's console to get the new version working. 41 | 42 | Test your page offline, the final JavaScript-added paragraph should have reappeared. 43 | 44 | ## 4. Faster updates! 45 | 46 | The update process you just encountered means only one version of your app can run at once. That's often useful, but we don't need it right now. 47 | 48 | `self.skipWaiting` called within a ServiceWorker means it won't wait for tabs to stop using the old version before it takes over. 49 | 50 | In your `install` event, before the call to `event.waitUntil` add: 51 | 52 | ```js 53 | if (self.skipWaiting) { self.skipWaiting(); } 54 | ``` 55 | 56 | If you refresh the page now, the new version should activate immediately. 57 | 58 | Chrome 40 shipped with ServiceWorker but without `skipWaiting`, so the `if` statement prevents errors there. If you want to see the effects of `skipWaiting`, use a newer version of Chrome, such as [Chrome Canary](https://www.google.com/chrome/browser/canary.html). 59 | 60 | `skipWaiting` means your new worker will handle requests from pages that were loaded with the old worker. If that's a problem, or if multiple tabs running different versions of your app/site is a problem, avoid `skipWaiting`. 61 | 62 | ## 5. Messing around with particular requests 63 | 64 | Currently we're responding to all requests by trying the cache & falling back to the network. Let's do something different for particular URLs. 65 | 66 | In the `fetch` event, before calling `event.respondWith`, add the following code: 67 | 68 | ```js 69 | if (/\.jpg$/.test(event.request.url)) { 70 | event.respondWith(fetch('trollface.svg')); 71 | return; 72 | } 73 | ``` 74 | 75 | Here we're intercepting URLs that end `.jpg` and responding with a network fetch for a different resource. 76 | 77 | Refresh the page, watch the console, and once the new ServiceWorker is active, refresh again. Now you get a different image! 78 | 79 | ## 6. Manual responses 80 | 81 | In the previous step, we handled all requests ending `.jpg`, but often you want finer control over which URLs you handle. 82 | 83 | In the `fetch` event, add the following code before the code you added in the previous exercise: 84 | 85 | ```js 86 | var pageURL = new URL('./', location); 87 | 88 | if (event.request.url === pageURL.href) { 89 | event.respondWith(new Response("Hello world!")) 90 | return; 91 | } 92 | ``` 93 | 94 | Refresh the page, watch the console, and once the new ServiceWorker is active, refresh again. Different response! This is how you create responses manually. 95 | 96 | # Further reading 97 | 98 | You're now cooking with ServiceWorkers! To learn more about how they work, and practical patterns you'll use in apps and sites, check out the resources listed on [is-serviceworker-ready](https://jakearchibald.github.io/isserviceworkerready/resources.html). 99 | -------------------------------------------------------------------------------- /doc-imgs/internals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakearchibald/simple-serviceworker-tutorial/0eb4656b652f75ddf14ea04172c27423c2866979/doc-imgs/internals.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Super Simple ServiceWorker 5 | 6 | 7 | 8 | 9 |

Super simple ServiceWorker

10 |

This is some text.

11 |

And now, a picture of an obscene ice-cream:

12 | Ew 13 |

And now, some content added via JavaScript:

14 | 15 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /logging.js: -------------------------------------------------------------------------------- 1 | // First time playing with SW? This script is just for logging, 2 | // you can pretty much ignore it until you want to dive deeper. 3 | 4 | if (!navigator.serviceWorker.controller) { 5 | console.log("This page is not controlled by a ServiceWorker"); 6 | } 7 | else { 8 | console.log("This page is controlled by a ServiceWorker"); 9 | } 10 | 11 | navigator.serviceWorker.getRegistration().then(function(reg) { 12 | function showWaitingMessage() { 13 | console.log("A new ServiceWorker is waiting to become active. It can't become active now because pages are still open that are controlled by the older version. Either close those tabs, or shift+reload them (which loads them without the ServiceWorker). That will allow the new version to become active, so it'll be used for the next page load."); 14 | } 15 | 16 | reg.addEventListener('updatefound', function() { 17 | console.log("Found a new ServiceWorker!"); 18 | var installing = reg.installing; 19 | reg.installing.addEventListener('statechange', function() { 20 | if (installing.state == 'installed') { 21 | console.log("New ServiceWorker installed."); 22 | // give it a second to see if it activates immediately 23 | setTimeout(function() { 24 | if (installing.state == 'activated') { 25 | console.log("New ServiceWorker activated! Reload to load this page with the new ServiceWorker."); 26 | } 27 | else { 28 | showWaitingMessage(); 29 | } 30 | }, 1000); 31 | } 32 | else if (installing.state == 'redundant') { 33 | console.log("The new worker failed to install - likely an error during install"); 34 | } 35 | }); 36 | }); 37 | 38 | if (reg.waiting) { 39 | showWaitingMessage(); 40 | } 41 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-serviceworker-tutorial", 3 | "private": true, 4 | "scripts": { 5 | "start": "http-server -c-1" 6 | }, 7 | "devDependencies": { 8 | "http-server": "0.8.5" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /reset/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reset ServiceWorker 5 | 6 | 7 | 8 | 9 |

Resetting ServiceWorker

10 | 47 | 48 | -------------------------------------------------------------------------------- /reset/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('install', function(event) { 2 | // delete all the caches 3 | event.waitUntil( 4 | caches.keys().then(function(keys) { 5 | return Promise.all( 6 | keys.map(function(key) { 7 | return caches.delete(key); 8 | }) 9 | ); 10 | }) 11 | ); 12 | }); -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | var p = document.createElement('p'); 2 | p.textContent = 'This content was added via JavaScript!'; 3 | document.body.appendChild(p); -------------------------------------------------------------------------------- /serviceworker-cache-polyfill.js: -------------------------------------------------------------------------------- 1 | if (!Cache.prototype.add) { 2 | Cache.prototype.add = function add(request) { 3 | return this.addAll([request]); 4 | }; 5 | } 6 | 7 | if (!Cache.prototype.addAll) { 8 | Cache.prototype.addAll = function addAll(requests) { 9 | var cache = this; 10 | 11 | // Since DOMExceptions are not constructable: 12 | function NetworkError(message) { 13 | this.name = 'NetworkError'; 14 | this.code = 19; 15 | this.message = message; 16 | } 17 | NetworkError.prototype = Object.create(Error.prototype); 18 | 19 | return Promise.resolve().then(function() { 20 | if (arguments.length < 1) throw new TypeError(); 21 | 22 | // Simulate sequence<(Request or USVString)> binding: 23 | var sequence = []; 24 | 25 | requests = requests.map(function(request) { 26 | if (request instanceof Request) { 27 | return request; 28 | } 29 | else { 30 | return String(request); // may throw TypeError 31 | } 32 | }); 33 | 34 | return Promise.all( 35 | requests.map(function(request) { 36 | if (typeof request === 'string') { 37 | request = new Request(request); 38 | } 39 | 40 | var scheme = new URL(request.url).protocol; 41 | 42 | if (scheme !== 'http:' && scheme !== 'https:') { 43 | throw new NetworkError("Invalid scheme"); 44 | } 45 | 46 | return fetch(request.clone()); 47 | }) 48 | ); 49 | }).then(function(responses) { 50 | // TODO: check that requests don't overwrite one another 51 | // (don't think this is possible to polyfill due to opaque responses) 52 | return Promise.all( 53 | responses.map(function(response, i) { 54 | return cache.put(requests[i], response); 55 | }) 56 | ); 57 | }).then(function() { 58 | return undefined; 59 | }); 60 | }; 61 | } 62 | 63 | if (!CacheStorage.prototype.match) { 64 | // This is probably vulnerable to race conditions (removing caches etc) 65 | CacheStorage.prototype.match = function match(request, opts) { 66 | var caches = this; 67 | 68 | return this.keys().then(function(cacheNames) { 69 | var match; 70 | 71 | return cacheNames.reduce(function(chain, cacheName) { 72 | return chain.then(function() { 73 | return match || caches.open(cacheName).then(function(cache) { 74 | return cache.match(request, opts); 75 | }).then(function(response) { 76 | match = response; 77 | return match; 78 | }); 79 | }); 80 | }, Promise.resolve()); 81 | }); 82 | }; 83 | } -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | html { 2 | /* serif sucks! */ 3 | font-family: sans-serif; 4 | } 5 | 6 | img { 7 | max-width: 100%; 8 | } -------------------------------------------------------------------------------- /sw.js: -------------------------------------------------------------------------------- 1 | // Chrome's currently missing some useful cache methods, 2 | // this polyfill adds them. 3 | importScripts('serviceworker-cache-polyfill.js'); 4 | 5 | // Here comes the install event! 6 | // This only happens once, when the browser sees this 7 | // version of the ServiceWorker for the first time. 8 | self.addEventListener('install', function(event) { 9 | // We pass a promise to event.waitUntil to signal how 10 | // long install takes, and if it failed 11 | event.waitUntil( 12 | // We open a cache… 13 | caches.open('simple-sw-v1').then(function(cache) { 14 | // And add resources to it 15 | return cache.addAll([ 16 | './', 17 | 'style.css', 18 | 'logging.js' 19 | ]); 20 | }) 21 | ); 22 | }); 23 | 24 | // The fetch event happens for the page request with the 25 | // ServiceWorker's scope, and any request made within that 26 | // page 27 | self.addEventListener('fetch', function(event) { 28 | // Calling event.respondWith means we're in charge 29 | // of providing the response. We pass in a promise 30 | // that resolves with a response object 31 | event.respondWith( 32 | // First we look for something in the caches that 33 | // matches the request 34 | caches.match(event.request).then(function(response) { 35 | // If we get something, we return it, otherwise 36 | // it's null, and we'll pass the request to 37 | // fetch, which will use the network. 38 | return response || fetch(event.request); 39 | }) 40 | ); 41 | }); 42 | -------------------------------------------------------------------------------- /trollface.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------