├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Procfile ├── README.md ├── _recipe_template ├── README.md ├── index.html ├── index.js └── service-worker.js ├── api-analytics ├── README.md ├── index.html ├── index.js ├── report.html ├── server.js └── service-worker.js ├── cache-from-zip ├── README.md ├── imgs │ ├── a.jpeg │ ├── b.jpeg │ └── c.jpeg ├── index.html ├── index.js ├── lib │ ├── ArrayBufferReader.js │ ├── deflate.js │ ├── inflate.js │ └── zip.js ├── package.zip └── worker.js ├── cache-then-network ├── README.md ├── index.html └── index.js ├── dependency-injector ├── README.md ├── actual-controller.js ├── actual-dialogs.js ├── bootstrap.js ├── default-mapping.js ├── index.html ├── injector.js ├── mock-dialogs.js ├── production-sw.js └── testing-sw.js ├── favicon.ico ├── fetching ├── README.md ├── index.html ├── index.js └── service-worker.js ├── gulpfile.js ├── imgs └── random │ ├── picture-1.png │ ├── picture-10.png │ ├── picture-11.png │ ├── picture-12.png │ ├── picture-13.png │ ├── picture-14.png │ ├── picture-15.png │ ├── picture-16.png │ ├── picture-17.png │ ├── picture-18.png │ ├── picture-19.png │ ├── picture-2.png │ ├── picture-20.png │ ├── picture-21.png │ ├── picture-22.png │ ├── picture-23.png │ ├── picture-24.png │ ├── picture-25.png │ ├── picture-26.png │ ├── picture-27.png │ ├── picture-28.png │ ├── picture-29.png │ ├── picture-3.png │ ├── picture-30.png │ ├── picture-31.png │ ├── picture-32.png │ ├── picture-33.png │ ├── picture-34.png │ ├── picture-35.png │ ├── picture-36.png │ ├── picture-37.png │ ├── picture-38.png │ ├── picture-39.png │ ├── picture-4.png │ ├── picture-40.png │ ├── picture-41.png │ ├── picture-42.png │ ├── picture-43.png │ ├── picture-44.png │ ├── picture-45.png │ ├── picture-46.png │ ├── picture-47.png │ ├── picture-48.png │ ├── picture-49.png │ ├── picture-5.png │ ├── picture-50.png │ ├── picture-6.png │ ├── picture-7.png │ ├── picture-8.png │ └── picture-9.png ├── immediate-claim ├── README.md ├── default.jpg ├── index.html ├── index.js ├── server.js └── service-worker.js ├── json-cache ├── README.md ├── files-to-cache.json ├── index.html ├── index.js ├── random-1.png ├── random-2.png ├── random-3.png ├── random-4.png ├── random-5.png ├── random-6.png └── service-worker.js ├── live-flowchart ├── README.md ├── active-service-worker-unregister.png ├── active-service-worker.png ├── app.js ├── index.html ├── logger.js ├── no-active-service-worker.png ├── register-unregister.png ├── register.png ├── security-error.png ├── service-worker-util.js ├── service-worker.js ├── style.css ├── sw-flowchart.png └── wrong-scriptURL.png ├── load-balancer ├── README.md ├── index.html ├── index.js ├── server-1 │ └── imgs │ │ ├── a.jpeg │ │ ├── b.jpeg │ │ └── c.jpeg ├── server-2 │ └── imgs │ │ ├── a.jpeg │ │ ├── b.jpeg │ │ └── c.jpeg ├── server-3 │ └── imgs │ │ ├── a.jpeg │ │ ├── b.jpeg │ │ └── c.jpeg ├── server.js └── service-worker.js ├── local-download ├── README.md ├── index.html ├── index.js └── service-worker.js ├── message-relay ├── README.md ├── index.html ├── index.js └── service-worker.js ├── offline-fallback ├── README.md ├── index.html ├── index.js ├── offline.html └── service-worker.js ├── offline-status ├── README.md ├── app.js ├── index.html ├── index.js ├── random-1.png ├── random-2.png ├── random-3.png ├── random-4.png ├── random-5.png ├── random-6.png ├── service-worker.js └── style.css ├── package.json ├── parseRecipes.js ├── push-clients ├── README.md ├── index.html ├── index.js ├── server.js └── service-worker.js ├── push-get-payload ├── README.md ├── index.html ├── index.js ├── server.js └── service-worker.js ├── push-payload ├── README.md ├── index.html ├── index.js ├── server.js └── service-worker.js ├── push-quota ├── README.md ├── index.html ├── index.js ├── server.js └── service-worker.js ├── push-replace ├── README.md ├── index.html ├── index.js ├── server.js └── service-worker.js ├── push-rich ├── README.md ├── caesar.jpg ├── index.html ├── index.js ├── server.js └── service-worker.js ├── push-simple ├── README.md ├── index.html ├── index.js ├── server.js └── service-worker.js ├── push-subscription-management ├── README.md ├── index.html ├── index.js ├── server.js └── service-worker.js ├── render-store ├── README.md ├── index.html ├── index.js ├── pokemon.html ├── pokemon.js └── service-worker.js ├── request-deferrer ├── README.md ├── index.html ├── index.js ├── lib │ ├── ServiceWorkerWare.js │ └── localforage.js ├── server.js └── service-worker.js ├── server.js ├── src ├── css │ ├── docco.css │ ├── foundation-icons.css │ ├── foundation.css │ ├── foundation.normalize.css │ └── style.css ├── js │ └── layout.js └── tpl │ ├── category.html │ ├── demo.html │ ├── docco │ └── docco.jst │ ├── index.html │ ├── intro.html │ └── layout.html ├── strategy-cache-and-update ├── README.md ├── controlled.html ├── index.html ├── index.js ├── non-controlled.html ├── server.js └── service-worker.js ├── strategy-cache-only ├── README.md ├── controlled.html ├── index.html ├── index.js ├── non-controlled.html ├── server.js └── service-worker.js ├── strategy-cache-update-and-refresh ├── README.md ├── controlled.html ├── controlled.js ├── index.html ├── index.js ├── non-controlled.html ├── server.js └── service-worker.js ├── strategy-embedded-fallback ├── README.md ├── controlled.html ├── controlled.js ├── index.html ├── index.js ├── non-controlled.html ├── server.js └── service-worker.js ├── strategy-network-or-cache ├── README.md ├── controlled.html ├── index.html ├── index.js ├── non-controlled.html ├── server.js └── service-worker.js ├── tools.js └── virtual-server ├── README.md ├── index.html ├── index.js ├── lib └── ServiceWorkerWare.js └── service-worker.js /.eslintignore: -------------------------------------------------------------------------------- 1 | *.js 2 | 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb/legacy", 3 | "rules": { 4 | "no-console": 0, 5 | "comma-dangle": 0, 6 | "func-names": 0, 7 | "vars-on-top": 0, 8 | "no-use-before-define": 0, 9 | "space-before-function-paren": 0, 10 | "max-len": [1, 80, 4, {"ignoreComments": true, "ignoreUrls": true}], 11 | "no-param-reassign": 0, 12 | "quote-props": 0, 13 | "wrap-iife": [2, "inside"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | git: 2 | depth: 10 3 | language: node_js 4 | node_js: 5 | - '6' 6 | deploy: 7 | provider: heroku 8 | api_key: 9 | secure: Vw45m2w7wedVrQdoMqdy3MQc5YpTguqX6ZtRo+4miJ9AqdWAcMO/S5pCosv0AfjvGzvpv55FQ1kWWorzX1GHEjm86NoatygtGJF3BH26WZx6fldgVwnyG+pC3+PcJ/PYs/gfIl2rOAfYTjlnWbWieGHIlztDEtEoV7xsOYHwp5/HhpWsvdACXcIr3trCuNkBo10VmjpjFv8Anms4ULqguMN12pTA7asD9Y+JIQCLUzQ5WqfTYDC+IMoYPDkciSQmN5KfBPFfEVeUCWXGI5aTeCrs/M9LPOlsT8UaWaoC1FN1Ebl9OMsXeqGAbJvQvme4HbhyekcfmHGlfdPA/lpaJ3LbRhBiidsA0Pw5FzlEwTRmGDeIbEB6eM5jH30tKWKiRfwyozbOI3ga/ozLYR29Kw6ER2sUGAho6npHt1LAB5n1+BNbyuNT4M/dkZp3n/X7wlCU+jzxrDAOnpEaQbi6XnxST7KYLjvs4NgTFq8xf8vGM1Bd4yfs9h4UfAM0k9Ez/XoK2Za5jYU9nZyfeSw76VERphUgxLK+kwdoifqMz/avQ4pl/hIVCvKAYY/9jLqQcJTuUiei4h28lEqKRMBgTItVJLvQu8xsdKfAdSxFfu4J95/DtjiOfHDz15MJALNCfyLFcFOCzUMN9Fi7X4ZZBlAkksYiUDXEEmoKsPfC37I= 10 | app: serviceworker-cookbook 11 | skip_cleanup: true 12 | on: 13 | repo: mozilla/serviceworker-cookbook 14 | branch: master 15 | node: '6' 16 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Harald Kirschner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start 2 | -------------------------------------------------------------------------------- /_recipe_template/README.md: -------------------------------------------------------------------------------- 1 | # (Title) 2 | (Summary) 3 | 4 | ## Difficulty 5 | (Beginner|Intermediate|Advanced) 6 | 7 | ## Use Case 8 | (Summary) 9 | 10 | ## Features and Usage 11 | 12 | - 13 | - 14 | - 15 | 16 | ## Compatibility 17 | Tests have been run in: 18 | 19 | - 20 | - 21 | - 22 | 23 | ## Solution 24 | (Add details here) 25 | 26 | ## Category 27 | (General Usage|Beyond Offline|Performance|Offline|Web Push|Caching strategies) 28 | -------------------------------------------------------------------------------- /_recipe_template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | (title) - ServiceWorker Cookbook 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /_recipe_template/index.js: -------------------------------------------------------------------------------- 1 | // Register the ServiceWorker 2 | navigator.serviceWorker.register('service-worker.js', { 3 | scope: '.' 4 | }).then(function(registration) { 5 | console.log('The service worker has been registered ', registration); 6 | }); 7 | -------------------------------------------------------------------------------- /_recipe_template/service-worker.js: -------------------------------------------------------------------------------- 1 | // [Working example](/serviceworker-cookbook/DIR-HERE/). 2 | 3 | self.addEventListener('install', function() { 4 | 5 | }); 6 | 7 | self.addEventListener('fetch', function() { 8 | 9 | }); 10 | 11 | self.addEventListener('activate', function() { 12 | 13 | }); 14 | -------------------------------------------------------------------------------- /api-analytics/README.md: -------------------------------------------------------------------------------- 1 | # API Analytics 2 | 3 | Perform API usage logging without interfering with the UI layer by adding a service worker to gather the usage and use the sync API to upload gathered data from time to time. 4 | 5 | ## Difficulty 6 | Intermediate 7 | 8 | ## Use Case 9 | As a web app developer, I want to add API tracking capabilities to my web application trying to not modify client code nor server code at all. 10 | 11 | ## Solution 12 | With the use of a service worker, we intercept each request of a client and send some information to a log API. 13 | 14 | ## Category 15 | Beyond Offline 16 | -------------------------------------------------------------------------------- /api-analytics/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | API analytics - ServiceWorker Cookbook 6 | 7 | 14 | 15 | 16 | Try to add and delete some quotations. 17 |
18 | 19 | 20 |
21 | 22 |
23 | Go to report. 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /api-analytics/report.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | API analytics - Report - ServiceWorker Cookbook 6 | 7 | 12 | 13 | 14 | 15 |

API report

16 |

This is a report about how many times certain operations have been performed on specific URLs

17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for entry in statistics %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% endfor %} 32 |
urlGETPOSTDELETE
{{ entry.url }}{{ entry.GET }}{{ entry.POST }}{{ entry.DELETE }}
33 | 34 | 35 | -------------------------------------------------------------------------------- /api-analytics/service-worker.js: -------------------------------------------------------------------------------- 1 | // In a real use case, the endpoint could point to another origin. 2 | var LOG_ENDPOINT = 'report/logs'; 3 | 4 | // The code in `oninstall` and `onactive` force the service worker to 5 | // control the clients ASAP. 6 | self.oninstall = function(event) { 7 | event.waitUntil(self.skipWaiting()); 8 | }; 9 | 10 | self.onactivate = function(event) { 11 | event.waitUntil(self.clients.claim()); 12 | }; 13 | 14 | self.onfetch = function(event) { 15 | event.respondWith( 16 | // Log the request … 17 | log(event.request) 18 | // … and then actually perform it. 19 | .then(fetch) 20 | ); 21 | }; 22 | 23 | // Post basic information of the request to a backend for historical purposes. 24 | function log(request) { 25 | var returnRequest = function() { 26 | return request; 27 | }; 28 | 29 | var data = { 30 | method: request.method, 31 | url: request.url 32 | }; 33 | 34 | return fetch(LOG_ENDPOINT, { 35 | method: 'POST', 36 | body: JSON.stringify(data), 37 | headers: { 'content-type': 'application/json' } 38 | }) 39 | .then(returnRequest, returnRequest); 40 | } 41 | -------------------------------------------------------------------------------- /cache-from-zip/README.md: -------------------------------------------------------------------------------- 1 | # Cache from ZIP 2 | This recipe illustrates how to cache contents from a zipfile. 3 | 4 | ## Difficulty 5 | Intermediate 6 | 7 | ## Use Case 8 | As a web developer I want to distribute my applications as individual zipped packages, so that I can reduce the number of HTTP requests and provide an implicit way of listing all resources for offline use. 9 | 10 | ## Solution 11 | While installing the SW, download the zipfile and decompress, caching each of resources. 12 | 13 | ## Category 14 | Beyond Offline 15 | -------------------------------------------------------------------------------- /cache-from-zip/imgs/a.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/cache-from-zip/imgs/a.jpeg -------------------------------------------------------------------------------- /cache-from-zip/imgs/b.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/cache-from-zip/imgs/b.jpeg -------------------------------------------------------------------------------- /cache-from-zip/imgs/c.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/cache-from-zip/imgs/c.jpeg -------------------------------------------------------------------------------- /cache-from-zip/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cache from ZIP - ServiceWorker Cookbook 6 | 7 | 17 | 18 | 19 |
20 | We are going to simulate installing and uninstalling a web application 21 | in the browser. To do that, enable logs from service workers to be shown 22 | and click on install button. 23 | 24 |
25 | 26 | 38 | 39 |

40 | 
41 | 
42 | 
43 | 
44 | 


--------------------------------------------------------------------------------
/cache-from-zip/index.js:
--------------------------------------------------------------------------------
 1 | // A convenient shortcut for `document.querySelector()`
 2 | var $ = document.querySelector.bind(document); // eslint-disable-line id-length
 3 | 
 4 | // Check if the application is installed by checking the controller.
 5 | // If there is a service worker controlling this page, let's assume
 6 | // the application is installed.
 7 | navigator.serviceWorker.getRegistration().then(function(registration) {
 8 |   if (registration && registration.active) {
 9 |     showImagesSection();
10 |   }
11 | });
12 | 
13 | // During installation, once the service worker is active, we shows
14 | // the image dynamic loader.
15 | navigator.serviceWorker.oncontrollerchange = function() {
16 |   if (navigator.serviceWorker.controller) {
17 |     logInstall('The application has been installed');
18 |     showImagesSection();
19 |   }
20 | };
21 | 
22 | // Install the worker is no more than registering. It is in charge of
23 | // downloading the package, decompress and cache the resources.
24 | $('#install').onclick = function() {
25 |   navigator.serviceWorker.register('worker.js').then(function() {
26 |     logInstall('Installing...');
27 |   }).catch(function(error) {
28 |     logInstall('An error happened during installing the service worker:');
29 |     logInstall(error.message);
30 |   });
31 | };
32 | 
33 | // Uninstalling the worker is simply unregistering it. Notice this
34 | // wont erase the offline cache so the resources are actually still
35 | // installed but there is no service worker to serve them.
36 | $('#uninstall').onclick = function() {
37 |   navigator.serviceWorker.getRegistration().then(function(registration) {
38 |     if (!registration) { return; }
39 |     registration.unregister()
40 |       .then(function() {
41 |         logUninstall('The application has been uninstalled');
42 |         setTimeout(function() { location.reload(); }, 500);
43 |       })
44 |       .catch(function(error) {
45 |         logUninstall('Error while uninstalling the service worker:');
46 |         logUninstall(error.message);
47 |       });
48 |   });
49 | };
50 | 
51 | // To load an image is no more than assiging the correct URL to
52 | // the displayer.
53 | $('#load-image').onclick = function() {
54 |   $('img').src = $('select').value;
55 | };
56 | 
57 | // A bunch of helpers to control the UI.
58 | function showImagesSection() {
59 |   $('#images').hidden = false;
60 |   $('#install-notice').hidden = true;
61 | }
62 | 
63 | function logInstall(what) {
64 |   log(what, 'Install');
65 | }
66 | 
67 | function logUninstall(what) {
68 |   log(what, 'Uninstall');
69 | }
70 | 
71 | function log(what, tag) {
72 |   var label = '[' + tag + ']';
73 |   console.log(label, what);
74 |   $('#results').textContent += label + ' ' + what + '\n';
75 | }
76 | 


--------------------------------------------------------------------------------
/cache-from-zip/lib/ArrayBufferReader.js:
--------------------------------------------------------------------------------
 1 | /* global zip */
 2 | 
 3 | (function (zip) {
 4 |   'use strict';
 5 | 
 6 |   function ArrayBufferReader(arraybuffer) {
 7 |     var that = this;
 8 | 
 9 |     this.init = init;
10 |     this.readUint8Array = readUint8Array;
11 | 
12 |     function init(callback, onerror) {
13 |       try {
14 |         that.data = new Uint8Array(arraybuffer);
15 |         that.size = that.data.length;
16 |         callback();
17 |       } catch (e) {
18 |         onerror(e);
19 |       }
20 |     }
21 | 
22 |     function readUint8Array(index, length, callback, onerror) {
23 |       callback(new Uint8Array(that.data.subarray(index, index + length)));
24 |     }
25 |   }
26 |   ArrayBufferReader.prototype = new zip.Reader();
27 |   ArrayBufferReader.prototype.constructor = ArrayBufferReader;
28 | 
29 |   zip.ArrayBufferReader = ArrayBufferReader;
30 | }(zip));
31 | 


--------------------------------------------------------------------------------
/cache-from-zip/package.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/cache-from-zip/package.zip


--------------------------------------------------------------------------------
/cache-then-network/README.md:
--------------------------------------------------------------------------------
 1 | # Cache then Network
 2 | 
 3 | This recipe illustrates methods to return network requests from either the cache or network.
 4 | 
 5 | ## Difficulty
 6 | Beginner
 7 | 
 8 | ## Use Case
 9 | One advantage to using service workers is having programmatic control over what you return from cache and what you prefer to load from server.  This example provides you a front-end to experiment with that choice.
10 | 
11 | ## Features and Usage
12 | 
13 | - Configure form to allow or disallow cache and/or network
14 | - Click "Get new data" to execute requests
15 | 
16 | ## Category
17 | Performance
18 | 


--------------------------------------------------------------------------------
/cache-then-network/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 |   
 5 |   Cache then Network - ServiceWorker Cookbook
 6 |   
 7 |   
14 | 
15 | 
16 | 
17 |   
18 |   
19 |   
20 | 
21 |   
Cache: 22 | Fail 23 | Delay: ms 24 |
25 | 26 |
Network: 27 | Fail 28 | Delay: ms 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /dependency-injector/README.md: -------------------------------------------------------------------------------- 1 | # Dependency Injection 2 | This recipe shows how a Service Worker can act as a dependency injector, avoiding _hard wiring_ dependencies for high level components. 3 | 4 | ## Difficulty 5 | Advanced 6 | 7 | ## Use Case 8 | As a framework developer, I want to provide production and testing environments, configuring two different injectors to provide the proper mock ups for the components without altering client code. 9 | 10 | ## Solution 11 | A service worker can act as the injector. Provide two different injectors one for production and another for testing and let your framework bootstrap code decide which one register, then serve different contents for the same resource according to the mappings inside the injectors. 12 | 13 | Start by looking at `bootstrap.js` to see how a framework could detect which injector should use. The file `default-mapping.js` contains the specification about how abstract resources are mapped to concrete ones. The `production-sw.js` and `testing-sw.js` are the Service Workers acting as injectors. The first simply use `default-mapping.js` spec without modifications while the latter override `utils/dialogs` to serve a mockup instead. 14 | 15 | Compare the actual and mocked implementation of the `dialogs` interface in `actual-dialogs.js` and `mock-dialogs.js`. 16 | 17 | ## Category 18 | Beyond Offline 19 | -------------------------------------------------------------------------------- /dependency-injector/actual-controller.js: -------------------------------------------------------------------------------- 1 | /* global dialogs */ 2 | 3 | document.getElementById('show-alert').onclick = 4 | dialogs.alert.bind(dialogs, 'Hello!'); 5 | 6 | document.getElementById('show-confirm').onclick = 7 | dialogs.confirm.bind(dialogs, 'Do you like these demos?'); 8 | 9 | document.getElementById('show-prompt').onclick = 10 | dialogs.prompt.bind(dialogs, 'Enter your rate about these demos:'); 11 | -------------------------------------------------------------------------------- /dependency-injector/actual-dialogs.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | // The real implementation of dialog rely on the browser dialogs but it could 3 | // implement their own full HTML5 UI. 4 | window.dialogs = { 5 | alert: function(msg) { 6 | window.alert(msg); 7 | }, 8 | 9 | confirm: function(msg) { 10 | return window.confirm(msg); 11 | }, 12 | 13 | prompt: function(msg) { 14 | return window.prompt(msg); 15 | } 16 | }; 17 | })(window); 18 | -------------------------------------------------------------------------------- /dependency-injector/bootstrap.js: -------------------------------------------------------------------------------- 1 | 2 | // This would be the framework bootstrap script, injected in the template HTML 3 | // when building the application. 4 | 5 | // We switch between production and testing environments by checking 6 | // the hash of the URL. 7 | window.onhashchange = function() { 8 | var injector = window.location.hash.substr(1) || 'production'; 9 | var currentInjector = getCurrentInjector(); 10 | 11 | if (injector !== currentInjector) { 12 | // When the new injector is activated, reload... 13 | navigator.serviceWorker.oncontrollerchange = function() { 14 | this.controller.onstatechange = function() { 15 | // ...if this were a real framework, instead of reloading we would 16 | // start to load the view scripts in an asynchronous way but providing 17 | // such mechanisms is out of the scope of this demo. 18 | if (this.state === 'activated') { 19 | window.location.reload(); 20 | } 21 | }; 22 | }; 23 | registerInjector(injector); 24 | } 25 | }; 26 | 27 | // Force the initial check. 28 | window.onhashchange(); 29 | 30 | // Register one or another Service Worker depending on the type of environment. 31 | function registerInjector(injector) { 32 | var injectorUrl = injector + '-sw.js'; 33 | return navigator.serviceWorker.register(injectorUrl); 34 | } 35 | 36 | // Gets the current injector inspecting the service worker registered, if any. 37 | function getCurrentInjector() { 38 | var injector; 39 | var controller = navigator.serviceWorker.controller; 40 | if (controller) { 41 | injector = controller.scriptURL.endsWith('production-sw.js') 42 | ? 'production' : 'testing'; 43 | } 44 | return injector; 45 | } 46 | -------------------------------------------------------------------------------- /dependency-injector/default-mapping.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | 3 | // This is the default mapping relating each abstract resource with 4 | // the concrete implementation. 5 | var mapping = { 6 | 'controller': 'actual-controller.js', 7 | 'utils/dialogs': 'actual-dialogs.js' 8 | }; 9 | -------------------------------------------------------------------------------- /dependency-injector/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dependency injection - ServiceWorker Cookbook 6 | 7 | 8 | 9 | 10 | 11 | 12 |

The demo shows Service Workers as dependency injectors. Depending on your selection, 13 | a production or testing Service Worker will control this page and will serve proper 14 | versions of the scripts needed to run the interaction section below.

15 |

Select an environment:

16 |

17 | Switch to production (dialogs will appear using the browser UI)
18 | Switch to testing (dialogs will leave a trace in the console log) 19 |

20 |

What is cool about this demo is that you can see different content for the same resources 21 | if you explore the source files (Chrome only, Nightly fails to load the resource because it 22 | actually does not exists) with the developer tools.

23 |

Interaction

24 |

25 | 26 | 27 | 28 |

29 | 30 | 31 | -------------------------------------------------------------------------------- /dependency-injector/injector.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | /* global mapping */ 3 | 4 | // This script must to be added via `importScripts()` to the 5 | // service workers. See `testing-sw.js` and `production-sw.js` 6 | // first. 7 | 8 | // Make the SW control the client ASAP. 9 | function onInstall(event) { 10 | event.waitUntil(self.skipWaiting()); 11 | } 12 | 13 | function onActivate(event) { 14 | event.waitUntil(self.clients.claim()); 15 | } 16 | 17 | // Easy, simply try to find an actual resource URL for an abstract request. 18 | // If not found, let's fetch the abstract resource. In this demo, this never 19 | // fails. 20 | function onFetch(event) { 21 | var abstractResource = event.request.url; 22 | var actualResource = findActualResource(abstractResource); 23 | event.respondWith(fetch(actualResource || abstractResource)); 24 | } 25 | 26 | // Look inside mapping to get the actual resource URL for the abstract resource URL 27 | // passed as parameter. This act like the dependency factory in charge of creating 28 | // the objects to be injected. 29 | function findActualResource(abstractResource) { 30 | var actualResource; 31 | var patterns = Object.keys(mapping); 32 | for (var index = 0; index < patterns.length; index++) { 33 | var pattern = patterns[index]; 34 | // A really silly matcher just for learning purposes. 35 | if (abstractResource.endsWith(pattern)) { 36 | actualResource = mapping[pattern]; 37 | break; 38 | } 39 | } 40 | // Can return undefined if there is no actual resource. 41 | return actualResource; 42 | } 43 | -------------------------------------------------------------------------------- /dependency-injector/mock-dialogs.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | // The module mock dialogs exports a non blocking fake implementation 3 | // that simply logs the calls. 4 | window.dialogs = { 5 | alert: function(msg) { 6 | console.log('alert:', msg); 7 | }, 8 | 9 | confirm: function(msg) { 10 | console.log('confirm:', msg); 11 | return true; 12 | }, 13 | 14 | prompt: function(msg) { 15 | console.log('prompt:', msg); 16 | return 'test'; 17 | } 18 | }; 19 | })(window); 20 | -------------------------------------------------------------------------------- /dependency-injector/production-sw.js: -------------------------------------------------------------------------------- 1 | /* global importScripts, onFetch, onInstall, onActivate, mapping */ 2 | 3 | importScripts('injector.js'); 4 | 5 | // The default mapping contains the map for abstract resources to their 6 | // concrete counterparts. 7 | importScripts('default-mapping.js'); 8 | 9 | // All the functionality is in `injector.js` here we only wire the listeners. 10 | self.onfetch = onFetch; 11 | self.oninstall = onInstall; 12 | self.onactivate = onActivate; 13 | -------------------------------------------------------------------------------- /dependency-injector/testing-sw.js: -------------------------------------------------------------------------------- 1 | /* global importScripts, onFetch, onInstall, onActivate, mapping */ 2 | 3 | importScripts('injector.js'); 4 | 5 | // Load the default mapping between abstract and concrete resources. 6 | importScripts('default-mapping.js'); 7 | 8 | // But override ``utils/dialogs`` to serve the mockup instead. 9 | mapping['utils/dialogs'] = 'mock-dialogs.js'; 10 | 11 | self.onfetch = onFetch; 12 | self.oninstall = onInstall; 13 | self.onactivate = onActivate; 14 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/favicon.ico -------------------------------------------------------------------------------- /fetching/README.md: -------------------------------------------------------------------------------- 1 | # Fetching Remote Resources 2 | 3 | This recipe shows 2 standard ways of loading a remote resource and one way to use service worker as a proxy middleware. 4 | 5 | There are used 3 types of remote resources - unsecure (http), secured with `allow-origin` header, and secured without the header. 6 | 7 | ## DOM Element 8 | DOM elements are loading the resources 9 | 10 | ## Fetch 11 | Fetch issues a cors or no-cors request to each resource 12 | 13 | ## Fetch with Service Worker Proxy 14 | Fetch on the client loads a local resource `./cookbook-proxy/{full URL}` which is then translated to real URL in the service worker and forwarded to the client. 15 | 16 | ## Difficulty 17 | Intermediate 18 | 19 | ## Category 20 | General Usage 21 | -------------------------------------------------------------------------------- /fetching/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Fetching - ServiceWorker Cookbook 6 | 7 | 28 | 29 | 30 | 31 |
32 | Receiving via DOM Elements 33 | 34 | https, Access-Control-Allow-Origin=* 35 | 36 |
37 |
38 | https, no Access-Control-Allow-Origin 39 | 40 |
41 |
42 | http 43 | 44 |
45 | 46 |
47 | Receiving via fetch 48 | 49 | request type / source protocol, response header 50 | cors / https, Access-Control-Allow-Origin=* 51 |
52 |
53 | no-cors / https, Access-Control-Allow-Origin=* 54 |
55 |
56 | cors / https, no Access-Control-Allow-Origin 57 |
58 |
59 | no-cors / https, no Access-Control-Allow-Origin 60 |
61 |
62 | cors / http 63 |
64 |
65 | no-cors / http 66 |
67 | 68 | 69 |
70 | Receiving via fetch, with a serviceworker proxy 71 | This is to check if there is any difference when client is requesting a local resource and service-worker middleware is responding with a remote resource. 72 | request type / source protocol, response header 73 | 74 | cors / https, Access-Control-Allow-Origin=* 75 |
76 |
77 | no-cors / https, Access-Control-Allow-Origin=* 78 |
79 |
80 | cors / https, no Access-Control-Allow-Origin 81 |
82 |
83 | no-cors / https, no Access-Control-Allow-Origin 84 |
85 |
86 | cors / http 87 |
88 |
89 | no-cors / http 90 |
91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /fetching/service-worker.js: -------------------------------------------------------------------------------- 1 | // [Working example](/serviceworker-cookbook/fetching/). 2 | 3 | // Create a proxy for all requests to the local urls containing a 4 | // `cookbook-proxy` string. 5 | self.onfetch = function(event) { 6 | if (event.request.url.includes('cookbook-proxy')) { 7 | var init = { method: 'GET', 8 | mode: event.request.mode, 9 | cache: 'default' }; 10 | var url = event.request.url.split('cookbook-proxy/')[1]; 11 | console.log('DEBUG: proxying', url); 12 | event.respondWith(fetch(url, init)); 13 | } else { 14 | event.respondWith(fetch(event.request)); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /imgs/random/picture-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-1.png -------------------------------------------------------------------------------- /imgs/random/picture-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-10.png -------------------------------------------------------------------------------- /imgs/random/picture-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-11.png -------------------------------------------------------------------------------- /imgs/random/picture-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-12.png -------------------------------------------------------------------------------- /imgs/random/picture-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-13.png -------------------------------------------------------------------------------- /imgs/random/picture-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-14.png -------------------------------------------------------------------------------- /imgs/random/picture-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-15.png -------------------------------------------------------------------------------- /imgs/random/picture-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-16.png -------------------------------------------------------------------------------- /imgs/random/picture-17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-17.png -------------------------------------------------------------------------------- /imgs/random/picture-18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-18.png -------------------------------------------------------------------------------- /imgs/random/picture-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-19.png -------------------------------------------------------------------------------- /imgs/random/picture-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-2.png -------------------------------------------------------------------------------- /imgs/random/picture-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-20.png -------------------------------------------------------------------------------- /imgs/random/picture-21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-21.png -------------------------------------------------------------------------------- /imgs/random/picture-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-22.png -------------------------------------------------------------------------------- /imgs/random/picture-23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-23.png -------------------------------------------------------------------------------- /imgs/random/picture-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-24.png -------------------------------------------------------------------------------- /imgs/random/picture-25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-25.png -------------------------------------------------------------------------------- /imgs/random/picture-26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-26.png -------------------------------------------------------------------------------- /imgs/random/picture-27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-27.png -------------------------------------------------------------------------------- /imgs/random/picture-28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-28.png -------------------------------------------------------------------------------- /imgs/random/picture-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-29.png -------------------------------------------------------------------------------- /imgs/random/picture-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-3.png -------------------------------------------------------------------------------- /imgs/random/picture-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-30.png -------------------------------------------------------------------------------- /imgs/random/picture-31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-31.png -------------------------------------------------------------------------------- /imgs/random/picture-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-32.png -------------------------------------------------------------------------------- /imgs/random/picture-33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-33.png -------------------------------------------------------------------------------- /imgs/random/picture-34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-34.png -------------------------------------------------------------------------------- /imgs/random/picture-35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-35.png -------------------------------------------------------------------------------- /imgs/random/picture-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-36.png -------------------------------------------------------------------------------- /imgs/random/picture-37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-37.png -------------------------------------------------------------------------------- /imgs/random/picture-38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-38.png -------------------------------------------------------------------------------- /imgs/random/picture-39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-39.png -------------------------------------------------------------------------------- /imgs/random/picture-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-4.png -------------------------------------------------------------------------------- /imgs/random/picture-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-40.png -------------------------------------------------------------------------------- /imgs/random/picture-41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-41.png -------------------------------------------------------------------------------- /imgs/random/picture-42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-42.png -------------------------------------------------------------------------------- /imgs/random/picture-43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-43.png -------------------------------------------------------------------------------- /imgs/random/picture-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-44.png -------------------------------------------------------------------------------- /imgs/random/picture-45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-45.png -------------------------------------------------------------------------------- /imgs/random/picture-46.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-46.png -------------------------------------------------------------------------------- /imgs/random/picture-47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-47.png -------------------------------------------------------------------------------- /imgs/random/picture-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-48.png -------------------------------------------------------------------------------- /imgs/random/picture-49.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-49.png -------------------------------------------------------------------------------- /imgs/random/picture-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-5.png -------------------------------------------------------------------------------- /imgs/random/picture-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-50.png -------------------------------------------------------------------------------- /imgs/random/picture-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-6.png -------------------------------------------------------------------------------- /imgs/random/picture-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-7.png -------------------------------------------------------------------------------- /imgs/random/picture-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-8.png -------------------------------------------------------------------------------- /imgs/random/picture-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/imgs/random/picture-9.png -------------------------------------------------------------------------------- /immediate-claim/README.md: -------------------------------------------------------------------------------- 1 | # Immediate Claim 2 | 3 | This recipe shows how to have the service worker immediately take control of the page without waiting for a navigation event. 4 | 5 | ## Difficulty 6 | Beginner 7 | 8 | ## Use Case 9 | Basic service worker registration requires a navigation event to occur before the service worker starts working. This recipe illustrates a trick you can use for the service worker to immediately start working upon install. 10 | 11 | ## Features and Usage 12 | 13 | - Register a service worker 14 | - Delete old cache if present 15 | - Immediately claim the service worker 16 | 17 | ## Category 18 | General Usage 19 | -------------------------------------------------------------------------------- /immediate-claim/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/immediate-claim/default.jpg -------------------------------------------------------------------------------- /immediate-claim/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Immediate Claim - ServiceWorker Cookbook 6 | 7 | 14 | 15 | 16 | [Version -] 17 | 18 | A random picture by lorempixel.com: 19 | 20 | 21 | 22 | 23 | 24 | onLoad: 25 | Pending … 26 | Registered: 27 | Nothing registered 28 | onControllerchange: 29 | Not fired 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /immediate-claim/index.js: -------------------------------------------------------------------------------- 1 | // This function simulates a simple interface update code, that reflects 2 | // the current state in the UI, updating an image and a version number. 3 | function fetchUpdate() { 4 | var img = document.querySelector('#random'); 5 | img.src = img.src = 'random.jpg?' + Date.now(); 6 | fetch('./version').then(function(response) { 7 | return response.text(); 8 | }).then(function(text) { 9 | debug(text, 'version'); 10 | }); 11 | } 12 | 13 | // A ServiceWorker controls the site on load and therefor can handle offline 14 | // fallbacks. 15 | if (navigator.serviceWorker.controller) { 16 | var url = navigator.serviceWorker.controller.scriptURL; 17 | console.log('serviceWorker.controller', url); 18 | debug(url, 'onload'); 19 | fetchUpdate(); 20 | } else { 21 | // Register the ServiceWorker 22 | navigator.serviceWorker.register('service-worker.js', { 23 | scope: './' 24 | }).then(function(registration) { 25 | debug('Refresh to allow ServiceWorker to control this client', 'onload'); 26 | debug(registration.scope, 'register'); 27 | }); 28 | } 29 | 30 | navigator.serviceWorker.addEventListener('controllerchange', function() { 31 | var scriptURL = navigator.serviceWorker.controller.scriptURL; 32 | console.log('serviceWorker.onControllerchange', scriptURL); 33 | debug(scriptURL, 'oncontrollerchange'); 34 | fetchUpdate(); 35 | }); 36 | 37 | document.querySelector('#update').addEventListener('click', function() { 38 | navigator.serviceWorker.ready.then(function(registration) { 39 | registration.update().then(function() { 40 | console.log('Checked for update'); 41 | }).catch(function(error) { 42 | console.error('Update failed', error); 43 | }); 44 | }); 45 | }); 46 | 47 | function debug(message, element) { 48 | var target = document.querySelector('#' + element || 'log'); 49 | target.textContent = message; 50 | } 51 | -------------------------------------------------------------------------------- /immediate-claim/server.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var swig = require('swig'); 3 | var request = require('request'); 4 | 5 | module.exports = function autoClaim(app, route) { 6 | var tpl = swig.compileFile(path.join(__dirname, './service-worker.js')); 7 | 8 | app.get(route + 'service-worker.js', function getServiceWorker(req, res) { 9 | // Get current time with 10sec accuracy, so the generated ServiceWorker 10 | // is updated every 10sec 11 | var nowMinute = new Date(); 12 | nowMinute.setSeconds(Math.floor(nowMinute.getSeconds() / 5) * 5); 13 | // Replace {{ version }} service-worker.js 14 | var buffer = tpl({ 15 | version: [ 16 | nowMinute.getFullYear(), 17 | nowMinute.getMonth(), 18 | nowMinute.getDate(), 19 | [ 20 | nowMinute.getHours(), 21 | nowMinute.getMinutes(), 22 | nowMinute.getSeconds() 23 | ].join(':') 24 | ].join('-') 25 | }); 26 | res.type('js').send(buffer); 27 | }); 28 | 29 | app.get(route + 'random-cached.jpg', function getRandom(req, res) { 30 | request('http://lorempixel.com/200/100/').pipe(res); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /json-cache/README.md: -------------------------------------------------------------------------------- 1 | # JSON Cache 2 | 3 | This recipe illustrates fetching a JSON file during service worker installation and adding all resources to cache. This recipe also immediately claims the service worker for faster activation. 4 | 5 | ## Difficulty 6 | Beginner 7 | 8 | ## Use Case 9 | You may not always want to keep the array of files to cache in the service worker's `.js` file itself -- you may want to hold that information in another place, possibly for versioning purposes. 10 | 11 | ## Features and Usage 12 | 13 | - Register a service worker 14 | - Service worker retrieves a JSON file listing important resources to be cached 15 | - Service worker parses response and fetches required files 16 | 17 | The only action required is loading the page initially. After initial load, the service worker has installed and the assets have been cached. 18 | 19 | ## Compatibility 20 | 21 | Tests have been run in: 22 | 23 | - Firefox Nightly 44.0a1 24 | - Chrome Canary 48.0.2533.0 25 | - Opera 32.0 26 | 27 | ## Category 28 | Offline 29 | -------------------------------------------------------------------------------- /json-cache/files-to-cache.json: -------------------------------------------------------------------------------- 1 | [ 2 | "index.html", 3 | "index.js", 4 | "random-1.png", 5 | "random-2.png", 6 | "random-3.png", 7 | "random-4.png", 8 | "random-5.png", 9 | "random-6.png" 10 | ] -------------------------------------------------------------------------------- /json-cache/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSON Cache - ServiceWorker Cookbook 6 | 7 | 19 | 20 | 21 | 22 |

This demo uses a service worker which loads a JSON file representing files to be cached by the Service Worker. Once the JSON file is loaded and parsed, files are placed into the cache via the Service Worker.

23 | 24 |

Assets to Cache

25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /json-cache/index.js: -------------------------------------------------------------------------------- 1 | // Register the ServiceWorker 2 | navigator.serviceWorker.register('service-worker.js', { 3 | scope: '.' 4 | }).then(function(registration) { 5 | console.log('The service worker has been registered ', registration); 6 | }); 7 | -------------------------------------------------------------------------------- /json-cache/random-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/json-cache/random-1.png -------------------------------------------------------------------------------- /json-cache/random-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/json-cache/random-2.png -------------------------------------------------------------------------------- /json-cache/random-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/json-cache/random-3.png -------------------------------------------------------------------------------- /json-cache/random-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/json-cache/random-4.png -------------------------------------------------------------------------------- /json-cache/random-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/json-cache/random-5.png -------------------------------------------------------------------------------- /json-cache/random-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/json-cache/random-6.png -------------------------------------------------------------------------------- /json-cache/service-worker.js: -------------------------------------------------------------------------------- 1 | // [Working example](/serviceworker-cookbook/json-cache/). 2 | 3 | var CACHE_NAME = 'dependencies-cache'; 4 | 5 | self.addEventListener('install', function(event) { 6 | // Perform the install step: 7 | // * Load a JSON file from server 8 | // * Parse as JSON 9 | // * Add files to the cache list 10 | 11 | // Message to simply show the lifecycle flow 12 | console.log('[install] Kicking off service worker registration!'); 13 | 14 | event.waitUntil( 15 | caches.open(CACHE_NAME) 16 | .then(function(cache) { 17 | // With the cache opened, load a JSON file containing an array of files to be cached 18 | return fetch('files-to-cache.json').then(function(response) { 19 | // Once the contents are loaded, convert the raw text to a JavaScript object 20 | return response.json(); 21 | }).then(function(files) { 22 | // Use cache.addAll just as you would a hardcoded array of items 23 | console.log('[install] Adding files from JSON file: ', files); 24 | return cache.addAll(files); 25 | }); 26 | }) 27 | .then(function() { 28 | // Message to simply show the lifecycle flow 29 | console.log( 30 | '[install] All required resources have been cached;', 31 | 'the Service Worker was successfully installed!' 32 | ); 33 | 34 | // Force activation 35 | return self.skipWaiting(); 36 | }) 37 | ); 38 | }); 39 | 40 | self.addEventListener('fetch', function(event) { 41 | event.respondWith( 42 | caches.match(event.request) 43 | .then(function(response) { 44 | // Cache hit - return the response from the cached version 45 | if (response) { 46 | console.log( 47 | '[fetch] Returning from Service Worker cache: ', 48 | event.request.url 49 | ); 50 | return response; 51 | } 52 | 53 | // Not in cache - return the result from the live server 54 | // `fetch` is essentially a "fallback" 55 | console.log('[fetch] Returning from server: ', event.request.url); 56 | return fetch(event.request); 57 | } 58 | ) 59 | ); 60 | }); 61 | 62 | self.addEventListener('activate', function(event) { 63 | // Message to simply show the lifecycle flow 64 | console.log('[activate] Activating service worker!'); 65 | 66 | // Claim the service work for this client, forcing `controllerchange` event 67 | console.log('[activate] Claiming this service worker!'); 68 | event.waitUntil(self.clients.claim()); 69 | }); 70 | -------------------------------------------------------------------------------- /live-flowchart/README.md: -------------------------------------------------------------------------------- 1 | # Live Flowchart 2 | 3 | This recipe provides a way to learn how to use service workers (SW) through showing [the flow diagram of SW workflow explained on the Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers), and logging on screen the steps taken by a real Web App running service workers. 4 | 5 | ## Difficulty 6 | Advanced 7 | 8 | ## Screenshots 9 | 10 | Register + Unregister 11 | 12 | ![Screenshot](https://cdn.rawgit.com/mozilla/serviceworker-cookbook/master/live-flowchart/register-unregister.png) 13 | 14 | Active service worker on page load + unregister 15 | 16 | ![Screenshot](https://cdn.rawgit.com/mozilla/serviceworker-cookbook/master/live-flowchart/active-service-worker-unregister.png) 17 | 18 | Wrong service worker scriptURL 19 | 20 | ![Screenshot](https://cdn.rawgit.com/mozilla/serviceworker-cookbook/master/live-flowchart/wrong-scriptURL.png) 21 | 22 | Security error 23 | 24 | ![Screenshot](https://cdn.rawgit.com/mozilla/serviceworker-cookbook/master/live-flowchart/security-error.png) 25 | 26 | ## Features and Usage 27 | 28 | Features are: 29 | 30 | - Register a service worker 31 | - Reload document 32 | - Unregister the service worker 33 | 34 | The features coincide to the buttons at the top of the page, which can be pressed in a whatever order. You can also specify the SW scriptURL and scope to simulate different test cases. 35 | 36 | Usage: 37 | 38 | - press buttons 39 | - read the logs 40 | - take actions in case (e.g. open about://serviceworkers) 41 | - hack the code and see what happens 42 | 43 | ## How to Read the Logs 44 | 45 | There are two logs to read: 46 | 47 | - the HTML log 48 | - the browser log 49 | 50 | In the HTML log colors mean different log levels: 51 | 52 | - red => error 53 | - yellow => warn 54 | - green => info 55 | - white => log 56 | - gray => debug 57 | 58 | The browser log prints: 59 | 60 | - the same messages printed on the HTML log, using the same log level 61 | - the service worker log (since service workers can't access the DOM) 62 | 63 | ## Compatibility 64 | 65 | Tests have been run in: 66 | 67 | - Firefox Nightly 44.0a1 (2015-10-12) 68 | - Chrome Canary 48.0.2533.0 69 | - Opera 32.0 70 | 71 | on a machine running Mac OS X 10.8.5 72 | 73 | Notes: 74 | 75 | - the browser has to support ES6 76 | 77 | ## What's Next / Contributions 78 | 79 | - responsive to work on mobile 80 | - have the flowchart build in SVG and visualize the service worker states 81 | - add specs / automatic tests for existing features 82 | - add specs / automatic tests for new features in a BDD/TDD fashion 83 | - do something oninstall 84 | - do something onactivate 85 | - do something onfetch 86 | - provide a button to simulate the offline network status 87 | - provide a button to simulate the lie-fi network status (very low connectivity but not completely offline) 88 | 89 | ## Category 90 | General Usage 91 | -------------------------------------------------------------------------------- /live-flowchart/active-service-worker-unregister.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/live-flowchart/active-service-worker-unregister.png -------------------------------------------------------------------------------- /live-flowchart/active-service-worker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/live-flowchart/active-service-worker.png -------------------------------------------------------------------------------- /live-flowchart/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Live flowchart 5 | 6 | 7 | 8 |
9 |
10 |

Using Service Workers

11 | 12 | sw-flowchart 13 |
14 | 15 |
16 |
17 | Service Worker 18 | 19 | Options: 20 | 21 | This works: './service-worker.js' (in scope './') 22 |
23 |
24 |
    25 |
  • 26 |
  • 27 |
  • 28 |
29 |
30 |
31 |
32 | @franciov 33 |
34 |
35 | 36 | 37 | 38 | 39 |
40 | 41 | -------------------------------------------------------------------------------- /live-flowchart/logger.js: -------------------------------------------------------------------------------- 1 | 2 | //

Logger

3 | // Helper to log to the browser console and an HTML log 4 | var Logger = { 5 | 6 | // The HTML DOM Object to log to 7 | logDomObj: document.querySelector('#log'), 8 | 9 | //

Debug log

10 | // Log using a debug style 11 | debug: function debug(message) { 12 | this.writeLog(message, { debug: true }); 13 | }, 14 | 15 | //

Info log

16 | // Log using a info style 17 | info: function info(message) { 18 | this.writeLog(message, { info: true }); 19 | }, 20 | 21 | //

Default log

22 | // Log using a log style 23 | log: function log(message) { 24 | this.writeLog(message); 25 | }, 26 | 27 | //

Warning log

28 | // Log using a warning style 29 | warn: function warn(message) { 30 | this.writeLog(message, { warn: true }); 31 | }, 32 | 33 | //

Error log

34 | // Log using a error style 35 | error: function error(message) { 36 | this.writeLog(message, { error: true }); 37 | }, 38 | 39 | //

New section

40 | // Log a section separator to the browser console and the HTML log 41 | newSection: function newSection() { 42 | var separator = '----------'; 43 | var newLine = document.createElement('div'); 44 | newLine.innerHTML = separator; 45 | this.logDomObj.appendChild(newLine); 46 | console.log(separator); 47 | }, 48 | 49 | //

Write log

50 | // Internal method to log to the browser console and the HTML log 51 | writeLog: function writeLog(message, level) { 52 | var logMessage = document.createElement('div'); 53 | 54 | if (message) { 55 | // add message to the HTML node 56 | logMessage.innerHTML = message; 57 | } else { 58 | // make sure to log all messages on the HTML log 59 | if (message === null) { 60 | logMessage.innerHTML = 'null'; 61 | } else if (message === undefined) { 62 | logMessage.innerHTML = 'undefined'; 63 | } 64 | } 65 | 66 | // level check 67 | if (level) { 68 | if (level.error) { 69 | logMessage.setAttribute('class', 'error'); 70 | console.error(message); 71 | } else if (level.warn) { 72 | logMessage.setAttribute('class', 'warning'); 73 | console.warn(message); 74 | } else if (level.info) { 75 | logMessage.setAttribute('class', 'info'); 76 | console.info(message); 77 | } else if (level.debug) { 78 | logMessage.setAttribute('class', 'debug'); 79 | console.debug(message); 80 | } 81 | } else { 82 | // log into the browser console 83 | console.log(message); 84 | } 85 | 86 | // log into the HTML console 87 | this.logDomObj.appendChild(logMessage); 88 | this.logDomObj.scrollTop = this.logDomObj.scrollHeight; 89 | }, 90 | 91 | }; 92 | 93 | // log a new section 94 | Logger.newSection(); 95 | -------------------------------------------------------------------------------- /live-flowchart/no-active-service-worker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/live-flowchart/no-active-service-worker.png -------------------------------------------------------------------------------- /live-flowchart/register-unregister.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/live-flowchart/register-unregister.png -------------------------------------------------------------------------------- /live-flowchart/register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/live-flowchart/register.png -------------------------------------------------------------------------------- /live-flowchart/security-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/live-flowchart/security-error.png -------------------------------------------------------------------------------- /live-flowchart/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | } 3 | 4 | div.container { 5 | display: flex; 6 | width: 100%; 7 | } 8 | 9 | a { 10 | color: #66CC00; 11 | } 12 | 13 | div.flowchart { 14 | margin-right: 3em; 15 | width: 1000px; 16 | } 17 | 18 | div.flowchart h1 { 19 | margin-left: 15px; 20 | } 21 | 22 | div.flowchart img { 23 | width: 100%; 24 | } 25 | 26 | div.playground { 27 | width: 100%; 28 | } 29 | 30 | div.playground .inputs { 31 | margin: 1em 0; 32 | } 33 | 34 | div.playground .inputs span.title { 35 | font-weight: bold; 36 | } 37 | 38 | div.playground .inputs span { 39 | } 40 | 41 | div.playground .inputs input { 42 | margin-left: 0.5em; 43 | margin-right: 1em; 44 | } 45 | 46 | div.playground .actions { 47 | margin: 1em 0; 48 | } 49 | 50 | div.playground .actions ul { 51 | width: 100%%; 52 | margin: 0; 53 | padding: 0; 54 | } 55 | 56 | div.playground .actions ul li { 57 | display: inline-block; 58 | width: 33%; 59 | margin: 0; 60 | padding: 0; 61 | } 62 | 63 | div.playground .actions ul li button { 64 | width: 100%; 65 | min-height: 3em; 66 | background-color: #66CC00; 67 | font-weight: bold; 68 | cursor: pointer; 69 | } 70 | 71 | div.playground .actions button:disabled { 72 | cursor: auto; 73 | } 74 | 75 | div.playground .log { 76 | overflow: scroll; 77 | background: black; 78 | color: white; 79 | padding: 1em; 80 | font-family: monospace; 81 | line-height: 150%; 82 | height: 80vh; 83 | } 84 | 85 | div.playground .log .debug { 86 | color: gray; 87 | } 88 | 89 | div.playground .log .info { 90 | color: #66CC00; 91 | } 92 | 93 | div.playground .log .warning { 94 | color: yellow; 95 | } 96 | 97 | div.playground .log .error { 98 | color: red; 99 | } 100 | 101 | div.playground .button { 102 | width: auto; 103 | height: 3em; 104 | } 105 | 106 | div.credits { 107 | text-align: right; 108 | margin-right: 1em; 109 | } 110 | -------------------------------------------------------------------------------- /live-flowchart/sw-flowchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/live-flowchart/sw-flowchart.png -------------------------------------------------------------------------------- /live-flowchart/wrong-scriptURL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/live-flowchart/wrong-scriptURL.png -------------------------------------------------------------------------------- /load-balancer/README.md: -------------------------------------------------------------------------------- 1 | # Load balancer 2 | This recipe shows a Service Worker containing network logic to dynamically select the best content provider accordingly to server availability. 3 | 4 | ## Difficulty 5 | Intermediate 6 | 7 | ## Use case 8 | As a service provider, I want to provide the best source in terms of availability for a selected resource. 9 | 10 | ## Solution 11 | Use a Service Worker to intercept the requests to the resources and select the proper content provider accordingly to their availability. 12 | 13 | ## Category 14 | Beyond Offline 15 | -------------------------------------------------------------------------------- /load-balancer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Load balancer - ServiceWorker Cookbook 6 | 7 | 8 | 9 |

Resources

10 |

11 | Choose from one of the images to be loaded: 12 |

13 |

14 | Notice the white label in the image which is telling you the server it comes from. 15 |

16 |

17 | 23 |

24 |

25 |

Configuration

26 |

Configure the load of the content providers.

27 |

Servers set to:

28 |
29 |

30 |

31 |

32 | 33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /load-balancer/index.js: -------------------------------------------------------------------------------- 1 | // A convenient shortcut for `document.querySelector()` 2 | var $ = document.querySelector.bind(document); // eslint-disable-line id-length 3 | 4 | var serverLoadInputs = [ 5 | $('#load-1'), 6 | $('#load-2'), 7 | $('#load-3') 8 | ]; 9 | 10 | // Register the worker and enable image selection. 11 | navigator.serviceWorker.register('service-worker.js'); 12 | navigator.serviceWorker.ready.then(enableUI); 13 | 14 | function enableUI() { 15 | getServerLoads().then(function(loads) { 16 | serverLoadInputs.forEach(function(input, index) { 17 | input.value = loads[index]; 18 | input.disabled = false; 19 | }); 20 | $('#image-selector').disabled = false; 21 | }); 22 | } 23 | 24 | function getServerLoads() { 25 | return fetch(addSession('./server-loads/')).then(function(response) { 26 | return response.json(); 27 | }); 28 | } 29 | 30 | // When _clicking_ configure button, send the load values to the back-end to 31 | // simulate server loads. 32 | $('#load-configuration').onsubmit = function(event) { 33 | // Avoid navigation 34 | event.preventDefault(); 35 | 36 | // Get fake levels from inputs. 37 | var loads = serverLoadInputs.map(function(input) { 38 | return parseInt(input.value, 10); 39 | }); 40 | 41 | // Send the request to configure the load levels serializing the body 42 | // and setting the content type header properly. 43 | fetch(addSession('./server-loads'), { 44 | method: 'PUT', 45 | headers: { 'Content-Type': 'application/json' }, 46 | body: JSON.stringify(loads) 47 | }).then(function(response) { 48 | return response.json(); 49 | }).then(function(result) { 50 | $('#loads-label').textContent = result; 51 | }); 52 | }; 53 | 54 | // Simply change the source for the image. 55 | $('#image-selector').onchange = function() { 56 | var imgUrl = $('select').value; 57 | if (imgUrl) { 58 | // The bumping parameter `_b` is just to avoid HTTP cache. 59 | $('img').src = addSession(imgUrl) + '&_b=' + Date.now(); 60 | 61 | // Specifically for the cookbook :( 62 | $('img').onload = function() { 63 | if (window.parent !== window) { 64 | window.parent 65 | .document.body.dispatchEvent(new CustomEvent('iframeresize')); 66 | } 67 | }; 68 | } 69 | }; 70 | 71 | // Add the session parameter to an URL. 72 | function addSession(url) { 73 | return url + '?session=' + getSession(); 74 | } 75 | 76 | // A simple session manager based on a random string stored in the localStorage 77 | function getSession() { 78 | var session = localStorage.getItem('session'); 79 | if (!session) { 80 | session = '' + Date.now() + '-' + Math.random(); 81 | localStorage.setItem('session', session); 82 | } 83 | return session; 84 | } 85 | -------------------------------------------------------------------------------- /load-balancer/server-1/imgs/a.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/load-balancer/server-1/imgs/a.jpeg -------------------------------------------------------------------------------- /load-balancer/server-1/imgs/b.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/load-balancer/server-1/imgs/b.jpeg -------------------------------------------------------------------------------- /load-balancer/server-1/imgs/c.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/load-balancer/server-1/imgs/c.jpeg -------------------------------------------------------------------------------- /load-balancer/server-2/imgs/a.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/load-balancer/server-2/imgs/a.jpeg -------------------------------------------------------------------------------- /load-balancer/server-2/imgs/b.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/load-balancer/server-2/imgs/b.jpeg -------------------------------------------------------------------------------- /load-balancer/server-2/imgs/c.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/load-balancer/server-2/imgs/c.jpeg -------------------------------------------------------------------------------- /load-balancer/server-3/imgs/a.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/load-balancer/server-3/imgs/a.jpeg -------------------------------------------------------------------------------- /load-balancer/server-3/imgs/b.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/load-balancer/server-3/imgs/b.jpeg -------------------------------------------------------------------------------- /load-balancer/server-3/imgs/c.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/load-balancer/server-3/imgs/c.jpeg -------------------------------------------------------------------------------- /load-balancer/server.js: -------------------------------------------------------------------------------- 1 | 2 | var bodyParser = require('body-parser'); 3 | 4 | // Simple session handling with a hash of sessions. 5 | var sessions = {}; 6 | 7 | // A simple API to simulate load in simulated content provided servers. 8 | module.exports = function(app, route) { 9 | // Allow express to parse the body of the requests. 10 | app.use(bodyParser.json()); 11 | 12 | // Configure servers loads. 13 | app.put(route + 'server-loads', function(req, res) { 14 | var loads = req.body; 15 | sessions[req.query.session] = loads; 16 | res.status(201).json(loads); 17 | }); 18 | 19 | // Query servers loads. 20 | app.get(route + 'server-loads', function(req, res) { 21 | var loads = sessions[req.query.session] || [50, 75, 25]; 22 | res.json(loads); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /load-balancer/service-worker.js: -------------------------------------------------------------------------------- 1 | // The code in `oninstall` and `onactivate` force the service worker to 2 | // control the clients ASAP. 3 | self.oninstall = function(event) { 4 | event.waitUntil(self.skipWaiting()); 5 | }; 6 | 7 | self.onactivate = function(event) { 8 | event.waitUntil(self.clients.claim()); 9 | }; 10 | 11 | // When fetching, distinguish if this is a resource fetch. If so, 12 | // apply the server selection algorithm. Else, let the request reach the 13 | // network. Could should be autoexplanatory. 14 | self.onfetch = function(event) { 15 | var request = event.request; 16 | if (isResource(request)) { 17 | event.respondWith(fetchFromBestServer(request)); 18 | } else { 19 | event.respondWith(fetch(request)); 20 | } 21 | }; 22 | 23 | // A request is a resource request if it is a `GET` for something inside `imgs`. 24 | function isResource(request) { 25 | return request.url.match(/\/imgs\/.*$/) && request.method === 'GET'; 26 | } 27 | 28 | // Fetching from the best server consists of getting the server loads, 29 | // selecting the server with lowest load, and compose a new request to 30 | // find the resource in the selected server. 31 | function fetchFromBestServer(request) { 32 | var session = request.url.match(/\?session=([^&]*)/)[1]; 33 | return getServerLoads(session) 34 | .then(selectServer) 35 | .then(function(serverUrl) { 36 | // Get the resource path and combine with `serverUrl` to get 37 | // the resource URL but **in the selected server**. 38 | var resourcePath = request.url.match(/\/imgs\/[^?]*/)[0]; 39 | var serverRequest = new Request(serverUrl + resourcePath); 40 | return fetch(serverRequest); 41 | }); 42 | } 43 | 44 | // Query the back-end for servers loads. 45 | function getServerLoads(session) { 46 | return fetch('./server-loads?session=' + session).then(function(response) { 47 | return response.json(); 48 | }); 49 | } 50 | 51 | // Get the server with minimum load and return its URL. In a real 52 | // scenario this could return servers in other domains, just remember 53 | // to set the CORS headers properly. 54 | function selectServer(serverLoads) { 55 | // Not very efficient but super-clear way of finding the index of the server 56 | // with minimum load. 57 | var min = Math.min.apply(Math, serverLoads); 58 | var serverIndex = serverLoads.indexOf(min); 59 | 60 | // Servers are 1, 2, 3... 61 | return './server-' + (serverIndex + 1); 62 | } 63 | -------------------------------------------------------------------------------- /local-download/README.md: -------------------------------------------------------------------------------- 1 | # Local Download 2 | 3 | Allow a user to "download" a file that's been generated on the client side. 4 | 5 | ## Difficulty 6 | Beginner 7 | 8 | ## Use Case 9 | Often it will be necessary to include a download feature in a single page application - for example, a drawing program might want the ability to export as SVG, or a bitmap format generated client side. 10 | 11 | ## Solution 12 | Using the serviceworker to intercept a form POST operation, pull the data from the body of the request. The data can then be put in a request that behaves as a downloadable attachment, which is fed back to the client as a file. The file will appear to have been downloaded, without any round trips to the server necessary. 13 | 14 | ## Category 15 | Beyond Offline 16 | -------------------------------------------------------------------------------- /local-download/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Local Download - ServiceWorker Cookbook 6 | 7 | 14 | 15 | 16 |
17 | 21 | 27 |
28 | 29 |
30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /local-download/index.js: -------------------------------------------------------------------------------- 1 | // Register the ServiceWorker 2 | navigator.serviceWorker.register('service-worker.js', { 3 | scope: '.' 4 | }) 5 | -------------------------------------------------------------------------------- /local-download/service-worker.js: -------------------------------------------------------------------------------- 1 | // [Working example](/serviceworker-cookbook/local-download/). 2 | 3 | // Listen on fetch events for posts to download-file 4 | self.addEventListener('fetch', function(event) { 5 | // If the request is going to the download-file endpoint, parse the post data and return a file. 6 | // This can be paired with a server side function with the same behavior as a fallback. 7 | if(event.request.url.indexOf("download-file") !== -1) { 8 | event.respondWith(event.request.formData().then(function (formdata){ 9 | var filename = formdata.get("filename"); 10 | var body = formdata.get("filebody"); 11 | var response = new Response(body); 12 | response.headers.append('Content-Disposition', 'attachment; filename="' + filename + '"'); 13 | return response; 14 | })); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /message-relay/README.md: -------------------------------------------------------------------------------- 1 | # Message Relay 2 | 3 | This recipe shows how to communicate between the service worker and a page and shows how to use a service worker to relay messages between pages. 4 | 5 | ## Difficulty 6 | Beginner 7 | 8 | ## Use Case 9 | The `postMessage` API is brilliant for passing messages between windows and `iframe`s, and now we can use the service worker's `message` event to act as a messenger. 10 | 11 | ## Features and Usage 12 | 13 | - postMessage API 14 | - Service worker registration 15 | 16 | ## Category 17 | General Usage 18 | -------------------------------------------------------------------------------- /message-relay/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Message Relay - ServiceWorker Cookbook 6 | 7 | 14 | 15 | 16 | 17 |
18 | Service Worker Status: not supported 19 |
20 | 21 | Open another window with this page and type some text in below to postMessage it to the ServiceWorker which will forward the message along. 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /message-relay/index.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | // Only setup the demo if service workers are supported. 3 | if (navigator.serviceWorker) { 4 | // Get the DOM nodes for our UI. 5 | var message = document.getElementById('message'); 6 | var received = document.getElementById('received'); 7 | var status = document.getElementById('status'); 8 | var inbox = {}; 9 | 10 | // Give an indicator that service workers are supported. 11 | status.textContent = 'supported'; 12 | 13 | navigator.serviceWorker.register('service-worker.js'); 14 | 15 | // Listen for any messages from the service worker. 16 | navigator.serviceWorker.addEventListener('message', function(event) { 17 | // A message has been received, now show the message on the page. 18 | var clientId = event.data.client; 19 | var node; 20 | // A message from this client hasn't been received before, so we need to 21 | // setup a place to show its messages. 22 | if (!inbox[clientId]) { 23 | node = document.createElement('div'); 24 | received.appendChild(node); 25 | inbox[clientId] = node; 26 | } 27 | // Show the message. 28 | node = inbox[clientId]; 29 | node.textContent = 'Client ' + clientId + ' says: ' + event.data.message; 30 | }); 31 | 32 | message.addEventListener('input', function() { 33 | // There isn't always a service worker to send a message to. This can happen 34 | // when the page is force reloaded. 35 | if (!navigator.serviceWorker.controller) { 36 | status.textContent = 'error: no controller'; 37 | return; 38 | } 39 | // Send the message to the service worker. 40 | navigator.serviceWorker.controller.postMessage(message.value); 41 | }); 42 | } 43 | })(); 44 | -------------------------------------------------------------------------------- /message-relay/service-worker.js: -------------------------------------------------------------------------------- 1 | // Listen for messages from clients. 2 | self.addEventListener('message', function(event) { 3 | // Get all the connected clients and forward the message along. 4 | var promise = self.clients.matchAll() 5 | .then(function(clientList) { 6 | // event.source.id contains the ID of the sender of the message. 7 | var senderID = event.source.id; 8 | 9 | clientList.forEach(function(client) { 10 | // Skip sending the message to the client that sent it. 11 | if (client.id === senderID) { 12 | return; 13 | } 14 | client.postMessage({ 15 | client: senderID, 16 | message: event.data 17 | }); 18 | }); 19 | }); 20 | 21 | // If event.waitUntil is defined, use it to extend the 22 | // lifetime of the Service Worker. 23 | if (event.waitUntil) { 24 | event.waitUntil(promise); 25 | } 26 | }); 27 | 28 | // Immediately claim any new clients. This is not needed to send messages, but 29 | // makes for a better demo experience since the user does not need to refresh. 30 | // A more complete example of this given in the immediate-claim recipe. 31 | self.addEventListener('activate', function(event) { 32 | event.waitUntil(self.clients.claim()); 33 | }); 34 | -------------------------------------------------------------------------------- /offline-fallback/README.md: -------------------------------------------------------------------------------- 1 | # Offline Fallback 2 | 3 | This recipe shows how to serve content from the cache when the user is offline. 4 | 5 | ## Difficulty 6 | Beginner 7 | 8 | ## Use Case 9 | There's a problem with relying on the browser's default "you are offline" message: 10 | 11 | - The screen isn't branded the same as your app 12 | - The screen looks different in each browser 13 | - The message may not be localized 14 | 15 | A better solution would be to show the user a custom offline snippet served from the cache. 16 | 17 | ## Features and Usage 18 | 19 | - Register a service worker 20 | - Cache an `offline.html` file 21 | - Serve the `offline.html` file content if the network cannot be reached 22 | 23 | ## Category 24 | Offline 25 | -------------------------------------------------------------------------------- /offline-fallback/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Offline Fallback - ServiceWorker Cookbook 6 | 7 | 14 | 15 | 16 | Yay, you are online! 17 | Now go offline and refresh! 18 | 19 | Registered ServiceWorker: Did not register 20 | Active Controller: Did not activate 21 | 22 | ServiceWorker logs: 23 |
Here be logs:
24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /offline-fallback/index.js: -------------------------------------------------------------------------------- 1 | if (navigator.serviceWorker.controller) { 2 | // A ServiceWorker controls the site on load and therefor can handle offline 3 | // fallbacks. 4 | debug( 5 | navigator.serviceWorker.controller.scriptURL + 6 | ' (onload)', 'controller' 7 | ); 8 | debug( 9 | 'An active service worker controller was found, ' + 10 | 'no need to register' 11 | ); 12 | } else { 13 | // Register the ServiceWorker 14 | navigator.serviceWorker.register('service-worker.js', { 15 | scope: './' 16 | }).then(function(reg) { 17 | debug(reg.scope, 'register'); 18 | debug('Service worker change, registered the service worker'); 19 | }); 20 | } 21 | 22 | // The refresh link needs a cache-busting URL parameter 23 | document.querySelector('#refresh').search = Date.now(); 24 | 25 | // Debug helper 26 | function debug(message, element, append) { 27 | var target = document.querySelector('#' + (element || 'log')); 28 | target.textContent = message + ((append) ? ('/n' + target.textContent) : ''); 29 | } 30 | 31 | // Allow for "replaying" this example 32 | document.getElementById('clearAndReRegister').addEventListener('click', 33 | function() { 34 | navigator.serviceWorker.getRegistration().then(function(registration) { 35 | registration.unregister(); 36 | window.location.reload(); 37 | }); 38 | } 39 | ); 40 | -------------------------------------------------------------------------------- /offline-fallback/offline.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Not Connected 5 | 6 | 13 | 14 | 15 | Oh no, you seem to be offline :( 16 |

Check your internet connection and refresh.

17 | 18 | 19 | -------------------------------------------------------------------------------- /offline-fallback/service-worker.js: -------------------------------------------------------------------------------- 1 | // [Working example](/serviceworker-cookbook/offline-fallback/). 2 | 3 | self.addEventListener('install', function(event) { 4 | // Put `offline.html` page into cache 5 | var offlineRequest = new Request('offline.html'); 6 | event.waitUntil( 7 | fetch(offlineRequest).then(function(response) { 8 | return caches.open('offline').then(function(cache) { 9 | console.log('[oninstall] Cached offline page', response.url); 10 | return cache.put(offlineRequest, response); 11 | }); 12 | }) 13 | ); 14 | }); 15 | 16 | self.addEventListener('fetch', function(event) { 17 | // Only fall back for HTML documents. 18 | var request = event.request; 19 | // && request.headers.get('accept').includes('text/html') 20 | if (request.method === 'GET') { 21 | // `fetch()` will use the cache when possible, to this examples 22 | // depends on cache-busting URL parameter to avoid the cache. 23 | event.respondWith( 24 | fetch(request).catch(function(error) { 25 | // `fetch()` throws an exception when the server is unreachable but not 26 | // for valid HTTP responses, even `4xx` or `5xx` range. 27 | console.error( 28 | '[onfetch] Failed. Serving cached offline fallback ' + 29 | error 30 | ); 31 | return caches.open('offline').then(function(cache) { 32 | return cache.match('offline.html'); 33 | }); 34 | }) 35 | ); 36 | } 37 | // Any other handlers come here. Without calls to `event.respondWith()` the 38 | // request will be handled without the ServiceWorker. 39 | }); 40 | -------------------------------------------------------------------------------- /offline-status/README.md: -------------------------------------------------------------------------------- 1 | # Offline Status 2 | 3 | This basic recipe illustrates caching critical resources for offline use and then notifying the user that they may go offline and enjoy the same experience. 4 | 5 | ## Difficulty 6 | Beginner 7 | 8 | ## Use Case 9 | The most basic of service worker use cases: caching a set of files so that the user may go offline. The added value in this demo is showing a notification to the user that they can safely go offline. 10 | 11 | ## Features and Usage 12 | 13 | - Register a service worker 14 | - Monitor the cached status of required resources 15 | - Notification to user when resources have been cached 16 | 17 | The only action required is loading the page initially. After initial load, the service worker has installed and the assets have been cached. 18 | 19 | ## Compatibility 20 | 21 | Tests have been run in: 22 | 23 | - Firefox Nightly 44.0a1 24 | - Chrome Canary 48.0.2533.0 25 | - Opera 32.0 26 | 27 | ## Category 28 | Offline 29 | -------------------------------------------------------------------------------- /offline-status/app.js: -------------------------------------------------------------------------------- 1 | // This file is required to make the "app" work offline 2 | 3 | document.getElementById('randomButton').addEventListener('click', function() { 4 | var image = document.getElementById('logoImage'); 5 | var currentIndex = Number(image.src.match('random-([0-9])')[1]); 6 | var newIndex = getRandomNumber(); 7 | 8 | // Ensure that we receive a different image than the current 9 | while (newIndex === currentIndex) { 10 | newIndex = getRandomNumber(); 11 | } 12 | 13 | image.src = 'random-' + newIndex + '.png'; 14 | 15 | function getRandomNumber() { 16 | return Math.floor(Math.random() * 6) + 1; 17 | } 18 | }); 19 | 20 | document.getElementById('clearAndReRegister').addEventListener('click', 21 | function() { 22 | navigator.serviceWorker.getRegistration().then(function(registration) { 23 | registration.unregister(); 24 | window.location.reload(); 25 | }); 26 | } 27 | ); 28 | -------------------------------------------------------------------------------- /offline-status/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Offline Status - ServiceWorker Cookbook 6 | 7 | 8 | 15 | 16 | 17 | 18 |

The goal of this recipe is to create an app which can be used both online and offline with the help of a ServiceWorker. Once the ServiceWorker has cached assets and becomes activated, the user will receive a notification that they can then go offline and use the app!

19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /offline-status/index.js: -------------------------------------------------------------------------------- 1 | // Register the ServiceWorker 2 | navigator.serviceWorker.register('service-worker.js', { 3 | scope: '.' 4 | }).then(function(registration) { 5 | console.log('The service worker has been registered ', registration); 6 | }); 7 | 8 | // Listen for claiming of our ServiceWorker 9 | navigator.serviceWorker.addEventListener('controllerchange', function(event) { 10 | console.log( 11 | '[controllerchange] A "controllerchange" event has happened ' + 12 | 'within navigator.serviceWorker: ', event 13 | ); 14 | 15 | // Listen for changes in the state of our ServiceWorker 16 | navigator.serviceWorker.controller.addEventListener('statechange', 17 | function() { 18 | console.log('[controllerchange][statechange] ' + 19 | 'A "statechange" has occured: ', this.state 20 | ); 21 | 22 | // If the ServiceWorker becomes "activated", let the user know they can go offline! 23 | if (this.state === 'activated') { 24 | // Show the "You may now use offline" notification 25 | document.getElementById('offlineNotification') 26 | .classList.remove('hidden'); 27 | } 28 | } 29 | ); 30 | }); 31 | -------------------------------------------------------------------------------- /offline-status/random-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/offline-status/random-1.png -------------------------------------------------------------------------------- /offline-status/random-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/offline-status/random-2.png -------------------------------------------------------------------------------- /offline-status/random-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/offline-status/random-3.png -------------------------------------------------------------------------------- /offline-status/random-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/offline-status/random-4.png -------------------------------------------------------------------------------- /offline-status/random-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/offline-status/random-5.png -------------------------------------------------------------------------------- /offline-status/random-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/offline-status/random-6.png -------------------------------------------------------------------------------- /offline-status/service-worker.js: -------------------------------------------------------------------------------- 1 | // [Working example](/serviceworker-cookbook/offline-status/). 2 | 3 | var CACHE_NAME = 'dependencies-cache'; 4 | 5 | // Files required to make this app work offline 6 | var REQUIRED_FILES = [ 7 | 'random-1.png', 8 | 'random-2.png', 9 | 'random-3.png', 10 | 'random-4.png', 11 | 'random-5.png', 12 | 'random-6.png', 13 | 'style.css', 14 | 'index.html', 15 | 'index.js', 16 | 'app.js' 17 | ]; 18 | 19 | self.addEventListener('install', function(event) { 20 | // Perform install step: loading each required file into cache 21 | event.waitUntil( 22 | caches.open(CACHE_NAME) 23 | .then(function(cache) { 24 | // Add all offline dependencies to the cache 25 | console.log('[install] Caches opened, adding all core components' + 26 | 'to cache'); 27 | return cache.addAll(REQUIRED_FILES); 28 | }) 29 | .then(function() { 30 | console.log('[install] All required resources have been cached, ' + 31 | 'we\'re good!'); 32 | return self.skipWaiting(); 33 | }) 34 | ); 35 | }); 36 | 37 | self.addEventListener('fetch', function(event) { 38 | event.respondWith( 39 | caches.match(event.request) 40 | .then(function(response) { 41 | // Cache hit - return the response from the cached version 42 | if (response) { 43 | console.log( 44 | '[fetch] Returning from ServiceWorker cache: ', 45 | event.request.url 46 | ); 47 | return response; 48 | } 49 | 50 | // Not in cache - return the result from the live server 51 | // `fetch` is essentially a "fallback" 52 | console.log('[fetch] Returning from server: ', event.request.url); 53 | return fetch(event.request); 54 | } 55 | ) 56 | ); 57 | }); 58 | 59 | self.addEventListener('activate', function(event) { 60 | console.log('[activate] Activating ServiceWorker!'); 61 | 62 | // Calling claim() to force a "controllerchange" event on navigator.serviceWorker 63 | console.log('[activate] Claiming this ServiceWorker!'); 64 | event.waitUntil(self.clients.claim()); 65 | }); 66 | -------------------------------------------------------------------------------- /offline-status/style.css: -------------------------------------------------------------------------------- 1 | img { 2 | max-width: 200px; 3 | } 4 | 5 | .hidden { 6 | display: none; 7 | opacity: 0; 8 | } 9 | 10 | .notification { 11 | position: absolute; 12 | top: 20px; 13 | right: 20px; 14 | background: lightgreen; 15 | font-weight: bold; 16 | padding: 10px 20px; 17 | border-radius: 10px; 18 | opacity: 1; 19 | 20 | transition: opacity 2s; 21 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serviceworker-cookbook", 3 | "version": "0.1.0", 4 | "description": "A collection of Service Worker use cases, with commented sources and playgrounds.", 5 | "private": true, 6 | "scripts": { 7 | "start": "node --harmony server.js", 8 | "pretest": "npm run build", 9 | "test": "gulp test", 10 | "watch": "gulp watch", 11 | "build": "gulp build" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/mozilla/serviceworker-cookbook.git" 16 | }, 17 | "keywords": [ 18 | "serviceworker", 19 | "cookbook" 20 | ], 21 | "author": "Harald Kirschner (http://digitarald.de/)", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/mozilla/serviceworker-cookbook/issues" 25 | }, 26 | "homepage": "https://mozilla.github.com/serviceworker-cookbook/", 27 | "devDependencies": { 28 | "browser-sync": "^2.9.6", 29 | "del": "^4.1.0", 30 | "eslint": "^3.16.0", 31 | "eslint-config-airbnb": "^14.1.0", 32 | "eslint-plugin-import": "^2.2.0", 33 | "eslint-plugin-jsx-a11y": "^4.0.0", 34 | "eslint-plugin-react": "^6.10.0", 35 | "gulp": "^3.9.0", 36 | "gulp-concat": "^2.6.0", 37 | "gulp-css-base64": "^1.3.2", 38 | "gulp-docco": "0.0.4", 39 | "gulp-eslint": "^3.0.1", 40 | "gulp-load-plugins": "^1.1.0", 41 | "gulp-minify-css": "^1.2.2", 42 | "gulp-rename": "^1.2.2", 43 | 44 | "gulp-swig": "^0.9.1", 45 | "gulp-uglify": "^2.0.1", 46 | "marked": "^0.6.1", 47 | "merge-stream": "^1.0.0", 48 | "mozilla-tabzilla": "^0.5.1", 49 | "rename": "^1.0.3", 50 | "through2": "^3.0.1" 51 | }, 52 | "dependencies": { 53 | "body-parser": "^1.14.1", 54 | "es6-promisify": "^6.0.0", 55 | "express": "^4.13.3", 56 | "glob": "^7.0.0", 57 | "request": "^2.83.0", 58 | "swig": "^1.4.2", 59 | "web-push": "^3.2.2" 60 | }, 61 | "engines": { 62 | "node": "6" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /push-clients/README.md: -------------------------------------------------------------------------------- 1 | # Push Clients 2 | 3 | Control the clients of a service worker when the user clicks on a notification generated from a push event. Allows you to focus the tab of your app or even re-open it if it was closed. 4 | 5 | ## Use Cases 6 | Demonstrates 3 uses cases of delivering different notifications depending on the state of the app. It can recognize when you are on a page, need to switch to an open tab, or re-open a tab. 7 | 8 | ## Difficulty 9 | Advanced 10 | 11 | ## Category 12 | Web Push 13 | -------------------------------------------------------------------------------- /push-clients/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Push Notification with clients management - ServiceWorker Cookbook 6 | 7 | 14 | 15 | 16 |

This demo shows how to control the clients of a service worker when the user clicks on a push notification.

17 | 18 |

Press on 'Send notification' and try one of:

19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /push-clients/index.js: -------------------------------------------------------------------------------- 1 | // Register a Service Worker. 2 | navigator.serviceWorker.register('service-worker.js'); 3 | 4 | navigator.serviceWorker.ready 5 | .then(function(registration) { 6 | // Use the PushManager to get the user's subscription to the push service. 7 | return registration.pushManager.getSubscription() 8 | .then(async function(subscription) { 9 | // If a subscription was found, return it. 10 | if (subscription) { 11 | return subscription; 12 | } 13 | 14 | // Get the server's public key 15 | const response = await fetch('./vapidPublicKey'); 16 | const vapidPublicKey = await response.text(); 17 | // Chrome doesn't accept the base64-encoded (string) vapidPublicKey yet 18 | // urlBase64ToUint8Array() is defined in /tools.js 19 | const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey); 20 | 21 | // Otherwise, subscribe the user (userVisibleOnly allows to specify that we don't plan to 22 | // send notifications that don't have a visible effect for the user). 23 | return registration.pushManager.subscribe({ 24 | userVisibleOnly: true, 25 | applicationServerKey: convertedVapidKey 26 | }); 27 | }); 28 | }).then(function(subscription) { 29 | // Send the subscription details to the server using the Fetch API. 30 | fetch('./register', { 31 | method: 'post', 32 | headers: { 33 | 'Content-type': 'application/json' 34 | }, 35 | body: JSON.stringify({ 36 | subscription: subscription 37 | }), 38 | }); 39 | 40 | document.getElementById('doIt').onclick = function() { 41 | // Ask the server to send the client a notification (for testing purposes, in actual 42 | // applications the push notification is likely going to be generated by some event 43 | // in the server). 44 | fetch('./sendNotification', { 45 | method: 'post', 46 | headers: { 47 | 'Content-type': 'application/json' 48 | }, 49 | body: JSON.stringify({ 50 | subscription: subscription 51 | }), 52 | }); 53 | }; 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /push-clients/server.js: -------------------------------------------------------------------------------- 1 | // Use the web-push library to hide the implementation details of the communication 2 | // between the application server and the push service. 3 | // For details, see https://tools.ietf.org/html/draft-ietf-webpush-protocol and 4 | // https://tools.ietf.org/html/draft-ietf-webpush-encryption. 5 | const webPush = require("web-push"); 6 | 7 | if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) { 8 | console.log( 9 | "You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY " + 10 | "environment variables. You can use the following ones:" 11 | ); 12 | console.log(webPush.generateVAPIDKeys()); 13 | return; 14 | } 15 | // Set the keys used for encrypting the push messages. 16 | webPush.setVapidDetails( 17 | "https://example.com/", 18 | process.env.VAPID_PUBLIC_KEY, 19 | process.env.VAPID_PRIVATE_KEY 20 | ); 21 | 22 | module.exports = function (app, route) { 23 | app.get(route + "vapidPublicKey", function (req, res) { 24 | res.send(process.env.VAPID_PUBLIC_KEY); 25 | }); 26 | 27 | app.post(route + "register", function (req, res) { 28 | // A real world application would store the subscription info. 29 | res.sendStatus(201); 30 | }); 31 | 32 | app.post(route + "sendNotification", function (req, res) { 33 | setTimeout(function () { 34 | webPush 35 | .sendNotification(req.body.subscription) 36 | .then(function () { 37 | res.sendStatus(201); 38 | }) 39 | .catch(function (error) { 40 | res.sendStatus(500); 41 | console.log(error); 42 | }); 43 | }, 10000); 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /push-clients/service-worker.js: -------------------------------------------------------------------------------- 1 | // Immediately take control of the page, see the 'Immediate Claim' recipe 2 | // for a detailed explanation of the implementation of the following two 3 | // event listeners. 4 | 5 | self.addEventListener('install', function(event) { 6 | event.waitUntil(self.skipWaiting()); 7 | }); 8 | 9 | self.addEventListener('activate', function(event) { 10 | event.waitUntil(self.clients.claim()); 11 | }); 12 | 13 | // Register event listener for the 'push' event. 14 | self.addEventListener('push', function(event) { 15 | event.waitUntil( 16 | // Retrieve a list of the clients of this service worker. 17 | self.clients.matchAll().then(function(clientList) { 18 | // Check if there's at least one focused client. 19 | var focused = clientList.some(function(client) { 20 | return client.focused; 21 | }); 22 | 23 | var notificationMessage; 24 | if (focused) { 25 | notificationMessage = 'You\'re still here, thanks!'; 26 | } else if (clientList.length > 0) { 27 | notificationMessage = 'You haven\'t closed the page, ' + 28 | 'click here to focus it!'; 29 | } else { 30 | notificationMessage = 'You have closed the page, ' + 31 | 'click here to re-open it!'; 32 | } 33 | 34 | // Show a notification with title 'ServiceWorker Cookbook' and body depending 35 | // on the state of the clients of the service worker (three different bodies: 36 | // 1, the page is focused; 2, the page is still open but unfocused; 3, the page 37 | // is closed). 38 | return self.registration.showNotification('ServiceWorker Cookbook', { 39 | body: notificationMessage, 40 | }); 41 | }) 42 | ); 43 | }); 44 | 45 | // Register event listener for the 'notificationclick' event. 46 | self.addEventListener('notificationclick', function(event) { 47 | event.waitUntil( 48 | // Retrieve a list of the clients of this service worker. 49 | self.clients.matchAll().then(function(clientList) { 50 | // If there is at least one client, focus it. 51 | if (clientList.length > 0) { 52 | return clientList[0].focus(); 53 | } 54 | 55 | // Otherwise, open a new page. 56 | return self.clients.openWindow('../push-clients_demo.html'); 57 | }) 58 | ); 59 | }); 60 | -------------------------------------------------------------------------------- /push-get-payload/README.md: -------------------------------------------------------------------------------- 1 | # Push and Retrieve Payload 2 | 3 | Send push notifications and retrieve a payload once a notification is received. 4 | 5 | ## Difficulty 6 | Beginner 7 | 8 | ## Use Case 9 | This recipe demonstrates how you can deliver a notification and retrieve a payload when it arrives. 10 | 11 | ## Category 12 | Web Push 13 | -------------------------------------------------------------------------------- /push-get-payload/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Push Notification with payload retrieval - ServiceWorker Cookbook 6 | 7 | 14 | 15 | 16 |

This demo shows how to send push notifications and retrieve a payload when the notification is received.

17 | 18 |
19 | Notification payload: 20 | Notification delay: seconds 21 | Notification Time-To-Live: seconds 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /push-get-payload/index.js: -------------------------------------------------------------------------------- 1 | // Register a Service Worker. 2 | navigator.serviceWorker.register('service-worker.js'); 3 | 4 | navigator.serviceWorker.ready 5 | .then(function(registration) { 6 | // Use the PushManager to get the user's subscription to the push service. 7 | return registration.pushManager.getSubscription() 8 | .then(async function(subscription) { 9 | // If a subscription was found, return it. 10 | if (subscription) { 11 | return subscription; 12 | } 13 | 14 | // Get the server's public key 15 | const response = await fetch('./vapidPublicKey'); 16 | const vapidPublicKey = await response.text(); 17 | // Chrome doesn't accept the base64-encoded (string) vapidPublicKey yet 18 | // urlBase64ToUint8Array() is defined in /tools.js 19 | const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey); 20 | 21 | // Otherwise, subscribe the user (userVisibleOnly allows to specify that we don't plan to 22 | // send notifications that don't have a visible effect for the user). 23 | return registration.pushManager.subscribe({ 24 | userVisibleOnly: true, 25 | applicationServerKey: convertedVapidKey 26 | }); 27 | }); 28 | }).then(function(subscription) { 29 | // Send the subscription details to the server using the Fetch API. 30 | fetch('./register', { 31 | method: 'post', 32 | headers: { 33 | 'Content-type': 'application/json' 34 | }, 35 | body: JSON.stringify({ 36 | subscription: subscription 37 | }), 38 | }); 39 | 40 | document.getElementById('doIt').onclick = function() { 41 | const payload = document.getElementById('notification-payload').value; 42 | const delay = document.getElementById('notification-delay').value; 43 | const ttl = document.getElementById('notification-ttl').value; 44 | 45 | // Ask the server to send the client a notification (for testing purposes, in actual 46 | // applications the push notification is likely going to be generated by some event 47 | // in the server). 48 | fetch('./sendNotification', { 49 | method: 'post', 50 | headers: { 51 | 'Content-type': 'application/json' 52 | }, 53 | body: JSON.stringify({ 54 | subscription: subscription, 55 | payload: payload, 56 | delay: delay, 57 | ttl: ttl, 58 | }), 59 | }); 60 | }; 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /push-get-payload/server.js: -------------------------------------------------------------------------------- 1 | // Use the web-push library to hide the implementation details of the communication 2 | // between the application server and the push service. 3 | // For details, see https://tools.ietf.org/html/draft-ietf-webpush-protocol and 4 | // https://tools.ietf.org/html/draft-ietf-webpush-encryption. 5 | const webPush = require("web-push"); 6 | 7 | if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) { 8 | console.log( 9 | "You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY " + 10 | "environment variables. You can use the following ones:" 11 | ); 12 | console.log(webPush.generateVAPIDKeys()); 13 | return; 14 | } 15 | // Set the keys used for encrypting the push messages. 16 | webPush.setVapidDetails( 17 | "https://example.com/", 18 | process.env.VAPID_PUBLIC_KEY, 19 | process.env.VAPID_PRIVATE_KEY 20 | ); 21 | 22 | const payloads = {}; 23 | 24 | module.exports = function (app, route) { 25 | app.get(route + "vapidPublicKey", function (req, res) { 26 | res.send(process.env.VAPID_PUBLIC_KEY); 27 | }); 28 | 29 | app.post(route + "register", function (req, res) { 30 | // A real world application would store the subscription info. 31 | res.sendStatus(201); 32 | }); 33 | 34 | app.post(route + "sendNotification", function (req, res) { 35 | const subscription = req.body.subscription; 36 | const payload = req.body.payload; 37 | const options = { 38 | TTL: req.body.ttl, 39 | }; 40 | 41 | setTimeout(function () { 42 | payloads[req.body.subscription.endpoint] = payload; 43 | webPush 44 | .sendNotification(subscription, null, options) 45 | .then(function () { 46 | res.sendStatus(201); 47 | }) 48 | .catch(function (error) { 49 | res.sendStatus(500); 50 | console.log(error); 51 | }); 52 | }, req.body.delay * 1000); 53 | }); 54 | 55 | app.get(route + "getPayload", function (req, res) { 56 | res.send(payloads[req.query.endpoint]); 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /push-get-payload/service-worker.js: -------------------------------------------------------------------------------- 1 | function getEndpoint() { 2 | return self.registration.pushManager.getSubscription() 3 | .then(function(subscription) { 4 | if (subscription) { 5 | return subscription.endpoint; 6 | } 7 | 8 | throw new Error('User not subscribed'); 9 | }); 10 | } 11 | 12 | // Register event listener for the 'push' event. 13 | self.addEventListener('push', function(event) { 14 | // Keep the service worker alive until the notification is created. 15 | event.waitUntil( 16 | getEndpoint() 17 | .then(function(endpoint) { 18 | // Retrieve the textual payload from the server using a GET request. 19 | // We are using the endpoint as an unique ID of the user for simplicity. 20 | return fetch('./getPayload?endpoint=' + endpoint); 21 | }) 22 | .then(function(response) { 23 | return response.text(); 24 | }) 25 | .then(function(payload) { 26 | // Show a notification with title 'ServiceWorker Cookbook' and use the payload 27 | // as the body. 28 | self.registration.showNotification('ServiceWorker Cookbook', { 29 | body: payload, 30 | }); 31 | }) 32 | ); 33 | }); 34 | -------------------------------------------------------------------------------- /push-payload/README.md: -------------------------------------------------------------------------------- 1 | # Push Payload 2 | 3 | Send push notifications with a payload. This recipe shows how to send and receive a string, but data can be extracted from a Push message in a variety of formats (string, ArrayBuffer, Blob, JSON). 4 | 5 | ## Difficulty 6 | Beginner 7 | 8 | ## Use Case 9 | A message does not have to deliver just text, but can deliver various kinds of payloads of data to an application. This demonstrates how you can deliver a rich payload to your app. 10 | 11 | ## Category 12 | Web Push 13 | -------------------------------------------------------------------------------- /push-payload/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Push Notification with payload - ServiceWorker Cookbook 6 | 7 | 14 | 15 | 16 |

This demo shows how to send push notifications with a payload.

17 | 18 |
19 | Notification payload: 20 | Notification delay: seconds 21 | Notification Time-To-Live: seconds 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /push-payload/index.js: -------------------------------------------------------------------------------- 1 | // Register a Service Worker. 2 | navigator.serviceWorker.register('service-worker.js'); 3 | 4 | navigator.serviceWorker.ready 5 | .then(function(registration) { 6 | // Use the PushManager to get the user's subscription to the push service. 7 | return registration.pushManager.getSubscription() 8 | .then(async function(subscription) { 9 | // If a subscription was found, return it. 10 | if (subscription) { 11 | return subscription; 12 | } 13 | 14 | // Get the server's public key 15 | const response = await fetch('./vapidPublicKey'); 16 | const vapidPublicKey = await response.text(); 17 | // Chrome doesn't accept the base64-encoded (string) vapidPublicKey yet 18 | // urlBase64ToUint8Array() is defined in /tools.js 19 | const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey); 20 | 21 | // Otherwise, subscribe the user (userVisibleOnly allows to specify that we don't plan to 22 | // send notifications that don't have a visible effect for the user). 23 | return registration.pushManager.subscribe({ 24 | userVisibleOnly: true, 25 | applicationServerKey: convertedVapidKey 26 | }); 27 | }); 28 | }).then(function(subscription) { 29 | // Send the subscription details to the server using the Fetch API. 30 | fetch('./register', { 31 | method: 'post', 32 | headers: { 33 | 'Content-type': 'application/json' 34 | }, 35 | body: JSON.stringify({ 36 | subscription: subscription 37 | }), 38 | }); 39 | 40 | document.getElementById('doIt').onclick = function() { 41 | const payload = document.getElementById('notification-payload').value; 42 | const delay = document.getElementById('notification-delay').value; 43 | const ttl = document.getElementById('notification-ttl').value; 44 | 45 | // Ask the server to send the client a notification (for testing purposes, in actual 46 | // applications the push notification is likely going to be generated by some event 47 | // in the server). 48 | fetch('./sendNotification', { 49 | method: 'post', 50 | headers: { 51 | 'Content-type': 'application/json' 52 | }, 53 | body: JSON.stringify({ 54 | subscription: subscription, 55 | payload: payload, 56 | delay: delay, 57 | ttl: ttl, 58 | }), 59 | }); 60 | }; 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /push-payload/server.js: -------------------------------------------------------------------------------- 1 | // Use the web-push library to hide the implementation details of the communication 2 | // between the application server and the push service. 3 | // For details, see https://tools.ietf.org/html/draft-ietf-webpush-protocol and 4 | // https://tools.ietf.org/html/draft-ietf-webpush-encryption. 5 | const webPush = require("web-push"); 6 | 7 | if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) { 8 | console.log( 9 | "You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY " + 10 | "environment variables. You can use the following ones:" 11 | ); 12 | console.log(webPush.generateVAPIDKeys()); 13 | return; 14 | } 15 | // Set the keys used for encrypting the push messages. 16 | webPush.setVapidDetails( 17 | "https://example.com/", 18 | process.env.VAPID_PUBLIC_KEY, 19 | process.env.VAPID_PRIVATE_KEY 20 | ); 21 | 22 | module.exports = function (app, route) { 23 | app.get(route + "vapidPublicKey", function (req, res) { 24 | res.send(process.env.VAPID_PUBLIC_KEY); 25 | }); 26 | 27 | app.post(route + "register", function (req, res) { 28 | // A real world application would store the subscription info. 29 | res.sendStatus(201); 30 | }); 31 | 32 | app.post(route + "sendNotification", function (req, res) { 33 | const subscription = req.body.subscription; 34 | const payload = req.body.payload; 35 | const options = { 36 | TTL: req.body.ttl, 37 | }; 38 | 39 | setTimeout(function () { 40 | webPush 41 | .sendNotification(subscription, payload, options) 42 | .then(function () { 43 | res.sendStatus(201); 44 | }) 45 | .catch(function (error) { 46 | console.log(error); 47 | res.sendStatus(500); 48 | }); 49 | }, req.body.delay * 1000); 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /push-payload/service-worker.js: -------------------------------------------------------------------------------- 1 | // Register event listener for the 'push' event. 2 | self.addEventListener('push', function(event) { 3 | // Retrieve the textual payload from event.data (a PushMessageData object). 4 | // Other formats are supported (ArrayBuffer, Blob, JSON), check out the documentation 5 | // on https://developer.mozilla.org/en-US/docs/Web/API/PushMessageData. 6 | const payload = event.data ? event.data.text() : 'no payload'; 7 | 8 | // Keep the service worker alive until the notification is created. 9 | event.waitUntil( 10 | // Show a notification with title 'ServiceWorker Cookbook' and use the payload 11 | // as the body. 12 | self.registration.showNotification('ServiceWorker Cookbook', { 13 | body: payload, 14 | }) 15 | ); 16 | }); 17 | -------------------------------------------------------------------------------- /push-quota/README.md: -------------------------------------------------------------------------------- 1 | # Push Quota 2 | 3 | Experiment with the quota management policies of different browsers. Try sending many notifications (visible or invisible) and see what happens if you keep the tab open vs close it, or if you click on some notifications vs you click on none of them. 4 | 5 | ## Difficulty 6 | Advanced 7 | 8 | ## Use Cases 9 | This code allows you to experiment with different push message quota policies with different browsers so you can see how they behave for different actions by the users. 10 | 11 | ## Category 12 | Web Push 13 | -------------------------------------------------------------------------------- /push-quota/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Push Notification with quota management - ServiceWorker Cookbook 6 | 7 | 18 | 19 | 20 |

This demo allows you to experiment with the quota management rules enforced by browsers. 21 | 22 | The browser will likely reduce the quota assigned to you if you send too many notifications not visible to the user, or if the user dismisses your visible notifications. 23 | Try closing this page after selecting one of the two options ("visible" or "invisible") with a high number of notifications.

24 | 25 | 29 | 30 |
31 | Number of notifications to send: 32 |
33 | 34 | 35 | 36 |

Received 0 visible notifications 37 | Received 0 invisible notifications

38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /push-quota/server.js: -------------------------------------------------------------------------------- 1 | // Use the web-push library to hide the implementation details of the communication 2 | // between the application server and the push service. 3 | // For details, see https://tools.ietf.org/html/draft-ietf-webpush-protocol and 4 | // https://tools.ietf.org/html/draft-ietf-webpush-encryption. 5 | const webPush = require("web-push"); 6 | 7 | if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) { 8 | console.log( 9 | "You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY " + 10 | "environment variables. You can use the following ones:" 11 | ); 12 | console.log(webPush.generateVAPIDKeys()); 13 | return; 14 | } 15 | // Set the keys used for encrypting the push messages. 16 | webPush.setVapidDetails( 17 | "https://example.com/", 18 | process.env.VAPID_PUBLIC_KEY, 19 | process.env.VAPID_PRIVATE_KEY 20 | ); 21 | 22 | module.exports = function (app, route) { 23 | app.get(route + "vapidPublicKey", function (req, res) { 24 | res.send(process.env.VAPID_PUBLIC_KEY); 25 | }); 26 | 27 | app.post(route + "register", function (req, res) { 28 | // A real world application would store the subscription info. 29 | res.sendStatus(201); 30 | }); 31 | 32 | // Send N notifications, specifying whether the service worker will need to show 33 | // a visible notification or not using the push payload: 34 | // - 'true': show a notification; 35 | // - 'false': don't show a notification. 36 | app.post(route + "sendNotification", function (req, res) { 37 | const subscription = req.body.subscription; 38 | const payload = JSON.stringify(req.body.visible); 39 | const options = { 40 | TTL: 200, 41 | }; 42 | 43 | let num = 1; 44 | 45 | let promises = []; 46 | 47 | let intervalID = setInterval(function () { 48 | promises.push(webPush.sendNotification(subscription, payload, options)); 49 | 50 | if (num++ === Number(req.body.num)) { 51 | clearInterval(intervalID); 52 | 53 | Promise.all(promises) 54 | .then(function () { 55 | res.sendStatus(201); 56 | }) 57 | .catch(function (error) { 58 | res.sendStatus(500); 59 | console.log(error); 60 | }); 61 | } 62 | }, 1000); 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /push-quota/service-worker.js: -------------------------------------------------------------------------------- 1 | var CACHE_NAME = 'notifications'; 2 | 3 | // On install, create the 'notifications' cache that will be used to store the Number 4 | // of notifications received. 5 | self.addEventListener('install', function(event) { 6 | event.waitUntil( 7 | caches.open(CACHE_NAME).then(function(cache) { 8 | return Promise.all([ 9 | // We create a fake request and response to store the info. 10 | cache.put(new Request('invisible'), new Response('0', { 11 | headers: { 12 | 'content-type': 'application/json' 13 | } 14 | })), 15 | cache.put(new Request('visible'), new Response('0', { 16 | headers: { 17 | 'content-type': 'application/json' 18 | } 19 | })), 20 | ]); 21 | }) 22 | ); 23 | }); 24 | 25 | self.addEventListener('activate', function(event) { 26 | event.waitUntil(self.clients.claim); 27 | }); 28 | 29 | function updateNumber(type) { 30 | // Update the number of notifications received of type 'type' (visible or invisible). 31 | return caches.open(CACHE_NAME).then(function(cache) { 32 | return cache.match(type).then(function(response) { 33 | return response.json().then(function(notificationNum) { 34 | var newNotificationNum = notificationNum + 1; 35 | 36 | return cache.put( 37 | new Request(type), 38 | new Response(JSON.stringify(newNotificationNum), { 39 | headers: { 40 | 'content-type': 'application/json', 41 | }, 42 | }) 43 | ).then(function() { 44 | return newNotificationNum; 45 | }); 46 | }); 47 | }); 48 | }); 49 | } 50 | 51 | // Register event listener for the 'push' event. 52 | self.addEventListener('push', function(event) { 53 | // Retrieve the payload from event.data (a PushMessageData object) as a JSON object. 54 | var visible = event.data ? event.data.json() : false; 55 | 56 | // Keep the service worker alive until the 'notifications' cache is updated and, 57 | // if visible is true, the notification is created. 58 | 59 | if (visible) { 60 | event.waitUntil(updateNumber('visible').then(function(num) { 61 | return self.registration.showNotification('ServiceWorker Cookbook', { 62 | body: 'Received ' + num + ' visible notifications', 63 | }); 64 | })); 65 | } else { 66 | event.waitUntil(updateNumber('invisible')); 67 | } 68 | }); 69 | -------------------------------------------------------------------------------- /push-replace/README.md: -------------------------------------------------------------------------------- 1 | # Push Tag 2 | 3 | Use the notification tag to replace old notifications with new ones. Allows you to show only up-to-date information to your users or collapse multiple notifications into a single one. 4 | 5 | ## Difficulty 6 | Intermediate 7 | 8 | ## Use Cases 9 | This code shows how to manage a queue of notifications so that previous notifications can be discarded or merged into a single notification. 10 | 11 | ## Category 12 | Web Push 13 | -------------------------------------------------------------------------------- /push-replace/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Push notification replacing - ServiceWorker Cookbook 6 | 7 | 14 | 15 | 16 |

This demo shows how to replace an old notification with a new one.

17 | 18 |
19 | Delay between notifications: seconds 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /push-replace/index.js: -------------------------------------------------------------------------------- 1 | // Register a Service Worker. 2 | navigator.serviceWorker.register('service-worker.js'); 3 | 4 | navigator.serviceWorker.ready 5 | .then(function(registration) { 6 | // Use the PushManager to get the user's subscription to the push service. 7 | return registration.pushManager.getSubscription() 8 | .then(async function(subscription) { 9 | // If a subscription was found, return it. 10 | if (subscription) { 11 | return subscription; 12 | } 13 | 14 | // Get the server's public key 15 | const response = await fetch('./vapidPublicKey'); 16 | const vapidPublicKey = await response.text(); 17 | // Chrome doesn't accept the base64-encoded (string) vapidPublicKey yet 18 | // urlBase64ToUint8Array() is defined in /tools.js 19 | const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey); 20 | 21 | // Otherwise, subscribe the user (userVisibleOnly allows to specify that we don't plan to 22 | // send notifications that don't have a visible effect for the user). 23 | return registration.pushManager.subscribe({ 24 | userVisibleOnly: true, 25 | applicationServerKey: convertedVapidKey 26 | }); 27 | }); 28 | }).then(function(subscription) { 29 | // Send the subscription details to the server using the Fetch API. 30 | fetch('./register', { 31 | method: 'post', 32 | headers: { 33 | 'Content-type': 'application/json' 34 | }, 35 | body: JSON.stringify({ 36 | subscription: subscription 37 | }), 38 | }); 39 | 40 | document.getElementById('doIt').onclick = function() { 41 | const delay = document.getElementById('notification-delay').value; 42 | 43 | // Ask the server to send the client a notification (for testing purposes, in actual 44 | // applications the push notification is likely going to be generated by some event 45 | // in the server). 46 | fetch('./sendNotification', { 47 | method: 'post', 48 | headers: { 49 | 'Content-type': 'application/json' 50 | }, 51 | body: JSON.stringify({ 52 | subscription: subscription, 53 | delay: delay, 54 | }), 55 | }); 56 | }; 57 | 58 | }); 59 | -------------------------------------------------------------------------------- /push-replace/server.js: -------------------------------------------------------------------------------- 1 | // Use the web-push library to hide the implementation details of the communication 2 | // between the application server and the push service. 3 | // For details, see https://tools.ietf.org/html/draft-ietf-webpush-protocol and 4 | // https://tools.ietf.org/html/draft-ietf-webpush-encryption. 5 | const webPush = require("web-push"); 6 | 7 | if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) { 8 | console.log( 9 | "You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY " + 10 | "environment variables. You can use the following ones:" 11 | ); 12 | console.log(webPush.generateVAPIDKeys()); 13 | return; 14 | } 15 | // Set the keys used for encrypting the push messages. 16 | webPush.setVapidDetails( 17 | "https://example.com/", 18 | process.env.VAPID_PUBLIC_KEY, 19 | process.env.VAPID_PRIVATE_KEY 20 | ); 21 | 22 | module.exports = function (app, route) { 23 | app.get(route + "vapidPublicKey", function (req, res) { 24 | res.send(process.env.VAPID_PUBLIC_KEY); 25 | }); 26 | 27 | app.post(route + "register", function (req, res) { 28 | // A real world application would store the subscription info. 29 | res.sendStatus(201); 30 | }); 31 | 32 | app.post(route + "sendNotification", function (req, res) { 33 | const subscription = req.body.subscription; 34 | const payload = null; 35 | const options = { 36 | TTL: 200, 37 | }; 38 | 39 | webPush.sendNotification(subscription, payload, options).catch(logError); 40 | 41 | setTimeout(function () { 42 | webPush 43 | .sendNotification(subscription, payload, options) 44 | .then(function () { 45 | res.sendStatus(201); 46 | }) 47 | .catch(logError); 48 | }, req.body.delay * 1000); 49 | 50 | function logError(error) { 51 | res.sendStatus(500); 52 | console.log(error); 53 | } 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /push-replace/service-worker.js: -------------------------------------------------------------------------------- 1 | let num = 1; 2 | 3 | // Register event listener for the 'push' event. 4 | self.addEventListener('push', function(event) { 5 | // Keep the service worker alive until the notification is created. 6 | event.waitUntil( 7 | // Show a notification with title 'ServiceWorker Cookbook' and body containing 8 | // a number that keeps increasing for each received notification. 9 | // The tag field allows replacing an old notification with a new one (a notification 10 | // with the same tag of another one will replace it). 11 | self.registration.showNotification('ServiceWorker Cookbook', { 12 | body: 'Notification ' + num++, 13 | tag: 'swc', 14 | }) 15 | ); 16 | }); 17 | -------------------------------------------------------------------------------- /push-rich/README.md: -------------------------------------------------------------------------------- 1 | # Push Rich 2 | 3 | Show rich push notifications, defining the language of the notification, a vibration pattern, an image to associate to the notification. See https://notifications.spec.whatwg.org/#api for the other parameters you can set (e.g. a set of actions that can be activated from the notification). 4 | 5 | ## Difficulty 6 | Beginner 7 | 8 | ## Category 9 | Web Push 10 | -------------------------------------------------------------------------------- /push-rich/caesar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdn/serviceworker-cookbook/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/push-rich/caesar.jpg -------------------------------------------------------------------------------- /push-rich/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rich Push Notification - ServiceWorker Cookbook 6 | 7 | 14 | 15 | 16 |

This demo shows how to show rich push notifications.

17 | 18 |
19 | Notification delay: seconds 20 | Notification Time-To-Live: seconds 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /push-rich/index.js: -------------------------------------------------------------------------------- 1 | // Register a Service Worker. 2 | navigator.serviceWorker.register('service-worker.js'); 3 | 4 | navigator.serviceWorker.ready 5 | .then(function(registration) { 6 | // Use the PushManager to get the user's subscription to the push service. 7 | return registration.pushManager.getSubscription() 8 | .then(async function(subscription) { 9 | // If a subscription was found, return it. 10 | if (subscription) { 11 | return subscription; 12 | } 13 | 14 | // Get the server's public key 15 | const response = await fetch('./vapidPublicKey'); 16 | const vapidPublicKey = await response.text(); 17 | // Chrome doesn't accept the base64-encoded (string) vapidPublicKey yet 18 | // urlBase64ToUint8Array() is defined in /tools.js 19 | const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey); 20 | 21 | // Otherwise, subscribe the user (userVisibleOnly allows to specify that we don't plan to 22 | // send notifications that don't have a visible effect for the user). 23 | return registration.pushManager.subscribe({ 24 | userVisibleOnly: true, 25 | applicationServerKey: convertedVapidKey 26 | }); 27 | }); 28 | }).then(function(subscription) { 29 | // Send the subscription details to the server using the Fetch API. 30 | fetch('./register', { 31 | method: 'post', 32 | headers: { 33 | 'Content-type': 'application/json' 34 | }, 35 | body: JSON.stringify({ 36 | subscription: subscription 37 | }), 38 | }); 39 | 40 | document.getElementById('doIt').onclick = function() { 41 | const delay = document.getElementById('notification-delay').value; 42 | const ttl = document.getElementById('notification-ttl').value; 43 | 44 | // Ask the server to send the client a notification (for testing purposes, in actual 45 | // applications the push notification is likely going to be generated by some event 46 | // in the server). 47 | fetch('./sendNotification', { 48 | method: 'post', 49 | headers: { 50 | 'Content-type': 'application/json' 51 | }, 52 | body: JSON.stringify({ 53 | subscription: subscription, 54 | delay: delay, 55 | ttl: ttl, 56 | }), 57 | }); 58 | }; 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /push-rich/server.js: -------------------------------------------------------------------------------- 1 | // Use the web-push library to hide the implementation details of the communication 2 | // between the application server and the push service. 3 | // For details, see https://tools.ietf.org/html/draft-ietf-webpush-protocol and 4 | // https://tools.ietf.org/html/draft-ietf-webpush-encryption. 5 | const webPush = require("web-push"); 6 | 7 | if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) { 8 | console.log( 9 | "You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY " + 10 | "environment variables. You can use the following ones:" 11 | ); 12 | console.log(webPush.generateVAPIDKeys()); 13 | return; 14 | } 15 | // Set the keys used for encrypting the push messages. 16 | webPush.setVapidDetails( 17 | "https://example.com/", 18 | process.env.VAPID_PUBLIC_KEY, 19 | process.env.VAPID_PRIVATE_KEY 20 | ); 21 | 22 | module.exports = function (app, route) { 23 | app.get(route + "vapidPublicKey", function (req, res) { 24 | res.send(process.env.VAPID_PUBLIC_KEY); 25 | }); 26 | 27 | app.post(route + "register", function (req, res) { 28 | // A real world application would store the subscription info. 29 | res.sendStatus(201); 30 | }); 31 | 32 | app.post(route + "sendNotification", function (req, res) { 33 | const subscription = req.body.subscription; 34 | const payload = null; 35 | const options = { 36 | TTL: req.body.ttl, 37 | }; 38 | 39 | setTimeout(function () { 40 | webPush 41 | .sendNotification(subscription, payload, options) 42 | .then(function () { 43 | res.sendStatus(201); 44 | }) 45 | .catch(function (error) { 46 | console.log(error); 47 | res.sendStatus(500); 48 | }); 49 | }, req.body.delay * 1000); 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /push-rich/service-worker.js: -------------------------------------------------------------------------------- 1 | // Register event listener for the 'push' event. 2 | self.addEventListener('push', function(event) { 3 | // Keep the service worker alive until the notification is created. 4 | event.waitUntil( 5 | // Show a notification with title 'ServiceWorker Cookbook' and body 'Alea iacta est'. 6 | // Set other parameters such as the notification language, a vibration pattern associated 7 | // to the notification, an image to show near the body. 8 | // There are many other possible options, for an exhaustive list see the specs: 9 | // https://notifications.spec.whatwg.org/ 10 | self.registration.showNotification('ServiceWorker Cookbook', { 11 | lang: 'la', 12 | body: 'Alea iacta est', 13 | icon: 'caesar.jpg', 14 | vibrate: [500, 100, 500], 15 | }) 16 | ); 17 | }); 18 | -------------------------------------------------------------------------------- /push-simple/README.md: -------------------------------------------------------------------------------- 1 | # Push Simple 2 | 3 | Simplest example of Web Push API usage. Send notifications to users even when your page is not open. 4 | 5 | ## Difficulty 6 | Beginner 7 | 8 | ## Category 9 | Web Push 10 | -------------------------------------------------------------------------------- /push-simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Push Notification - ServiceWorker Cookbook 6 | 7 | 14 | 15 | 16 |

This demo shows how to register for push notifications and how to send them.

17 | 18 |
19 | Notification delay: seconds 20 | Notification Time-To-Live: seconds 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /push-simple/index.js: -------------------------------------------------------------------------------- 1 | // Register a Service Worker. 2 | navigator.serviceWorker.register('service-worker.js'); 3 | 4 | navigator.serviceWorker.ready 5 | .then(function(registration) { 6 | // Use the PushManager to get the user's subscription to the push service. 7 | return registration.pushManager.getSubscription() 8 | .then(async function(subscription) { 9 | // If a subscription was found, return it. 10 | if (subscription) { 11 | return subscription; 12 | } 13 | 14 | // Get the server's public key 15 | const response = await fetch('./vapidPublicKey'); 16 | const vapidPublicKey = await response.text(); 17 | // Chrome doesn't accept the base64-encoded (string) vapidPublicKey yet 18 | // urlBase64ToUint8Array() is defined in /tools.js 19 | const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey); 20 | 21 | // Otherwise, subscribe the user (userVisibleOnly allows to specify that we don't plan to 22 | // send notifications that don't have a visible effect for the user). 23 | return registration.pushManager.subscribe({ 24 | userVisibleOnly: true, 25 | applicationServerKey: convertedVapidKey 26 | }); 27 | }); 28 | }).then(function(subscription) { 29 | // Send the subscription details to the server using the Fetch API. 30 | fetch('./register', { 31 | method: 'post', 32 | headers: { 33 | 'Content-type': 'application/json' 34 | }, 35 | body: JSON.stringify({ 36 | subscription: subscription 37 | }), 38 | }); 39 | 40 | document.getElementById('doIt').onclick = function() { 41 | const delay = document.getElementById('notification-delay').value; 42 | const ttl = document.getElementById('notification-ttl').value; 43 | 44 | // Ask the server to send the client a notification (for testing purposes, in actual 45 | // applications the push notification is likely going to be generated by some event 46 | // in the server). 47 | fetch('./sendNotification', { 48 | method: 'post', 49 | headers: { 50 | 'Content-type': 'application/json' 51 | }, 52 | body: JSON.stringify({ 53 | subscription: subscription, 54 | delay: delay, 55 | ttl: ttl, 56 | }), 57 | }); 58 | }; 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /push-simple/server.js: -------------------------------------------------------------------------------- 1 | // Use the web-push library to hide the implementation details of the communication 2 | // between the application server and the push service. 3 | // For details, see https://tools.ietf.org/html/draft-ietf-webpush-protocol and 4 | // https://tools.ietf.org/html/draft-ietf-webpush-encryption. 5 | const webPush = require("web-push"); 6 | 7 | if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) { 8 | console.log( 9 | "You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY " + 10 | "environment variables. You can use the following ones:" 11 | ); 12 | console.log(webPush.generateVAPIDKeys()); 13 | return; 14 | } 15 | // Set the keys used for encrypting the push messages. 16 | webPush.setVapidDetails( 17 | "https://example.com/", 18 | process.env.VAPID_PUBLIC_KEY, 19 | process.env.VAPID_PRIVATE_KEY 20 | ); 21 | 22 | module.exports = function (app, route) { 23 | app.get(route + "vapidPublicKey", function (req, res) { 24 | res.send(process.env.VAPID_PUBLIC_KEY); 25 | }); 26 | 27 | app.post(route + "register", function (req, res) { 28 | // A real world application would store the subscription info. 29 | res.sendStatus(201); 30 | }); 31 | 32 | app.post(route + "sendNotification", function (req, res) { 33 | const subscription = req.body.subscription; 34 | const payload = null; 35 | const options = { 36 | TTL: req.body.ttl, 37 | }; 38 | 39 | setTimeout(function () { 40 | webPush 41 | .sendNotification(subscription, payload, options) 42 | .then(function () { 43 | res.sendStatus(201); 44 | }) 45 | .catch(function (error) { 46 | res.sendStatus(500); 47 | console.log(error); 48 | }); 49 | }, req.body.delay * 1000); 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /push-simple/service-worker.js: -------------------------------------------------------------------------------- 1 | // Register event listener for the 'push' event. 2 | self.addEventListener('push', function(event) { 3 | // Keep the service worker alive until the notification is created. 4 | event.waitUntil( 5 | // Show a notification with title 'ServiceWorker Cookbook' and body 'Alea iacta est'. 6 | self.registration.showNotification('ServiceWorker Cookbook', { 7 | body: 'Alea iacta est', 8 | }) 9 | ); 10 | }); 11 | -------------------------------------------------------------------------------- /push-subscription-management/README.md: -------------------------------------------------------------------------------- 1 | # Push Subscription 2 | 3 | This recipe shows how to use push notifications with subscription management. 4 | 5 | ## Difficulty 6 | Advanced 7 | 8 | ## Use Case 9 | Allowing users to subscribe to features of your app allows you to keep in touch with and convert visitors! 10 | 11 | 12 | Init State 13 | ---------- 14 | After service worker is registered, client is checking if it is already subscribed to the notificiation service. Button's contents is set depending on this. 15 | 16 | Subscribe 17 | --------- 18 | After successful subscription (index.js::pushManager.subscribe) client sends a post request to application server to register the subscription 19 | 20 | Notifications 21 | ------------- 22 | Server periodically sends a notification using web-push library to all registered endpoints. 23 | If an endpoint is not registered anymore (expired or cancelled) it is removed from subscription list. 24 | 25 | Unsubscribe 26 | ----------- 27 | After successful unsubscription (index.js::pushSubscription.unsubscribe) client sends a post request to application server to unregister the subscription. Server is no longer sending notification. 28 | 29 | Subscription Expired 30 | -------------------- 31 | Service worker is watching for the *pushsubscriptionchange* event and resubscribes to the push service. 32 | 33 | Not in Recipe 34 | ------------- 35 | Subscription might be cancelled by the user outside of this page (from browser settings or notification UI). In this recipe server will stop to send the notifications, but the front-end doesn't know about it. One could periodically check if registration is still active. 36 | 37 | ## Category 38 | Web Push 39 | -------------------------------------------------------------------------------- /push-subscription-management/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Push notifications with subscription management - ServiceWorker Cookbook 6 | 7 | 14 | 15 | 16 |

This demo shows how to subscribe/unsubscribe to the push notifications.

17 | 18 |

To simulate the subscription expiration under Firefox please set the dom.push.userAgentId in about:config to an empty string. (Note: bug 1222428)

19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /push-subscription-management/service-worker.js: -------------------------------------------------------------------------------- 1 | // Listen to `push` notification event. Define the text to be displayed 2 | // and show the notification. 3 | self.addEventListener('push', function(event) { 4 | event.waitUntil(self.registration.showNotification('ServiceWorker Cookbook', { 5 | body: 'Push Notification Subscription Management' 6 | })); 7 | }); 8 | 9 | // Listen to `pushsubscriptionchange` event which is fired when 10 | // subscription expires. Subscribe again and register the new subscription 11 | // in the server by sending a POST request with endpoint. Real world 12 | // application would probably use also user identification. 13 | self.addEventListener('pushsubscriptionchange', function(event) { 14 | console.log('Subscription expired'); 15 | event.waitUntil( 16 | self.registration.pushManager.subscribe({ userVisibleOnly: true }) 17 | .then(function(subscription) { 18 | console.log('Subscribed after expiration', subscription.endpoint); 19 | return fetch('register', { 20 | method: 'post', 21 | headers: { 22 | 'Content-type': 'application/json' 23 | }, 24 | body: JSON.stringify({ 25 | endpoint: subscription.endpoint 26 | }) 27 | }); 28 | }) 29 | ); 30 | }); 31 | -------------------------------------------------------------------------------- /render-store/README.md: -------------------------------------------------------------------------------- 1 | # Render Store 2 | The recipe demonstrates one recommendation from the [NGA](https://wiki.mozilla.org/Gaia/Architecture_Proposal#Render_store). A cache containing the interpolated templates in order to avoid model fetching and render times upon successive requests. 3 | 4 | ## Difficulty 5 | Intermediate 6 | 7 | ## Use Case 8 | As a web app developer, I want to minimize the load time for revisited resources. 9 | 10 | ## Solution 11 | Use an offline cache to store the template once it's completely rendered and use this copy upon next requests. 12 | 13 | ## Category 14 | Performance 15 | -------------------------------------------------------------------------------- /render-store/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Render Store - ServiceWorker Cookbook 6 | 7 | 8 | 9 | 10 |

Pokemon Character Sheets

11 |

Choose a character once, see the times and reload. Check the difference in times.

12 |

13 | 14 | 15 | -------------------------------------------------------------------------------- /render-store/index.js: -------------------------------------------------------------------------------- 1 | // Modern browsers prevent mixed content. I.e., if the page is served from 2 | // a safe (https) origin, they will block the content from other (non https) 3 | // origins. We use this service to tunnel Pokemon API responses through a 4 | // secure origin. 5 | var PROXY = 'https://crossorigin.me/'; 6 | 7 | // We need the pokedex entry point to retrieve the complete list 8 | // of Pokemon. 9 | var POKEDEX = PROXY + 'http://pokeapi.co/api/v1/pokedex/1/'; 10 | 11 | // Once the Service Worker is activated, load the Pokemon list. 12 | if ('serviceWorker' in navigator) { 13 | if (navigator.serviceWorker.controller) { 14 | loadPokemonList(); 15 | } else { 16 | navigator.serviceWorker.register('service-worker.js'); 17 | navigator.serviceWorker.ready.then(function() { 18 | loadPokemonList(); 19 | }); 20 | } 21 | } 22 | 23 | // Fetch the Pokemon list from pokedex and create the list 24 | // of links. 25 | function loadPokemonList() { 26 | fetch(POKEDEX) 27 | .then(function(response) { 28 | return response.json(); 29 | }) 30 | .then(function(info) { 31 | fillPokemonList(info.pokemon); 32 | 33 | // Specifically for the cookbook site :( 34 | if (window.parent !== window) { 35 | window.parent.document.body 36 | .dispatchEvent(new CustomEvent('iframeresize')); 37 | } 38 | }); 39 | } 40 | 41 | // Creates the links for the pokemon list. These links will be intercepted 42 | // by the service worker. 43 | function fillPokemonList(pokemonList) { 44 | var listElement = document.getElementById('pokemon-list'); 45 | var buffer = pokemonList.map(function(pokemon) { 46 | var uriTokens = pokemon.resource_uri.split('/'); 47 | var id = uriTokens[uriTokens.length - 2]; 48 | return '
  • ' + pokemon.name + 49 | '
  • '; 50 | }); 51 | listElement.innerHTML = buffer.join('\n'); 52 | } 53 | -------------------------------------------------------------------------------- /render-store/pokemon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Render Store - ServiceWorker Cookbook 7 | 8 | 9 | 10 | 11 |

    #{{national_id}} {{name}}

    12 |

    13 |

    Fetching model time:

    14 |

    Interpolation time:

    15 |

    Loading time:

    16 | 17 |

    18 |
    19 |
    HP:
    {{hp}}
    20 |
    Attack:
    {{attack}}
    21 |
    Defense:
    {{defense}}
    22 |
    Speed:
    {{speed}}
    23 |
    Sp Atk:
    {{sp_atk}}
    24 |
    Sp Def:
    {{sp_def}}
    25 |
    26 | 27 | 28 | -------------------------------------------------------------------------------- /render-store/service-worker.js: -------------------------------------------------------------------------------- 1 | /* global fetch */ 2 | 3 | // Install the Service Worker ASAP. 4 | self.oninstall = function(event) { 5 | event.waitUntil(self.skipWaiting()); 6 | }; 7 | 8 | self.onactivate = function(event) { 9 | event.waitUntil(self.clients.claim()); 10 | }; 11 | 12 | // When fetching, distinguish on the method. This is naive but it suffices for 13 | // the example. For more sophisticated routing alternatives, use 14 | // [ServiceWorkerWare](https://github.com/gaia-components/serviceworkerware/) 15 | // or [sw-toolbox](https://github.com/GoogleChrome/sw-toolbox). 16 | self.onfetch = function(event) { 17 | // For this example, `GET` implies looking for a cached copy... 18 | if (event.request.method === 'GET') { 19 | event.respondWith(getFromRenderStoreOrNetwork(event.request)); 20 | } else { 21 | // While `PUT` means to cache contents... 22 | event.respondWith(cacheInRenderStore(event.request).then(function() { 23 | return new Response({ status: 202 }); 24 | })); 25 | } 26 | }; 27 | 28 | // It tries to recover a cached copy for the document. If not found, 29 | // it respond from the network. 30 | function getFromRenderStoreOrNetwork(request) { 31 | return self.caches.open('render-store').then(function(cache) { 32 | return cache.match(request).then(function(match) { 33 | return match || fetch(request); 34 | }); 35 | }); 36 | } 37 | 38 | // Obtains the interpolated HTML contents of a `PUT` request from the 39 | // `pokemon.js` client code and crafts an HTML response for the interpolated 40 | // result. 41 | function cacheInRenderStore(request) { 42 | return request.text().then(function(contents) { 43 | // Craft a `text/html` response for the contents to be cached. 44 | var headers = { 'Content-Type': 'text/html' }; 45 | var response = new Response(contents, { headers: headers }); 46 | return self.caches.open('render-store').then(function(cache) { 47 | // Associate the crafted response with the 48 | // [`referrer`](https://developer.mozilla.org/en-US/docs/Web/API/Request/referrer) 49 | // property of the request which is the URL of the client page 50 | // initiating the request. 51 | return cache.put(request.referrer, response); 52 | }); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /request-deferrer/README.md: -------------------------------------------------------------------------------- 1 | # Request Deferrer 2 | This recipe shows how to enqueue requests while in offline in an _outbox-like_ buffer to perform the operations once the connection is regained. 3 | 4 | ## Difficulty 5 | Advanced 6 | 7 | ## Use Case 8 | As a modern framework developer, I want to provide an agnostic way of handling requests while offline. 9 | 10 | ## Solution 11 | Use a Service Worker to intercept requests. While offline, record the successive requests in a queue to preserve the order and answer with fake responses. If online, flush the queue to replay the session and sync with the server. 12 | 13 | This advanced technique is intended to integrate with REST APIs and it requires the client to deal with asynchronous create (`POST`) operations (HTTP answering with status code 202, Accepted). 14 | 15 | The solution is a proof of concept and does not include error handling which is a real challenge in this implementation. Some of the problems are stated in the inlined documentation. 16 | 17 | ## Category 18 | Beyond Offline 19 | -------------------------------------------------------------------------------- /request-deferrer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Request sync - ServiceWorker Cookbook 6 | 7 | 15 | 16 | 17 |

    How to

    18 |

    Try to add and delete some quotations.

    19 |

    Then go offline by disconnecting from Internet

    20 |

    And try to continue adding some quotes.

    21 |

    You can see they don't have a delete button because they are in queue

    22 |

    Now reconnect to the Internet and you'll see how they automatically synchronize.

    23 |

    Show console logs for more information. 24 |

    25 | 26 | 27 |

    28 |

    Quotes

    29 | 30 |
    31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/js/layout.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function resizeIframe(iframe) { 4 | var documentElement = iframe.contentWindow.document.documentElement; 5 | var height = documentElement.getClientRects()[0].height; 6 | iframe.style.height = height + 'px'; 7 | } 8 | 9 | (function() { 10 | 11 | document.getElementById('navToggle').addEventListener('click', function() { 12 | var book = document.querySelector('.book'); 13 | localStorage.setItem('hideNav', 1-Number(book.classList.toggle('with-nav'))); 14 | }); 15 | 16 | // This is a hacky way to highlight the current active tab. This should 17 | // probably be done in the template generation, but this is way easier. 18 | var pathname = window.location.pathname; 19 | var file = pathname.substr(pathname.lastIndexOf('/') + 1); 20 | var navItem = document.querySelector('.nav-top .item a[href="' + file + '"]'); 21 | if (navItem) { 22 | navItem.classList.add('active'); 23 | } 24 | 25 | var mainNavItem = document.querySelector('nav a[href^="' + (file.split('_')[0] || 'index.html') + '"]'); 26 | if (mainNavItem) { 27 | mainNavItem.classList.add('active'); 28 | } 29 | 30 | })(); 31 | 32 | document.addEventListener('DOMContentLoaded', function() { 33 | document.querySelector('.book').classList.toggle( 34 | 'with-nav', 35 | 1-Number(localStorage.getItem('hideNav')) 36 | ); 37 | }); 38 | 39 | // Marking as loaded triggers tabzilla fade-in 40 | window.addEventListener('load', function() { 41 | document.body.classList.add('loaded'); 42 | }); 43 | 44 | // Demos that dynamically make the page grow require extra effort in sizing the iframe 45 | // These demos should trigger a custom event on the parent: 46 | // if (window.parent !== window) { window.parent.document.body.dispatchEvent(new CustomEvent('iframeresize')); } 47 | (function(iframe) { 48 | var callback = function () { 49 | resizeIframe(iframe); 50 | }; 51 | 52 | if(iframe) { 53 | document.body.addEventListener('iframeresize', callback); 54 | iframe.addEventListener('load', callback); 55 | } 56 | })(document.querySelector('iframe')); 57 | 58 | // Launch demos in new window when image is clicked 59 | (function(launchIcon) { 60 | if(launchIcon) { 61 | launchIcon.addEventListener('click', function(e) { 62 | e.preventDefault(); 63 | window.open(launchIcon.getAttribute('data-href')); 64 | }); 65 | } 66 | })(document.querySelector('.demo-launch')); 67 | -------------------------------------------------------------------------------- /src/tpl/category.html: -------------------------------------------------------------------------------- 1 |
    2 |

    {{ title }}

    3 | 4 |

    Recipes

    5 | {% for recipe in recipes %} 6 |

    {{ recipe.title }}

    7 |
    {% autoescape false %}{{ recipe.summary }}{% endautoescape %}
    8 | {% endfor %} 9 |
    10 | -------------------------------------------------------------------------------- /src/tpl/demo.html: -------------------------------------------------------------------------------- 1 |

    {{ recipe.name }} Demo

    2 |
    3 | 4 |
    5 | -------------------------------------------------------------------------------- /src/tpl/docco/docco.jst: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | <% if (sources.length > 1) { %> 4 | 21 | <% } %> 22 | 46 |
    47 | -------------------------------------------------------------------------------- /src/tpl/index.html: -------------------------------------------------------------------------------- 1 |
    2 |

    Introduction

    3 |

    The Service Worker Cookbook is a collection of working, practical examples of using service workers in modern web sites.

    4 |

    Tip: Open your Developer Tools console to view fetch events and informative messages about what each recipe's service worker is doing!

    5 | 6 |

    Attribution

    7 |

    The Service Worker Cookbook was created by Mozilla with contributions from developers like you. All source code is available on GitHub. Contributions and requests welcome.

    8 |

    Attribution of pictures in Caching strategies category can be found at lorempixel.com.

    9 | 10 |

    Recipes

    11 | {% for recipe in recipes %} 12 |

    {{ recipe.title }}

    13 |
    {% autoescape false %}{{ recipe.summary }}{% endautoescape %}
    14 | {% endfor %} 15 |
    16 | -------------------------------------------------------------------------------- /src/tpl/intro.html: -------------------------------------------------------------------------------- 1 |
    2 | {% autoescape false %}{{ markdown }}{% endautoescape %} 3 |
    -------------------------------------------------------------------------------- /strategy-cache-and-update/README.md: -------------------------------------------------------------------------------- 1 | # Cache and update 2 | The recipe provides a service worker responding from cache to deliver fast 3 | responses and also updating the cache entry from the network. 4 | 5 | ## Difficulty 6 | Beginner 7 | 8 | ## Use Case 9 | You want to instantly show content and you don't mind to be temporarily out of 10 | sync with the server. 11 | 12 | ## Solution 13 | Serve the content from the cache and also perform a network request to get fresh 14 | data to update the cache entry ensuring next time the user visits the page they 15 | will see up to date content. 16 | 17 | See 18 | [cache, update and refresh](/strategy-cache-update-and-refresh.html) for a 19 | variation in which the UI is notified when the fresh data is available. 20 | 21 | ## Category 22 | Caching strategies 23 | -------------------------------------------------------------------------------- /strategy-cache-and-update/controlled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cache and update: controlled page - ServiceWorker Cookbook 6 | 7 | 12 | 13 | 14 |

    Cache and update

    15 | sample asset 16 |

    This image request originates from a controlled page so the image will 17 | be served by the service worker. Even if the content in the server changes, 18 | the page will show out of date content since it is served by the cache but 19 | thanks to the update process, next visit will show up to date content.

    20 | 21 | 22 | -------------------------------------------------------------------------------- /strategy-cache-and-update/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cache and update - ServiceWorker Cookbook 6 | 7 | 29 | 30 | 31 |

    Cache and update

    32 |
    33 | 34 | 35 |
    36 |

    The images in these iframes point to the same asset in the server. But the first is controlled by the service worker and the second is not.

    37 |

    In the server, the image is updated every 10 seconds, try to click on reload to cause new requests from the controlled and uncontrolled pages.

    38 |

    39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /strategy-cache-and-update/index.js: -------------------------------------------------------------------------------- 1 | // Register the ServiceWorker limiting its action to those URL starting 2 | // by `controlled`. The scope is not a path but a prefix. First, it is 3 | // converted into an absolute URL, then used to determine if a page is 4 | // controlled by testing it is a prefix of the request URL. 5 | navigator.serviceWorker.register('service-worker.js', { 6 | scope: './controlled' 7 | }); 8 | 9 | // Load controlled and uncontrolled pages once the worker is active. 10 | navigator.serviceWorker.ready.then(reload); 11 | 12 | var referenceIframe = document.getElementById('reference'); 13 | var sampleIframe = document.getElementById('sample'); 14 | 15 | // Fix heights every time the iframe reload. 16 | referenceIframe.onload = fixHeight; 17 | sampleIframe.onload = fixHeight; 18 | 19 | // Reload both iframes on demand. 20 | var reloadButton = document.querySelector('#reload'); 21 | reloadButton.onclick = reload; 22 | 23 | // Loads the controlled and uncontrolled iframes. 24 | function loadIframes() { 25 | referenceIframe.src = './non-controlled.html'; 26 | sampleIframe.src = './controlled.html'; 27 | } 28 | 29 | // Compute the correct height for the content of an iframe and adjust it 30 | // to match the content. 31 | function fixHeight(evt) { 32 | var iframe = evt.target; 33 | var document = iframe.contentWindow.document.documentElement; 34 | iframe.style.height = document.getClientRects()[0].height + 'px'; 35 | // Specifically for the cookbook site :( 36 | if (window.parent !== window) { 37 | window.parent.document.body.dispatchEvent(new CustomEvent('iframeresize')); 38 | } 39 | } 40 | 41 | // Reload both iframes. 42 | function reload() { 43 | referenceIframe.contentWindow.location.reload(); 44 | sampleIframe.contentWindow.location.reload(); 45 | } 46 | -------------------------------------------------------------------------------- /strategy-cache-and-update/non-controlled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cache and update: non controlled page - ServiceWorker Cookbook 6 | 7 | 12 | 13 | 14 |

    Always synchronized

    15 | sample asset 16 |

    This image originates from a non controlled page so, if you reload, it will be always synced with the version in the server.

    17 | 18 | 19 | -------------------------------------------------------------------------------- /strategy-cache-and-update/server.js: -------------------------------------------------------------------------------- 1 | var MAX_IMAGES = 50; 2 | var imageNumber = 0; 3 | 4 | module.exports = function(app, route) { 5 | app.get(route + 'asset', function(req, res) { 6 | serveImage(res, 10000); 7 | }); 8 | }; 9 | 10 | var lastUpdate = 0; 11 | 12 | function serveImage(res, timeout) { 13 | var now = Date.now(); 14 | if (now - lastUpdate > timeout) { 15 | imageNumber = (imageNumber + 1) % MAX_IMAGES; 16 | lastUpdate = Date.now(); 17 | } 18 | var imageName = 'picture-' + (imageNumber + 1) + '.png'; 19 | res.sendFile(imageName, { root: './imgs/random/' }); 20 | } 21 | -------------------------------------------------------------------------------- /strategy-cache-and-update/service-worker.js: -------------------------------------------------------------------------------- 1 | var CACHE = 'cache-and-update'; 2 | 3 | // On install, cache some resources. 4 | self.addEventListener('install', function(evt) { 5 | console.log('The service worker is being installed.'); 6 | 7 | // Ask the service worker to keep installing until the returning promise 8 | // resolves. 9 | evt.waitUntil(precache()); 10 | }); 11 | 12 | // On fetch, use cache but update the entry with the latest contents 13 | // from the server. 14 | self.addEventListener('fetch', function(evt) { 15 | console.log('The service worker is serving the asset.'); 16 | // You can use `respondWith()` to answer immediately, without waiting for the 17 | // network response to reach the service worker... 18 | evt.respondWith(fromCache(evt.request)); 19 | // ...and `waitUntil()` to prevent the worker from being killed until the 20 | // cache is updated. 21 | evt.waitUntil(update(evt.request)); 22 | }); 23 | 24 | // Open a cache and use `addAll()` with an array of assets to add all of them 25 | // to the cache. Return a promise resolving when all the assets are added. 26 | function precache() { 27 | return caches.open(CACHE).then(function (cache) { 28 | return cache.addAll([ 29 | './controlled.html', 30 | './asset' 31 | ]); 32 | }); 33 | } 34 | 35 | // Open the cache where the assets were stored and search for the requested 36 | // resource. Notice that in case of no matching, the promise still resolves 37 | // but it does with `undefined` as value. 38 | function fromCache(request) { 39 | return caches.open(CACHE).then(function (cache) { 40 | return cache.match(request).then(function (matching) { 41 | return matching || Promise.reject('no-match'); 42 | }); 43 | }); 44 | } 45 | 46 | // Update consists in opening the cache, performing a network request and 47 | // storing the new response data. 48 | function update(request) { 49 | return caches.open(CACHE).then(function (cache) { 50 | return fetch(request).then(function (response) { 51 | return cache.put(request, response); 52 | }); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /strategy-cache-only/README.md: -------------------------------------------------------------------------------- 1 | # Cache only 2 | The recipe provides a service worker always answering from cache on `fetch` events. 3 | 4 | ## Difficulty 5 | Beginner 6 | 7 | ## Use Case 8 | For a given version of your site, you have static content that never changes 9 | such as the shell around the content. 10 | 11 | ## Solution 12 | Add static content during the installation of the service worker and use the 13 | cache to retrieve it whether the network is available or not. 14 | 15 | ## Category 16 | Caching strategies 17 | -------------------------------------------------------------------------------- /strategy-cache-only/controlled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cache only: controlled page - ServiceWorker Cookbook 6 | 7 | 12 | 13 | 14 |

    Cache only

    15 | sample asset 16 |

    This image request originates from a page under the service worker scope so 17 | the image will be served by the service worker. Due to cache only, it will 18 | be always served from the cache even if the server version changes and you 19 | reload.

    20 | 21 | 22 | -------------------------------------------------------------------------------- /strategy-cache-only/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cache only - ServiceWorker Cookbook 6 | 7 | 29 | 30 | 31 |

    Cache only

    32 |
    33 | 34 | 35 |
    36 |

    The images in these iframes point to the same asset in the server. But the first is controlled by the service worker and the second is not.

    37 |

    In the server, the image is updated every 10 seconds, try to click on reload to cause new requests from the controlled and uncontrolled pages.

    38 |

    39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /strategy-cache-only/index.js: -------------------------------------------------------------------------------- 1 | // Register the ServiceWorker limiting its scope of action to those URL starting 2 | // by `controlled`. The scope is not a path but a prefix. First, it is 3 | // converted into an absolute URL, then used to determine if a page is 4 | // controlled by testing it is a prefix of the request URL. 5 | navigator.serviceWorker.register('service-worker.js', { 6 | scope: './controlled' 7 | }); 8 | 9 | // Load controlled and uncontrolled pages once the worker is active. 10 | navigator.serviceWorker.ready.then(reload); 11 | 12 | var referenceIframe = document.getElementById('reference'); 13 | var sampleIframe = document.getElementById('sample'); 14 | 15 | // Fix heights every time the iframe is reloaded. 16 | referenceIframe.onload = fixHeight; 17 | sampleIframe.onload = fixHeight; 18 | 19 | // Reload both iframes on demand. 20 | var reloadButton = document.querySelector('#reload'); 21 | reloadButton.onclick = reload; 22 | 23 | // Loads the controlled and uncontrolled iframes. 24 | function loadIframes() { 25 | referenceIframe.src = './non-controlled.html'; 26 | sampleIframe.src = './controlled.html'; 27 | } 28 | 29 | // Compute the correct height for the content of an iframe and adjust it 30 | // to match the content. 31 | function fixHeight(evt) { 32 | var iframe = evt.target; 33 | var document = iframe.contentWindow.document.documentElement; 34 | iframe.style.height = document.getClientRects()[0].height + 'px'; 35 | // Specifically for the cookbook site :( 36 | if (window.parent !== window) { 37 | window.parent.document.body.dispatchEvent(new CustomEvent('iframeresize')); 38 | } 39 | } 40 | 41 | // Reload both iframes. 42 | function reload() { 43 | referenceIframe.contentWindow.location.reload(); 44 | sampleIframe.contentWindow.location.reload(); 45 | } 46 | -------------------------------------------------------------------------------- /strategy-cache-only/non-controlled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cache only: non controlled page - ServiceWorker Cookbook 6 | 7 | 12 | 13 | 14 |

    Always fresh from the server

    15 | sample asset 16 |

    This image originates from a non controlled page so, if you reload, it will be always synced with the version in the server.

    17 | 18 | 19 | -------------------------------------------------------------------------------- /strategy-cache-only/server.js: -------------------------------------------------------------------------------- 1 | var MAX_IMAGES = 50; 2 | var imageNumber = 0; 3 | 4 | module.exports = function(app, route) { 5 | app.get(route + 'asset', function(req, res) { 6 | serveImage(res, 10000); 7 | }); 8 | }; 9 | 10 | var lastUpdate = -Infinity; 11 | 12 | function serveImage(res, timeout) { 13 | var now = Date.now(); 14 | if (now - lastUpdate > timeout) { 15 | imageNumber = (imageNumber + 1) % MAX_IMAGES; 16 | lastUpdate = Date.now(); 17 | } 18 | var imageName = 'picture-' + (imageNumber + 1) + '.png'; 19 | res.sendFile(imageName, { root: './imgs/random/' }); 20 | } 21 | -------------------------------------------------------------------------------- /strategy-cache-only/service-worker.js: -------------------------------------------------------------------------------- 1 | var CACHE = 'cache-only'; 2 | 3 | // On install, cache some resources. 4 | self.addEventListener('install', function(evt) { 5 | console.log('The service worker is being installed.'); 6 | 7 | // Ask the service worker to keep installing until the returning promise 8 | // resolves. 9 | evt.waitUntil(precache()); 10 | }); 11 | 12 | // On fetch, use cache only strategy. 13 | self.addEventListener('fetch', function(evt) { 14 | console.log('The service worker is serving the asset.'); 15 | evt.respondWith(fromCache(evt.request)); 16 | }); 17 | 18 | // Open a cache and use `addAll()` with an array of assets to add all of them 19 | // to the cache. Return a promise resolving when all the assets are added. 20 | function precache() { 21 | return caches.open(CACHE).then(function (cache) { 22 | return cache.addAll([ 23 | './controlled.html', 24 | './asset' 25 | ]); 26 | }); 27 | } 28 | 29 | // Open the cache where the assets were stored and search for the requested 30 | // resource. Notice that in case of no matching, the promise still resolves 31 | // but it does with `undefined` as value. 32 | function fromCache(request) { 33 | return caches.open(CACHE).then(function (cache) { 34 | return cache.match(request).then(function (matching) { 35 | return matching || Promise.reject('no-match'); 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /strategy-cache-update-and-refresh/README.md: -------------------------------------------------------------------------------- 1 | # Cache, update and refresh 2 | The recipe provides a service worker responding from cache to deliver fast 3 | responses and also updating the cache entry from the network. When the network 4 | response is ready, the UI updates automatically. 5 | 6 | ## Difficulty 7 | Intermediate 8 | 9 | ## Use Case 10 | You want to instantly show content while retrieving new content in background. 11 | Once the new content is available you want to show it somehow. 12 | 13 | ## Solution 14 | Serve the content from the cache but at the same time, perform a network request 15 | to update the cache entry and inform the UI about new up to date content. 16 | 17 | ## Category 18 | Caching strategies 19 | -------------------------------------------------------------------------------- /strategy-cache-update-and-refresh/controlled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cache, update and refresh: controlled page - ServiceWorker Cookbook 6 | 7 | 22 | 23 | 24 |

    Cache first with update notice

    25 | sample asset 26 | 27 |

    This image request originates from a controlled page so the image will 28 | be served by the service worker. Even if the content in the server changes, 29 | the page will show out of date content since it is served by the cache but 30 | the service worker will inform the UI when the new content is 31 | available.

    32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /strategy-cache-update-and-refresh/controlled.js: -------------------------------------------------------------------------------- 1 | var CACHE = 'cache-update-and-refresh'; 2 | 3 | if ('serviceWorker' in navigator) { 4 | navigator.serviceWorker.onmessage = function (evt) { 5 | var message = JSON.parse(evt.data); 6 | 7 | var isRefresh = message.type === 'refresh'; 8 | var isAsset = message.url.includes('asset'); 9 | var lastETag = localStorage.currentETag; 10 | 11 | // [ETag](https://en.wikipedia.org/wiki/HTTP_ETag) header usually contains 12 | // the hash of the resource so it is a very effective way of check for fresh 13 | // content. 14 | var isNew = lastETag !== message.eTag; 15 | 16 | if (isRefresh && isAsset && isNew) { 17 | // Escape the first time (when there is no ETag yet) 18 | if (lastETag) { 19 | // Inform the user about the update 20 | notice.hidden = false; 21 | } 22 | // For teaching purposes, although this information is in the offline 23 | // cache and it could be retrieved from the service worker, keeping track 24 | // of the header in the `localStorage` keeps the implementation simple. 25 | localStorage.currentETag = message.eTag; 26 | } 27 | }; 28 | 29 | var notice = document.querySelector('#update-notice'); 30 | 31 | var update = document.querySelector('#update'); 32 | update.onclick = function (evt) { 33 | var img = document.querySelector('img'); 34 | // Avoid navigation. 35 | evt.preventDefault(); 36 | // Open the proper cache. 37 | caches.open(CACHE) 38 | // Get the updated response. 39 | .then(function (cache) { 40 | return cache.match(img.src); 41 | }) 42 | // Extract the body as a blob. 43 | .then(function (response) { 44 | return response.blob(); 45 | }) 46 | // Update the image content. 47 | .then(function (bodyBlob) { 48 | var url = URL.createObjectURL(bodyBlob); 49 | img.src = url; 50 | notice.hidden = true; 51 | }); 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /strategy-cache-update-and-refresh/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cache, update and refresh - ServiceWorker Cookbook 6 | 7 | 29 | 30 | 31 |

    Cache, update and refresh

    32 |
    33 | 34 | 35 |
    36 |

    The images in these iframes point to the same asset in the server. But the first is controlled by the service worker and the second is not.

    37 |

    In the server, the image is updated every 10 seconds, try to click on reload to cause new requests from the controlled and uncontrolled pages.

    38 |

    39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /strategy-cache-update-and-refresh/index.js: -------------------------------------------------------------------------------- 1 | // Register the ServiceWorker limiting its action to those URLs starting 2 | // by `controlled`. The scope is not a path but a prefix. First, it is 3 | // converted into an absolute URL, then used to determine if a page is 4 | // controlled by testing it is a prefix of the request URL. 5 | navigator.serviceWorker.register('service-worker.js', { 6 | scope: './controlled' 7 | }); 8 | 9 | // Load controlled and uncontrolled pages once the worker is active. 10 | navigator.serviceWorker.ready.then(reload); 11 | 12 | var referenceIframe = document.getElementById('reference'); 13 | var sampleIframe = document.getElementById('sample'); 14 | 15 | // Fix heights every time the iframe reload. 16 | referenceIframe.onload = fixHeight; 17 | sampleIframe.onload = fixHeight; 18 | 19 | // Reload both iframes on demand. 20 | var reloadButton = document.querySelector('#reload'); 21 | reloadButton.onclick = reload; 22 | 23 | // Loads the controlled and uncontrolled iframes. 24 | function loadIframes() { 25 | referenceIframe.src = './non-controlled.html'; 26 | sampleIframe.src = './controlled.html'; 27 | } 28 | 29 | // Compute the correct height for the content of an iframe and adjust it 30 | // to match the content. 31 | function fixHeight(evt) { 32 | var iframe = evt.target; 33 | var document = iframe.contentWindow.document.documentElement; 34 | iframe.style.height = document.getClientRects()[0].height + 'px'; 35 | // Specifically for the cookbook site :( 36 | if (window.parent !== window) { 37 | window.parent.document.body.dispatchEvent(new CustomEvent('iframeresize')); 38 | } 39 | } 40 | 41 | // Reload both iframes. 42 | function reload() { 43 | referenceIframe.contentWindow.location.reload(); 44 | sampleIframe.contentWindow.location.reload(); 45 | } 46 | -------------------------------------------------------------------------------- /strategy-cache-update-and-refresh/non-controlled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cache, update and refresh: non controlled page - ServiceWorker Cookbook 6 | 7 | 12 | 13 | 14 |

    Always fresh from the server

    15 | sample asset 16 |

    This image originates from a non controlled page, so if you reload, it will be always synced with the version in the server.

    17 | 18 | 19 | -------------------------------------------------------------------------------- /strategy-cache-update-and-refresh/server.js: -------------------------------------------------------------------------------- 1 | var MAX_IMAGES = 50; 2 | var imageNumber = 0; 3 | 4 | module.exports = function(app, route) { 5 | app.get(route + 'asset', function(req, res) { 6 | serveImage(res, 10000); 7 | }); 8 | }; 9 | 10 | var lastUpdate = -Infinity; 11 | 12 | function serveImage(res, timeout) { 13 | var now = Date.now(); 14 | if (now - lastUpdate > timeout) { 15 | imageNumber = (imageNumber + 1) % MAX_IMAGES; 16 | lastUpdate = Date.now(); 17 | } 18 | var imageName = 'picture-' + (imageNumber + 1) + '.png'; 19 | res.sendFile(imageName, { root: './imgs/random/' }); 20 | } 21 | -------------------------------------------------------------------------------- /strategy-cache-update-and-refresh/service-worker.js: -------------------------------------------------------------------------------- 1 | var CACHE = 'cache-update-and-refresh'; 2 | 3 | // On install, cache some resource. 4 | self.addEventListener('install', function(evt) { 5 | console.log('The service worker is being installed.'); 6 | // Open a cache and use `addAll()` with an array of assets to add all of them 7 | // to the cache. Ask the service worker to keep installing until the 8 | // returning promise resolves. 9 | evt.waitUntil(caches.open(CACHE).then(function (cache) { 10 | cache.addAll([ 11 | './controlled.html', 12 | './asset' 13 | ]); 14 | })); 15 | }); 16 | 17 | // On fetch, use cache but update the entry with the latest contents 18 | // from the server. 19 | self.addEventListener('fetch', function(evt) { 20 | console.log('The service worker is serving the asset.'); 21 | // You can use `respondWith()` to answer ASAP... 22 | evt.respondWith(fromCache(evt.request)); 23 | // ...and `waitUntil()` to prevent the worker to be killed until 24 | // the cache is updated. 25 | evt.waitUntil( 26 | update(evt.request) 27 | // Finally, send a message to the client to inform it about the 28 | // resource is up to date. 29 | .then(refresh) 30 | ); 31 | }); 32 | 33 | // Open the cache where the assets were stored and search for the requested 34 | // resource. Notice that in case of no matching, the promise still resolves 35 | // but it does with `undefined` as value. 36 | function fromCache(request) { 37 | return caches.open(CACHE).then(function (cache) { 38 | return cache.match(request); 39 | }); 40 | } 41 | 42 | 43 | // Update consists in opening the cache, performing a network request and 44 | // storing the new response data. 45 | function update(request) { 46 | return caches.open(CACHE).then(function (cache) { 47 | return fetch(request).then(function (response) { 48 | return cache.put(request, response.clone()).then(function () { 49 | return response; 50 | }); 51 | }); 52 | }); 53 | } 54 | 55 | // Sends a message to the clients. 56 | function refresh(response) { 57 | return self.clients.matchAll().then(function (clients) { 58 | clients.forEach(function (client) { 59 | // Encode which resource has been updated. By including the 60 | // [ETag](https://en.wikipedia.org/wiki/HTTP_ETag) the client can 61 | // check if the content has changed. 62 | var message = { 63 | type: 'refresh', 64 | url: response.url, 65 | // Notice not all servers return the ETag header. If this is not 66 | // provided you should use other cache headers or rely on your own 67 | // means to check if the content has changed. 68 | eTag: response.headers.get('ETag') 69 | }; 70 | // Tell the client about the update. 71 | client.postMessage(JSON.stringify(message)); 72 | }); 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /strategy-embedded-fallback/README.md: -------------------------------------------------------------------------------- 1 | # Embedded fallback 2 | The recipe provides a service worker serving an embedded content fallback in 3 | case of missing resources. 4 | 5 | ## Difficulty 6 | Intermediate 7 | 8 | ## Use Case 9 | You want to make sure the users always receive some content, even if the network 10 | is not available. 11 | 12 | ## Solution 13 | Embed fallback content and serve it in case of failure while requesting 14 | resources. 15 | 16 | ## Category 17 | Caching strategies 18 | -------------------------------------------------------------------------------- /strategy-embedded-fallback/controlled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Embedded fallback: controlled page - ServiceWorker Cookbook 6 | 7 | 12 | 13 | 14 |

    Offline fallback

    15 | missing asset 16 |

    This image request points to an unavailable resource but since the page 17 | is controlled by the SW providing an embedded fallback it won't fail.

    18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /strategy-embedded-fallback/controlled.js: -------------------------------------------------------------------------------- 1 | // To be able to provide fallbacks since the beginning, we can only load the 2 | // image once we know the SW is ready. In addition, the service worker should 3 | // start intercepting without waiting for current clients to be closed. 4 | 5 | // Checking for controller is an effective way to see if there is an active 6 | // service worker controlling the page. 7 | if (navigator.serviceWorker.controller) { 8 | loadImage(); 9 | 10 | // If there is not, wait until there is one... 11 | } else { 12 | navigator.serviceWorker.oncontrollerchange = function() { 13 | // ...and monitor it until it's ready to intercept requests. 14 | this.controller.onstatechange = function() { 15 | if (this.state === 'activated') { 16 | loadImage(); 17 | } 18 | }; 19 | }; 20 | } 21 | 22 | function loadImage() { 23 | document.querySelector('img').src = './missing'; 24 | } 25 | -------------------------------------------------------------------------------- /strategy-embedded-fallback/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cache only - ServiceWorker Cookbook 6 | 7 | 30 | 31 | 32 |

    Embedded fallback

    33 |
    34 | 35 | 36 |
    37 |

    The images in these iframe points to the same asset in the server. But the first is controlled by the service worker and the second is not.

    38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /strategy-embedded-fallback/index.js: -------------------------------------------------------------------------------- 1 | // Register the ServiceWorker limiting its action to those URL starting 2 | // by `controlled`. The scope is not a path but a prefix. First, it is 3 | // converted into an absolute URL, then used to determine if a page is 4 | // controlled by testing it is a prefix of the request URL. 5 | navigator.serviceWorker.register('service-worker.js', { 6 | scope: './controlled' 7 | }); 8 | 9 | var referenceIframe = document.getElementById('reference'); 10 | var sampleIframe = document.getElementById('sample'); 11 | 12 | // Fix heights every time the iframe reload. 13 | referenceIframe.onload = fixHeight; 14 | sampleIframe.onload = fixHeight; 15 | 16 | // Compute the correct height for the content of an iframe and adjust it 17 | // to match the content. 18 | function fixHeight(evt) { 19 | var iframe = evt.target; 20 | var document = iframe.contentWindow.document.documentElement; 21 | iframe.style.height = document.getClientRects()[0].height + 'px'; 22 | // Specifically for the cookbook site :( 23 | if (window.parent !== window) { 24 | window.parent.document.body.dispatchEvent(new CustomEvent('iframeresize')); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /strategy-embedded-fallback/non-controlled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Embedded fallback: non controlled page - ServiceWorker Cookbook 6 | 7 | 12 | 13 | 14 |

    Not controlled, no fallback

    15 | sample asset 16 |

    This image request points to an unavailable resource. Since there is no 17 | service worker providing a fallback, it will result in a missing image.

    18 | 19 | 20 | -------------------------------------------------------------------------------- /strategy-embedded-fallback/server.js: -------------------------------------------------------------------------------- 1 | var MAX_IMAGES = 50; 2 | var imageNumber = 0; 3 | 4 | module.exports = function(app, route) { 5 | app.get(route + 'asset', function(req, res) { 6 | serveImage(res, 10000); 7 | }); 8 | }; 9 | 10 | var lastUpdate = -Infinity; 11 | 12 | function serveImage(res, timeout) { 13 | var now = Date.now(); 14 | if (now - lastUpdate > timeout) { 15 | imageNumber = (imageNumber + 1) % MAX_IMAGES; 16 | lastUpdate = Date.now(); 17 | } 18 | var imageName = 'picture-' + (imageNumber + 1) + '.png'; 19 | res.sendFile(imageName, { root: './imgs/random/' }); 20 | } 21 | -------------------------------------------------------------------------------- /strategy-network-or-cache/README.md: -------------------------------------------------------------------------------- 1 | # Network or cache 2 | The service worker in this recipe tries to retrieve the most up to date content 3 | from the network but if the network is taking too long, it will serve cached 4 | content instead. 5 | 6 | ## Difficulty 7 | Beginner 8 | 9 | ## Use Case 10 | You want to show the most up to date content but it's preferable to load fast. 11 | 12 | ## Solution 13 | Serve content from network but include a timeout to fall back to cached data if 14 | the answer from the network doesn't arrive on time. 15 | 16 | ## Category 17 | Caching strategies 18 | -------------------------------------------------------------------------------- /strategy-network-or-cache/controlled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Network or cache: controlled page - ServiceWorker Cookbook 6 | 7 | 12 | 13 | 14 |

    Network or cache

    15 | sample asset 16 |

    This image request originates from a controlled page so the image will 17 | be served by the service worker. The service worker will try to retrieve 18 | the most updated content from network but if the answer does not arrive 19 | before a timeout, it will fall back to the cached content. Try to 20 | 21 | adjust throttling to GPRS to see the effects of network latency.

    22 | 23 | 24 | -------------------------------------------------------------------------------- /strategy-network-or-cache/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Network or cache - ServiceWorker Cookbook 6 | 7 | 29 | 30 | 31 |

    Network or cache

    32 |

    Try to adjust network throttling to GPRS to see the image falling back to the cached content.

    33 |
    34 | 35 | 36 |
    37 |

    The images in these iframes point to the same asset in the server. But the first is controlled by the service worker and the second is not.

    38 |

    In the server, the image is updated every 10 seconds, try to click on reload to cause new requests from the controlled and uncontrolled pages.

    39 |

    40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /strategy-network-or-cache/index.js: -------------------------------------------------------------------------------- 1 | // Register the ServiceWorker limiting its action to those URL starting 2 | // by `controlled`. The scope is not a path but a prefix. First, it is 3 | // converted into an absolute URL, then used to determine if a page is 4 | // controlled by testing it is a prefix of the request URL. 5 | navigator.serviceWorker.register('service-worker.js', { 6 | scope: './controlled' 7 | }); 8 | 9 | // Load controlled and uncontrolled pages once the worker is active. 10 | navigator.serviceWorker.ready.then(reload); 11 | 12 | var referenceIframe = document.getElementById('reference'); 13 | var sampleIframe = document.getElementById('sample'); 14 | 15 | // Fix heights every time the iframe reload. 16 | referenceIframe.onload = fixHeight; 17 | sampleIframe.onload = fixHeight; 18 | 19 | // Reload both iframes on demand. 20 | var reloadButton = document.querySelector('#reload'); 21 | reloadButton.onclick = reload; 22 | 23 | // Loads the controlled and uncontrolled iframes. 24 | function loadIframes() { 25 | referenceIframe.src = './non-controlled.html'; 26 | sampleIframe.src = './controlled.html'; 27 | } 28 | 29 | // Compute the correct height for the content of an iframe and adjust it 30 | // to match the content. 31 | function fixHeight(evt) { 32 | var iframe = evt.target; 33 | var document = iframe.contentWindow.document.documentElement; 34 | iframe.style.height = document.getClientRects()[0].height + 'px'; 35 | // Specifically for the cookbook site :( 36 | if (window.parent !== window) { 37 | window.parent.document.body.dispatchEvent(new CustomEvent('iframeresize')); 38 | } 39 | } 40 | 41 | // Reload both iframes. 42 | function reload() { 43 | referenceIframe.contentWindow.location.reload(); 44 | sampleIframe.contentWindow.location.reload(); 45 | } 46 | -------------------------------------------------------------------------------- /strategy-network-or-cache/non-controlled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Network or cache: non controlled page - ServiceWorker Cookbook 6 | 7 | 12 | 13 | 14 |

    Always synchronized

    15 | sample asset 16 |

    This image originates from a non controlled page so, if you reload, it will be always synced with the version in the server.

    17 | 18 | 19 | -------------------------------------------------------------------------------- /strategy-network-or-cache/server.js: -------------------------------------------------------------------------------- 1 | var MAX_IMAGES = 50; 2 | var imageNumber = 0; 3 | 4 | module.exports = function(app, route) { 5 | app.get(route + 'asset', function(req, res) { 6 | serveImage(res, 10000); 7 | }); 8 | }; 9 | 10 | var lastUpdate = -Infinity; 11 | 12 | function serveImage(res, timeout) { 13 | var now = Date.now(); 14 | if (now - lastUpdate > timeout) { 15 | imageNumber = (imageNumber + 1) % MAX_IMAGES; 16 | lastUpdate = Date.now(); 17 | } 18 | var imageName = 'picture-' + (imageNumber + 1) + '.png'; 19 | res.sendFile(imageName, { root: './imgs/random/' }); 20 | } 21 | -------------------------------------------------------------------------------- /strategy-network-or-cache/service-worker.js: -------------------------------------------------------------------------------- 1 | var CACHE = 'network-or-cache'; 2 | 3 | // On install, cache some resource. 4 | self.addEventListener('install', function(evt) { 5 | console.log('The service worker is being installed.'); 6 | 7 | // Ask the service worker to keep installing until the returning promise 8 | // resolves. 9 | evt.waitUntil(precache()); 10 | }); 11 | 12 | // On fetch, use cache but update the entry with the latest contents 13 | // from the server. 14 | self.addEventListener('fetch', function(evt) { 15 | console.log('The service worker is serving the asset.'); 16 | // Try network and if it fails, go for the cached copy. 17 | evt.respondWith(fromNetwork(evt.request, 400).catch(function () { 18 | return fromCache(evt.request); 19 | })); 20 | }); 21 | 22 | // Open a cache and use `addAll()` with an array of assets to add all of them 23 | // to the cache. Return a promise resolving when all the assets are added. 24 | function precache() { 25 | return caches.open(CACHE).then(function (cache) { 26 | return cache.addAll([ 27 | './controlled.html', 28 | './asset' 29 | ]); 30 | }); 31 | } 32 | 33 | // Time limited network request. If the network fails or the response is not 34 | // served before timeout, the promise is rejected. 35 | function fromNetwork(request, timeout) { 36 | return new Promise(function (fulfill, reject) { 37 | // Reject in case of timeout. 38 | var timeoutId = setTimeout(reject, timeout); 39 | // Fulfill in case of success. 40 | fetch(request).then(function (response) { 41 | clearTimeout(timeoutId); 42 | fulfill(response); 43 | // Reject also if network fetch rejects. 44 | }, reject); 45 | }); 46 | } 47 | 48 | // Open the cache where the assets were stored and search for the requested 49 | // resource. Notice that in case of no matching, the promise still resolves 50 | // but it does with `undefined` as value. 51 | function fromCache(request) { 52 | return caches.open(CACHE).then(function (cache) { 53 | return cache.match(request).then(function (matching) { 54 | return matching || Promise.reject('no-match'); 55 | }); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /tools.js: -------------------------------------------------------------------------------- 1 | // This function is needed because Chrome doesn't accept a base64 encoded string 2 | // as value for applicationServerKey in pushManager.subscribe yet 3 | // https://bugs.chromium.org/p/chromium/issues/detail?id=802280 4 | function urlBase64ToUint8Array(base64String) { 5 | var padding = '='.repeat((4 - base64String.length % 4) % 4); 6 | var base64 = (base64String + padding) 7 | .replace(/\-/g, '+') 8 | .replace(/_/g, '/'); 9 | 10 | var rawData = window.atob(base64); 11 | var outputArray = new Uint8Array(rawData.length); 12 | 13 | for (var i = 0; i < rawData.length; ++i) { 14 | outputArray[i] = rawData.charCodeAt(i); 15 | } 16 | return outputArray; 17 | } 18 | -------------------------------------------------------------------------------- /virtual-server/README.md: -------------------------------------------------------------------------------- 1 | # Virtual Server 2 | This recipe shows a service worker acting like a remote server. 3 | 4 | ## Difficulty 5 | Intermediate 6 | 7 | ## Use Case 8 | As an application developer, I want to to fully decouple UI from business logic. 9 | 10 | ## Solution 11 | With REST APIs you can decouple client from business logic. The business logic is actually a separated component placed on a remote server. With Service Workers you can do the same. Simply move your business logic to a Service Worker responding on fetch events. 12 | 13 | Instead of implementing your own logic to distinguish between routes and request methods, use [ServiceWorkerWare](https://github.com/gaia-components/serviceworkerware) or [sw-toolbox](https://github.com/GoogleChrome/sw-toolbox#defining-routes)' router feature to write your worker in a declarative way. 14 | 15 | The client code is virtually identical to [that in the API analytics recipe](/api-analytics_index_doc.html) (the report link has been removed). On the contrary, the [remote _Express server_](/api-analytics_server_doc.html) has been completely replaced by the _[ServiceWorkerWare](https://github.com/gaia-components/serviceworkerware) worker_. 16 | 17 | ## Category 18 | Beyond Offline 19 | -------------------------------------------------------------------------------- /virtual-server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Virtual server - ServiceWorker Cookbook 6 | 7 | 8 | 9 |

    How to

    10 |

    Try to add and delete some quotations. The session is not intended to survive refreshing the page but you could see it pretending to survive. This means the worker holding the state has not been killed yet.

    11 |

    12 | 13 | 14 |

    15 |

    Quotes

    16 | 17 |
    18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /virtual-server/service-worker.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | /* global importScripts, ServiceWorkerWare */ 3 | importScripts('./lib/ServiceWorkerWare.js'); 4 | 5 | // List of the default quotations. 6 | var quotations = [ 7 | { 8 | text: 'Humanity is smart. Sometime in the technology world we think' + 9 | 'we are smarter, but we are not smarter than you.', 10 | author: 'Mitchell Baker' 11 | }, 12 | { 13 | text: 'A computer would deserve to be called intelligent if it could ' + 14 | 'deceive a human into believing that it was human.', 15 | author: 'Alan Turing' 16 | }, 17 | { 18 | text: 'If you optimize everything, you will always be unhappy.', 19 | author: 'Donald Knuth' 20 | }, 21 | { 22 | text: 'If you don\'t fail at least 90 percent of the time' + 23 | 'you\'re not aiming high enough', 24 | author: 'Alan Kay' 25 | }, 26 | { 27 | text: 'Colorless green ideas sleep furiously.', 28 | author: 'Noam Chomsky' 29 | } 30 | ].map(function(quotation, index) { 31 | // Add the id and the sticky flag to make the default quotations non removable. 32 | quotation.id = index + 1; 33 | quotation.isSticky = true; 34 | 35 | return quotation; 36 | }); 37 | 38 | // Determine the root for the routes. I.e, if the Service Worker URL is 39 | // `http://example.com/path/to/sw.js`, then the root is 40 | // `http://example.com/path/to/` 41 | var root = (function() { 42 | var tokens = (self.location + '').split('/'); 43 | tokens[tokens.length - 1] = ''; 44 | return tokens.join('/'); 45 | })(); 46 | 47 | 48 | // By using Mozilla's ServiceWorkerWare we can quickly setup some routes 49 | // for a _virtual server_. Compare this code with the one from the 50 | // [server side in the API analytics recipe](/api-analytics_server_doc.html). 51 | var worker = new ServiceWorkerWare(); 52 | 53 | // Returns an array with all quotations. 54 | worker.get(root + 'api/quotations', function(req, res) { 55 | return new Response(JSON.stringify(quotations.filter(function(item) { 56 | return item !== null; 57 | }))); 58 | }); 59 | 60 | // Delete a quote specified by id. The id is the position in the collection 61 | // of quotations (the position is 1 based instead of 0). 62 | worker.delete(root + 'api/quotations/:id', function(req, res) { 63 | var id = parseInt(req.parameters.id, 10) - 1; 64 | if (!quotations[id].isSticky) { 65 | quotations[id] = null; 66 | } 67 | return new Response({ status: 204 }); 68 | }); 69 | 70 | // Add a new quote to the collection. 71 | worker.post(root + 'api/quotations', function(req, res) { 72 | return req.json().then(function(quote) { 73 | quote.id = quotations.length + 1; 74 | quotations.push(quote); 75 | return new Response(JSON.stringify(quote), { status: 201 }); 76 | }); 77 | }); 78 | 79 | // Start the service worker. 80 | worker.init(); 81 | --------------------------------------------------------------------------------