Skip to content

Commit

Permalink
Basic service worker sample revamp (#402)
Browse files Browse the repository at this point in the history
* Basic service worker sample revamp.

* Review feedback.
  • Loading branch information
jeffposnick authored Aug 3, 2016
1 parent b81ab9d commit dbca5f7
Show file tree
Hide file tree
Showing 12 changed files with 199 additions and 67 deletions.
7 changes: 6 additions & 1 deletion service-worker/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Service Worker Recipes

- [Basic registration](https://googlechrome.github.io/samples/service-worker/registration/index.html) -
- [Basic Demo](https://googlechrome.github.io/samples/service-worker/basic/index.html) -
a sample covering a basic, common use case. It precaches a set of local resources in a
versioned cache, and maintains another cache that's populated at runtime as additional
resources are requested.

- [Simple registration](https://googlechrome.github.io/samples/service-worker/registration/index.html) -
a bare-bones sample that simply performs service worker registration, with placeholders for various event handlers.

- [Detailed registration](https://googlechrome.github.io/samples/service-worker/registration-events/index.html) -
Expand Down
5 changes: 5 additions & 0 deletions service-worker/basic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Basic Service Worker Sample
===========================
See https://googlechrome.github.io/samples/service-worker/basic/index.html for a live demo.

Learn more at https://www.chromestatus.com/feature/6561526227927040
11 changes: 11 additions & 0 deletions service-worker/basic/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js');
}

document.querySelector('#show').addEventListener('click', () => {
const iconUrl = document.querySelector('select').selectedOptions[0].value;
let imgElement = document.createElement('img');
imgElement.src = iconUrl;
document.querySelector('#container').appendChild(imgElement);
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added service-worker/basic/icons/ic_folder_black_48dp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
139 changes: 95 additions & 44 deletions service-worker/basic/index.html
Original file line number Diff line number Diff line change
@@ -1,49 +1,100 @@
<!DOCTYPE html>
<!--
Copyright 2014 Google Inc. All Rights Reserved.
---
feature_name: Basic Service Worker
chrome_version: 40
feature_id: 6561526227927040
local_css_files: ['styles.css']
---

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
<h3>Background</h3>
<p>
This sample demonstrates a basic service worker that could be used as-is, or
as a starting point for further customization.
</p>

http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script>
var scope = 'hello/';
var registration;

function register() {
navigator.serviceWorker.register('service-worker.js', {scope: scope})
.then(function(r) {
console.log('registered: ');
registration = r;
console.log(registration);
})
.catch(function(whut) {
console.error('uh oh... ');
console.error(whut);
});
}

function unregister() {
registration.unregister()
.then(function() {
console.log('unregistered');
});
}
</script>
<button onclick="register()">Register</button>
<button onclick="unregister()">Unregister</button>

<p>Some nice places to visit:
<h4>What It Does</h4>
<ul>
<li>
Precaches the HTML, JavaScript, and CSS files needed to display this page offline.
(Try it out by reloading the page without a network connection!)
</li>
<li>Cleans up the previously precached entries when the cache name is updated.</li>
<li>Intercepts network requests, returning a cached response when available.</li>
<li>
If there's no cached response, fetches the response from the network and
adds it to the cache for future use.
</li>
</ul>
<p>
You can confirm the service worker's behavior using the
<a href="https://developers.google.com/web/tools/chrome-devtools/debug/progressive-web-apps/">Application panel</a>
of Chrome's DevTools.
</p>

<h4>What It Doesn't Do</h4>
<ul>
<li><a href="hello/world">world</a>
<li><a href="hello/Cleveland">Cleveland</a>
<li>
Automatically version any of the precached resources.<br>
<em>
You must manually update the <code>CACHES.PRECACHE</code> name to pick up
new versions after updating anything!
</em>
</li>
<li>
Cache-bust the precaching requests.<br>
<em>
The <code>cache.addAll()</code> call may be fulfilled with responses from
the HTTP cache, depending on the HTTP caching headers you use. If you
are using
<a href="https://jakearchibald.com/2016/caching-best-practices/">HTTP caching</a>
and unversioned resources, it can be safer to
<a href="https://github.com/GoogleChrome/samples/blob/5c20f8d74d890fad3d867747d2c3fc853727700c/service-worker/prefetch/service-worker.js#L56">cache-bust</a>
your precaching requests.
</em>
</li>
<li>
Refresh the entries in the runtime cache.<br>
<em>
Once an entry is added to the runtime cache, it's used indefinitely,
without consulting the network to check for updates. If your runtime cache
is used for resources that might be updated, a different strategy, like
<a href="https://jakearchibald.com/2014/offline-cookbook/#stale-while-revalidate">stale-while-revalidate</a>
could be more appropriate.
</em>
</li>
<li>
Clean out the runtime cache.<br>
<em>
The runtime cache will grow as new resource URLs are requested. In this
example, there are only 5 different images that might be loaded, so the
cache size isn't a concern. If your web app might request an arbitrary
number of unique resource URLs, then using a library like
<a href="https://github.com/GoogleChrome/sw-toolbox"><code>sw-toolbox</code></a>
which provides
<a href="https://github.com/GoogleChrome/sw-toolbox/tree/master/recipes/cache-expiration-options">cache-expiration</a>
is recommended.
</em>
</li>
</ul>

<h3>Live Demo</h3>
<p>
The following demo illustrates the service worker's runtime caching by loading
images in response to clicking the button below.
</p>
<p>
The first time a given image is requested, the service worker will be load it
from the network, but each subsequent time, it will be retrieved from the cache.
</p>
<label for="icons">Icons:</label>
<select id="icons">
<option value="icons/ic_create_new_folder_black_48dp.png">New Folder</option>
<option value="icons/ic_file_upload_black_48dp.png">File Upload</option>
<option value="icons/ic_folder_black_48dp.png">Closed Folder</option>
<option value="icons/ic_folder_open_black_48dp.png">Open Folder</option>
<option value="icons/ic_folder_shared_black_48dp.png">Shared Folder</option>
</select>
<button id="show">Show Icon</button>
<div id="container"></div>

{% include js_snippet.html filename='demo.js' %}
{% include js_snippet.html filename='service-worker.js' displayonly=true title="Service Worker's JavaScript" %}
14 changes: 0 additions & 14 deletions service-worker/basic/readme.md

This file was deleted.

80 changes: 72 additions & 8 deletions service-worker/basic/service-worker.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,76 @@
self.addEventListener('fetch', function(event) {
console.log('got a request');
/*
Copyright 2016 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

var salutation = 'Hello, ';
var whom = decodeURIComponent(event.request.url.match(/\/([^/]*)$/)[1]);
var energyLevel = (whom === 'Cleveland') ? '!!!' : '!';
var version = '\n\n(Version 1)';
// Names of the two caches used in this version of the service worker.
// Change to v2, etc. when you update any of the local resources, which will
// in turn trigger the install event again.
const PRECACHE = 'precache-v1';
const RUNTIME = 'runtime';

var body = new Blob([salutation, whom, energyLevel, version]);
// A list of local resources we always want to be cached.
const PRECACHE_URLS = [
'index.html',
'./', // Alias for index.html
'styles.css',
'../../styles/main.css',
'demo.js'
];

event.respondWith(new Response(body));
// The install handler takes care of precaching the resources we always need.
self.addEventListener('install', event => {
event.waitUntil(
caches.open(PRECACHE)
.then(cache => cache.addAll(PRECACHE_URLS))
.then(self.skipWaiting())

This comment has been minimized.

Copy link
@paulirish

paulirish Aug 11, 2016

Member

Of all my problems with service workers, I have the most problems with getting a new SW to finish the installing phase and move on to activation. And this line is very curious.

Over in sw-precache you then() to a function that returns skipWaiting(): sw-precache/service-worker.tmpl And we see that in mozilla's examples too.

Whereas here, the skipWaiting is evaluated "immediately", which I suppose races with the cache opening, right? I can't tell if that's intentional. But I do see the same thing in tests in chromium.

Then flipkart actually uses TWO calls on e.waitUntil which the spec is fairly unclear about:
image

And lastly I see this pattern from @jakearchibald from the other day, which immediately calls skipWaiting (not within e.waitUntil), but afterwards populates the cache

So I'm hoping to figure out:

  1. can skipWaiting race against cache population during the install handler?
  2. should it? is there any difference between that and only skipWaiting'ing after population is done?
  3. less important, but.. is multiple calls on e.waitUntil legit?

This comment has been minimized.

Copy link
@jakearchibald

jakearchibald Aug 11, 2016

There are three slots for service workers inside a registration:

  • installing
  • waiting
  • active

Usually, a "waiting" worker cannot progress to "active" until the current "active" worker is controlling zero clients.

Calling skipWaiting() sets a flag on the service worker telling it that doesn't need to wait until the current "active" worker is controlling zero clients before it progresses to "active". It has no effect over when the worker can progress from "installing" to "waiting".

This means skipWaiting() is safe to call before or during install, it won't make install finish earlier, and it won't turn a failing install into a successful install. In fact, you could safely put it at the top of your SW. Behaviour wouldn't change, you're just calling skipWaiting() more than you need to. Passing the result of skipWaiting() to waitUntil(), or calling skipWaiting() after install-caching is redundant, but doesn't break anything.

To answer your questions:

can skipWaiting race against cache population during the install handler?

Nope

should it? is there any difference between that and only skipWaiting'ing after population is done?

Nope & nope

less important, but.. is multiple calls on e.waitUntil legit?

Yep! The wait time will be the time it takes all passed promises to settle.

This comment has been minimized.

Copy link
@paulirish

paulirish Aug 11, 2016

Member

hero.

Thanks man, that is supremely useful.

This comment has been minimized.

Copy link
@jakearchibald

jakearchibald Aug 11, 2016

It's about time I wrote an in-depth thing on the service worker lifecycle. Started it yesterday. Will be finished sometime before ∞ o'clock

This comment has been minimized.

Copy link
@samccone

samccone Aug 11, 2016

Perhaps a silly question, but in what case would you want to not skipWaiting?

related thread https://twitter.com/sleeplessgeek/status/763402440070737921 of a new service worker not taking over and causing issues


having a line like this in your source to go ahead and "reload" the page paired with skipWaiting seems like the default behavior most people want to just work.

https://github.com/samccone/moji-brush/blob/master/src/app.js#L15-L19

This comment has been minimized.

Copy link
@jakearchibald

jakearchibald Aug 11, 2016

This can land you with multiple tabs running different versions of your code, which can result in data loss when they have different opinions of how data should be stored/processed.

Also, by using skipWaiting, your new SW is now controlling fetches from pages that were loaded with some older version. That might cause breakages of you're not defending against it.

I show a prompt when there's a new version waiting, but I don't skip waiting until the user clicks "reload". Then I use the controller change to initiate the reload across all open tabs, ensuring that any half-input data is safely stored first.

This comment has been minimized.

Copy link
@jeffposnick

jeffposnick Aug 11, 2016

Author Contributor

sw-precache currently opts you in to both skipWaiting() and clients.claim(), and while I haven't heard of anyone getting burned, I feel guilty about that: GoogleChromeLabs/sw-precache#122

I don't think that most developers are in a position to understand the subtleties of the 4 permutations of enabling/disabling skipWaiting() + clients.claim(). (I can't [clients.]claim to understand them all.) So having @jakearchibald write up the subtleties and then linking to that should help put developers in a position to make a meaningful decision.

This comment has been minimized.

Copy link
@ithinkihaveacat

ithinkihaveacat Aug 12, 2016

Contributor

Passing the result of skipWaiting() to waitUntil() [...] is redundant, but doesn't break anything.

@jakearchibald If it's redundant in this context, under what circumstances would you use the return value of skipWaiting() (Promise<undefined>)? (Why does it exist?)

This comment has been minimized.

Copy link
@jakearchibald

jakearchibald Aug 12, 2016

It resolves when the flag has successfully set. It's not all that interesting. It may be more interesting when it's exposed from window objects.

This comment has been minimized.

Copy link
@MarkTiedemann

MarkTiedemann Dec 6, 2016

Hey @jakearchibald. :)

I'm kinda new to SWs, so I might very well be missing something here. But I find that I do have to wait for .skipWaiting() if I don't want Chrome to choke:

This happens if I use the following code (which is similar to the code in the prefetch sample).

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(PREFETCH_CACHE)
      .then(cache => cache.addAll(PREFETCH_RESOURCES))
      .then(self.skipWaiting())
  )
})

It happens if I go into offline mode after the initial page load and try to refresh the page.

If I change the .then(self.skipWaiting()) to .then(() => self.skipWaiting()), no error occurs.

Any idea what's happening there? I'm kinda lost. ;)

This comment has been minimized.

Copy link
@ithinkihaveacat

ithinkihaveacat Apr 18, 2017

Contributor

@MarkTiedemann I can't see the image and I'm not sure what's tripping Chrome up, but you almost certainly don't want to do promise.then(self.skipWaiting()): .then() is expecting its argument to be a function whereas self.skipWaiting() is a Promise.

);
});

// The activate handler takes care of cleaning up old caches.
self.addEventListener('activate', event => {
const currentCaches = [PRECACHE, RUNTIME];
event.waitUntil(
caches.keys().then(cacheNames => {
return cacheNames.filter(cacheName => !currentCaches.includes(cacheName));
}).then(cachesToDelete => {
return Promise.all(cachesToDelete.map(cacheToDelete => {
return caches.delete(cacheToDelete);
}));
}).then(() => self.clients.claim())
);
});

// The fetch handler serves responses for same-origin resources from a cache.
// If no response is found, it populates the runtime cache with the response
// from the network before returning it to the page.
self.addEventListener('fetch', event => {
// Skip cross-origin requests, like those for Google Analytics.
if (event.request.url.startsWith(self.location.origin)) {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}

return caches.open(RUNTIME).then(cache => {
return fetch(event.request).then(response => {
// Put a copy of the response in the runtime cache.
return cache.put(event.request, response.clone()).then(() => {
return response;
});
});
});
})
);
}
});

10 changes: 10 additions & 0 deletions service-worker/basic/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-around;
}

li {
margin-bottom: 1em;
}

0 comments on commit dbca5f7

Please sign in to comment.