|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505 |
- title: Introduction to Service Worker
- url: http://www.html5rocks.com/en/tutorials/service-worker/introduction/
- hash_url: 64fd51915364724e3489270d05a68907
-
- <p>Rich offline experiences, periodic background syncs, push notifications—
- functionality that would normally require a native application—are coming to
- the web. Service workers provide the technical foundation that all these
- features will rely on.</p>
-
- <h2 id="toc-what">What is a Service Worker?</h2>
-
- <p>A service worker is a script that is run by your browser in the background,
- separate from a web page, opening the door to features which don't need a web
- page or user interaction. Today, they already include features like
- <a href="http://updates.html5rocks.com/2015/03/push-notificatons-on-the-open-web">
- push notifications</a> and in the future it will include other things like,
- background sync, or geofencing. The core feature discussed in this tutorial is
- the ability to intercept and handle network requests, including programmatically
- managing a cache of responses.</p>
-
- <p>The reason this is such an exciting API is that it allows you to support offline
- experiences, giving developers complete control over what exactly that
- experience is.</p>
-
- <p>Before service worker there was one other API that would give users an offline
- experience on the web called <a href="/tutorials/appcache/beginner/">App
- Cache</a>. The major
- issue with App Cache is the <a href="http://alistapart.com/article/application-cache-is-a-douchebag">number
- of gotcha's that exist</a> as well
- as the design working particularly well for single page web apps, but not for
- multi-page sites. Service workers have been designed to avoid these common pain
- points.</p>
-
- <p>Things to note about a service worker:</p>
-
- <ul>
- <li>It's a <a href="/tutorials/workers/basics/">JavaScript
- Worker</a>, so it can't
- access the DOM directly. Instead, a service worker can communicate with the
- pages it controls by responding to messages sent via the
- <a href="https://html.spec.whatwg.org/multipage/workers.html#dom-worker-postmessage"><code>postMessage</code></a>
- interface, and those pages can manipulate the DOM if needed.</li>
- <li>Service worker is a programmable network proxy, allowing you to control
- how network requests from your page are handled.</li>
- <li>It will be terminated when not in use, and restarted when it's next needed, so
- you cannot rely on global state within a service worker's <code>onfetch</code> and
- <code>onmessage</code> handlers. If there is information that you need to persist and
- reuse across restarts, service workers do have access to the
- <a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API">IndexedDB</a>
- API.</li>
- <li>Service workers make extensive use of promises, so if you're new to promises,
- then you should stop reading this and check out <a href="/tutorials/es6/promises/">Jake Archibald's
- article</a>.</li>
- </ul>
-
- <h2 id="lifecycle">Service Worker Lifecycle</h2>
-
- <p>A service worker has a lifecycle which is completely separate from your web
- page.</p>
-
- <p>To install a service worker for your site, you need to <strong>register</strong> it, which
- you do in your page's JavaScript. Registering a service worker will cause the
- browser to start the service worker install step in the background.</p>
-
- <p>Typically during the install step, you'll want to cache some static assets. If
- all the files are cached successfully, then the service worker becomes
- installed. If any of the files fail to download and cache, then the install step
- will fail and the service worker won't activate (i.e. won't be installed). If
- that happens, don't worry, it'll try again next time. But that means if it
- <em>does</em> install, you <strong>know</strong> you've got those static assets in the cache.</p>
-
- <p>When we're installed, the activation step will follow and this is a great
- opportunity for handling any management of old caches, which we'll cover during
- the <a href="#toc-how">service worker update section</a>.</p>
-
- <p>After the activation step, the service worker will control all pages that fall
- under its scope, though the page that registered the service worker for the
- first time won't be controlled until it's loaded again. Once a service worker is
- in control, it will be in one of two states: either the service worker will be
- terminated to save memory, or it will handle <code>fetch</code> and <code>message</code> events which
- occur when a network request or message is made from your page.</p>
-
- <p>Below is an overly simplified version of the service worker lifecycle on it's
- first installation.</p>
-
- <img src="http://www.html5rocks.com/en/tutorials/service-worker/introduction/images/sw-lifecycle.png"/>
-
- <h2 id="toc-before">Before We Start</h2>
-
- <p>Grab the caches polyfill from this repository
- <a href="https://github.com/coonsta/cache-polyfill">https://github.com/coonsta/cache-polyfill</a>.</p>
-
- <p>This polyfill will add support for <code>Cache.addAll</code> which Chrome M43's implementation of
- the <a href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#cache-interface">Cache
- API</a> doesn't currently support.</p>
-
- <p>Grab <strong><code>dist/serviceworker-cache-polyfill.js</code></strong> to put somewhere in your site
- and use it in a service worker with the <strong><code>importScripts</code></strong> method. Any script
- which is imported will automatically be cached by the service worker.</p>
-
- <pre class="prettyprint"><code>importScripts('serviceworker-cache-polyfill.js');</code></pre>
-
- <h3>HTTPS is Needed</h3>
-
- <p>During development you'll be able to use service worker through <code>localhost</code>, but
- to deploy it on a site you'll need to have HTTPS setup on your server.</p>
-
- <p>Using service worker you can hijack connections, fabricate, and filter
- responses. Powerful stuff. While you would use these powers for good, a
- man-in-the-middle might not. To avoid this, you can only register for service
- workers on pages served over HTTPS, so we know the service worker the browser
- receives hasn't been tampered with during its journey through the network.</p>
-
- <p><a href="https://pages.github.com/">Github
- Pages</a> are served over HTTPS, so they're a great place to host demos.</p>
-
- <p>If you want to add HTTPS to your server then you'll need to get a TLS
- certificate and set it up for your server. This varies depending on your setup,
- so check your server's documentation and be sure to check out <a href="https://mozilla.github.io/server-side-tls/ssl-config-generator/">Mozilla's SSL config generator</a> for best practices.</p>
-
- <h3>Using Service Worker</h3>
-
- <p>Now that we've got the Polyfill and covered HTTPS, let's look at what we do in
- each step.</p>
-
- <h3>How to Register and Install a Service Worker</h3>
-
- <p>To install a service worker you need to kick start the process by <strong>registering</strong>
- a service worker in your page. This tells the browser where your service
- worker JavaScript file lives.</p>
-
- <pre class="prettyprint"><code>if ('serviceWorker' in navigator) {
- navigator.serviceWorker.register('/sw.js').then(function(registration) {
- // Registration was successful
- console.log('ServiceWorker registration successful with scope: ', registration.scope);
- }).catch(function(err) {
- // registration failed :(
- console.log('ServiceWorker registration failed: ', err);
- });
- }</code></pre>
-
- <p>This code checks to see if the service worker API is available, and if it is,
- the service worker at <code>/sw.js</code> is registered.</p>
-
- <p>You can call register every time a page loads without concern; the browser will
- figure out if the service worker is already registered or not and handle it
- accordingly.</p>
-
- <p>One subtlety with the register method is the location of the service worker
- file. You'll notice in this case that the service worker file is at the root of
- the domain. This means that the service worker's scope will be the entire
- origin. In other words, this service worker will receive <code>fetch</code> events for
- everything on this domain. If we register the service worker file at
- <code>/example/sw.js</code>, then the service worker would only see <code>fetch</code> events for
- pages whose URL starts with <code>/example/</code> (i.e. <code>/example/page1/</code>,
- <code>/example/page2/</code>).</p>
-
- <p>Now you can check that a service worker is enabled by going to
- <code>chrome://inspect/#service-workers</code> and looking for your site.</p>
-
- <img src="http://www.html5rocks.com/en/tutorials/service-worker/introduction/images/sw-chrome-inspect.png"/>
-
- <p>When service worker was first being implemented you could also view your service
- worker details through <code>chrome://serviceworker-internals</code>. This may still be
- useful, if for nothing more than learning about the life cycle of service
- workers, but don't be surprised if it gets replaced by
- <code>chrome://inspect/#service-workers</code> at a later date.</p>
-
- <p>You may find it useful to test your service worker in an Incognito window so
- that you can close and reopen knowing that the previous service worker won't
- affect the new window. Any registrations and caches created from within an
- Incognito window will be cleared out once that window is closed.</p>
-
- <h3>Service Worker Install Step</h3>
-
- <p>After a controlled page kicks off the registration process, let's shift to the
- point of view of the service worker script, which is given the opportunity to
- handle the <code>install</code> event.</p>
-
- <p>For the most basic example, you need to define a callback for the <code>install</code> event
- and decide which files you want to cache.</p>
-
- <pre class="prettyprint"><code>// The files we want to cache
- var urlsToCache = [
- '/',
- '/styles/main.css',
- '/script/main.js'
- ];
-
- // Set the callback for the install step
- self.addEventListener('install', function(event) {
- // Perform install steps
- });</code></pre>
-
- <p>Inside of our install callback, we need to take the following steps:</p>
-
- <ol>
- <li>Open a cache</li>
- <li>Cache our files</li>
- <li>Confirm whether all the required assets are cached or not</li>
- </ol>
-
- <pre class="prettyprint"><code>var CACHE_NAME = 'my-site-cache-v1';
- var urlsToCache = [
- '/',
- '/styles/main.css',
- '/script/main.js'
- ];
-
- self.addEventListener('install', function(event) {
- // Perform install steps
- event.waitUntil(
- caches.open(CACHE_NAME)
- .then(function(cache) {
- console.log('Opened cache');
- return cache.addAll(urlsToCache);
- })
- );
- });</code></pre>
-
- <p>Here you can see we call <code>caches.open</code> with our desired cache name, after this we
- call <code>cache.addAll</code> and pass in our array of files. This is a chain of promises
- (<code>caches.open</code> and <code>cache.addAll</code>). <code>event.waitUntil</code> takes a promise and uses it
- to know how long installation takes, and whether it succeeded.</p>
-
- <p>If all the files are successfully cached, then the service worker
- will be installed. If <strong>any</strong> of the files fail to download, then the install
- step will fail. This allows you to rely on having all the assets that you
- defined, but does mean you need to be careful with the list of files you decide
- to cache in the install step. Defining a long list of files will increase the
- chance that one file may fail to cache, leading to your service worker not
- getting installed.</p>
-
- <p>This is just one example, you can perform other tasks in the <code>install</code> event or
- avoid setting an <code>install</code> event listener altogether.</p>
-
- <h3>How to Cache and Return Requests</h3>
-
- <p>Now that you've installed a service worker, you probably want to return one of
- your cached responses right?</p>
-
- <p>After a service worker is installed and the user navigates to a different page
- or refreshes, the service worker will begin to receive <code>fetch</code> events, an example
- of which is below.</p>
-
- <pre class="prettyprint"><code>self.addEventListener('fetch', function(event) {
- event.respondWith(
- caches.match(event.request)
- .then(function(response) {
- // Cache hit - return response
- if (response) {
- return response;
- }
-
- return fetch(event.request);
- }
- )
- );
- });</code></pre>
-
- <p>Here we've defined our <code>fetch</code> event and within the <code>event.respondWith</code>, we pass
- in a promise from <code>caches.match</code>. <code>caches.match</code> will look at the request
- and find any cached results from any of the caches your service worker created.</p>
-
- <p>If we have a matching response, we return the cached value, otherwise we return
- the result of a call to <code>fetch</code>, which will make a network request and return the
- data if anything can be retrieved from the network. This is a simple example and
- uses any cached assets we cached during the install step.</p>
-
- <p>If we wanted to cache new requests cumulatively, we can do so by handling the
- response of the fetch request and then adding it to the cache, like below.</p>
-
- <pre class="prettyprint"><code>self.addEventListener('fetch', function(event) {
- event.respondWith(
- caches.match(event.request)
- .then(function(response) {
- // Cache hit - return response
- if (response) {
- return response;
- }
-
- // IMPORTANT: Clone the request. A request is a stream and
- // can only be consumed once. Since we are consuming this
- // once by cache and once by the browser for fetch, we need
- // to clone the response
- var fetchRequest = event.request.clone();
-
- return fetch(fetchRequest).then(
- function(response) {
- // Check if we received a valid response
- if(!response || response.status !== 200 || response.type !== 'basic') {
- return response;
- }
-
- // IMPORTANT: Clone the response. A response is a stream
- // and because we want the browser to consume the response
- // as well as the cache consuming the response, we need
- // to clone it so we have 2 stream.
- var responseToCache = response.clone();
-
- caches.open(CACHE_NAME)
- .then(function(cache) {
- cache.put(event.request, responseToCache);
- });
-
- return response;
- }
- );
- })
- );
- });</code></pre>
-
- <p>What we are doing is this:</p>
-
- <ol>
- <li>Add a callback to <code>.then</code> on the fetch request</li>
- <li>Once we get a response, we perform the following checks:</li>
- <ol>
- <li>Ensure the response is valid.</li>
- <li>Check the status is <code>200</code> on the response.</li>
- <li>Make sure the response type is <strong>basic</strong>, which indicates that it's a
- request from our origin. This means that requests to third party assets
- aren't cached as well.</li>
- </ol>
- <li>If we pass the checks, we
- <a href="https://fetch.spec.whatwg.org/#dom-response-clone"><code>clone</code></a>
- the response. The reason for this is that because the response is a
- <a href="https://streams.spec.whatwg.org/">Stream</a>,
- the body can only be consumed once. Since we want to return the
- response for the browser to use, as well as pass it to the cache to use, we
- need to clone it so we can send one to the browser and one to the cache.</li>
- </ol>
-
- <h2 id="toc-how">How to Update a Service Worker</h2>
-
- <p>There will be a point in time where your service worker will need updating. When that time comes, you'll need to follow these steps:</p>
-
- <ol>
- <li>Update your service worker JavaScript file.</li>
- <ol>
- <li>When the user navigates to your site, the browser tries to redownload the
- script file that defined the service worker in the background. If there
- is even a byte's difference in the service worker file compared to what
- it currently has, it considers it 'new'.</li>
- </ol>
- <li>Your new service worker will be started and the <code>install</code> event will be fired.</li>
- <li>At this point the old service worker is still controlling the current pages
- so the new service worker will enter a "waiting" state.</li>
- <li>When the currently open pages of your site are closed, the old service worker
- will be killed and the new service worker will take control.</li>
- <li>Once your new service worker takes control, its <code>activate</code> event will be fired.</li>
- </ol>
-
- <p>One common task that will occur in the activate callback is cache management.
- The reason you'll want to do this in the activate callback is because if you
- were to wipe out any old caches in the install step, any old service worker,
- which keeps control of all the current pages, will suddenly stop being able to
- serve files from that cache.</p>
-
- <p>Let's say we have one cache called 'my-site-cache-v1', and we find that we want
- to split this out into one cache for pages and one cache for blog posts. This
- means in the install step we'd create two caches, 'pages-cache-v1' and
- 'blog-posts-cache-v1' and in the activate step we'd want to delete our older
- 'my-site-cache-v1'.</p>
-
- <p>The following code would do this by looping through all of the caches in the
- service worker and deleting any caches which aren't defined in the cache
- whitelist.</p>
-
- <pre class="prettyprint"><code>self.addEventListener('activate', function(event) {
-
- var cacheWhitelist = ['pages-cache-v1', 'blog-posts-cache-v1'];
-
- event.waitUntil(
- caches.keys().then(function(cacheNames) {
- return Promise.all(
- cacheNames.map(function(cacheName) {
- if (cacheWhitelist.indexOf(cacheName) === -1) {
- return caches.delete(cacheName);
- }
- })
- );
- })
- );
- });</code></pre>
-
- <h2 id="toc-rough">Rough Edges & Gotchas</h2>
-
- <p>This stuff is really new. Here's a collection of issues that get in the way.
- Hopefully this section can be deleted soon, but for now these are worth being
- mindful of.</p>
-
- <h3>If Installation Fails, We're Not so Good at Telling You About It</h3>
-
- <p>If a worker registers, but then doesn't appear in
- <code>chrome://inspect/#service-workers</code> or <code>chrome://serviceworker-internals</code>, it's
- likely its failed to install due to an error being thrown, or a rejected promise
- being passed to <code>event.waitUntil</code>.</p>
-
- <p>To work around this, go to <code>chrome://serviceworker-internals</code> and check "Opens the
- DevTools window for service worker on start for debugging", and put a debugger;
- statement at the start of your <code>install</code> event. This, along with "<a href="https://developer.chrome.com/devtools/docs/javascript-debugging#pause-on-uncaught-exceptions">
- Pause on uncaught exceptions</a>", should reveal the issue.</p>
-
- <h3>The Defaults of fetch()</h3>
- <h4>No Credentials by Default</h4>
-
- <p>When you use <code>fetch</code>, by default, requests won't contain credentials such as
- cookies. If you want credentials, instead call:</p>
-
- <pre class="prettyprint"><code>fetch(url, {
- credentials: 'include'
- })</code></pre>
-
- <p>This behaviour is on purpose, and is arguably better than XHR's more complex
- default of sending credentials if the URL is same-origin, but omiting them
- otherwise. Fetch's behaviour is more like other CORS requests, such as <code><img crossorigin></code>, which never sends cookies unless you opt-in with <code><img crossorigin="use-credentials"></code>.</p>
-
- <h4>Non-CORS Fail by Default</h4>
-
- <p>By default, fetching a resource from a third party URL will fail if it doesn't
- support CORS. You can add a non-CORS option to the <code>Request</code> to overcome this,
- although this will cause an <a href="https://fetch.spec.whatwg.org/#concept-filtered-response-opaque">'opaque'
- response</a>,
- which means you won't be able to tell if the response was successful or not.</p>
-
- <pre class="prettyprint"><code>cache.addAll(urlsToPrefetch.map(function(urlToPrefetch) {
- return new Request(urlToPrefetch, { mode: 'no-cors' });
- })).then(function() {
- console.log('All resources have been fetched and cached.');
- });</code></pre>
-
- <h3>Handling Responsive Images</h3>
-
- <p>The <code>srcset</code> attribute or the <code><picture></code> element
- will select the most appropriate image asset at run time and make a network request.</p>
-
- <p>For service worker, if you wanted to cache an image during the install step, you
- have a few options:</p>
-
- <ol>
- <li>Install all the images <code><picture></code> element and <code>srcset</code> attribute will request</li>
- <li>Install a single low-res version of the image</li>
- <li>Install a single high-res version of the image</li>
- </ol>
-
- <p>Realistically you should be picking option 2 or 3 since downloading all of the
- images would be a waste of memory.</p>
-
- <p>Let's assume you go for the low res version at install time and you want to try
- and retrieve higher res images from the network when the page is loaded, but if
- the high res images fail, fallback to the low res version. This is fine and
- dandy to do but there is one problem.</p>
-
- <p>If we have the following two images:</p>
-
- <table>
- <tr>
- <td>Screen Density</td>
- <td>Width</td>
- <td>Height</td>
- </tr>
- <tr>
- <td>1x</td>
- <td>400</td>
- <td>400</td>
- </tr>
- <tr>
- <td>2x</td>
- <td>800</td>
- <td>800</td>
- </tr>
- </table>
-
- <p>In a srcset image, we'd have some markup like this:</p>
-
- <pre class="prettyprint"><code><img src="image-src.png" srcset="image-src.png 1x, image-2x.png 2x" /></code></pre>
-
- <p>If we are on a 2x display, then the browser will opt to download <code>image-2x.png</code>,
- if we are offline you could <code>.catch</code> this request and return <code>image-src.png</code>
- instead if it's cached, however the browser will expect an image which
- takes into account the extra pixels on a 2x screen, so the image will appear as
- 200x200 CSS pixels instead of 400x400 CSS pixels. The only way around this is to
- set a fixed height and width on the image.</p>
-
- <pre class="prettyprint"><code><img src="image-src.png" srcset="image-src.png 1x, image-2x.png 2x"
- style="width:400px; height: 400px;" /></code></pre>
-
- <img src="http://www.html5rocks.com/en/tutorials/service-worker/introduction/images/sw-responsive-img-example.png"/>
-
- <p>For <code><picture></code> elements being used for art direction, this becomes considerably
- more difficult and will depend heavily on how your images are created and used,
- but you may be able to use a similar approach to srcset.</p>
-
- <h2 id="toc-learn">Learn More</h2>
-
- <p>There is a list of documentation on service worker being maintained at
- <a href="https://jakearchibald.github.io/isserviceworkerready/resources.html">https://jakearchibald.github.io/isserviceworkerready/resources.html</a>
- that you may find useful.</p>
-
- <h2 id="toc-help">Get Help</h2>
-
- <p>If you get stuck then please post your questions on Stackoverflow and use the
- <a href="http://stackoverflow.com/questions/tagged/service-worker">'service-worker'</a>
- tag so that we can keep a track of issues and try and help as much as possible.</p>
|