A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.md 14KB

4 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. title: Offline Recipes for Service Workers
  2. url: https://hacks.mozilla.org/2015/11/offline-service-workers/
  3. hash_url: 855debdfdc494d53ccf8e1939ea85362
  4. <p>“Offline” is a big topic these days, especially as many web apps look to also function as mobile apps.  The original offline helper API, the Application Cache API (also known as “appcache”), has a host of problems, many of which can be found in Jake Archibald’s <a href="http://alistapart.com/article/application-cache-is-a-douchebag" target="_blank">Application Cache is a Douchebag</a> post.  Problems with appcache include:</p>
  5. <ul>
  6. <li>Files are served from cache even when the user is online.</li>
  7. <li>There’s no dynamism: the appcache file is simply a list of files to cache.</li>
  8. <li>One is able to <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache#Gotchas" target="_blank">cache the .appcache file itself</a> and that leads to update problems.</li>
  9. <li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache#Gotchas" target="_blank">Other gotchas</a>.</li>
  10. </ul>
  11. <p>Today there’s a new API available to developers to ensure their web apps work properly:  <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API" target="_blank">the Service Worker API</a>.  The Service Worker API allows developers to manage what does and doesn’t go into cache for offline use with JavaScript.</p>
  12. <h2>Introducing the Service Worker Cookbook</h2>
  13. <p>To introduce you to the Service Worker API we’ll be using examples from Mozilla’s new  <a href="https://serviceworke.rs" target="_blank">Service Worker Cookbook</a>!  The Cookbook is a collection of working, practical examples of service workers in use in modern web apps.  We’ll be introducing service workers within this three-part series:</p>
  14. <ul>
  15. <li>Offline Recipes for Service Workers (today’s post)</li>
  16. <li>At Your Service for More Than Just appcache</li>
  17. <li>Web Push Updates to the Masses</li>
  18. </ul>
  19. <p>Of course this API has advantages other than enabling offline capabilities, such as performance for one, but I’d like to start by introducing basic service worker strategies for offline.</p>
  20. <h2>What do we mean by <em>offline</em>?</h2>
  21. <p>Offline doesn’t just mean the user doesn’t have an internet connection — it can also mean that the user is on a flaky network connection.  Essentially “offline” means that the user doesn’t have a reliable connection, and we’ve all been there before!</p>
  22. <p>The Offline Status recipe illustrates how to use a service worker to cache a known asset list and then notify the user that they may now go offline and use the app. The app itself is quite simple: show a random image when a button is clicked.  Let’s have a look at the components involved in making this happen.</p>
  23. <h3>The Service Worker</h3>
  24. <p>We’ll start by looking at the <code>service-worker.js</code> file to see what we’re caching. We’ll be caching the random images to display, as well as the display page and critical JavaScript resources, in a cache named <code>dependencies-cache</code>:</p>
  25. <pre><code class="language-javascript">&#13;
  26. var CACHE_NAME = 'dependencies-cache';&#13;
  27. &#13;
  28. // Files required to make this app work offline&#13;
  29. var REQUIRED_FILES = [&#13;
  30. 'random-1.png',&#13;
  31. 'random-2.png',&#13;
  32. 'random-3.png',&#13;
  33. 'random-4.png',&#13;
  34. 'random-5.png',&#13;
  35. 'random-6.png',&#13;
  36. 'style.css',&#13;
  37. 'index.html',&#13;
  38. '/', // Separate URL than index.html!&#13;
  39. 'index.js',&#13;
  40. 'app.js'&#13;
  41. ];&#13;
  42. </code></pre>
  43. <p>The service worker’s <code>install</code> event will open the cache and use <code>addAll</code> to direct the service worker to cache our specified files:</p>
  44. <pre><code class="language-javascript">&#13;
  45. self.addEventListener('install', function(event) {&#13;
  46. // Perform install step: loading each required file into cache&#13;
  47. event.waitUntil(&#13;
  48. caches.open(CACHE_NAME)&#13;
  49. .then(function(cache) {&#13;
  50. // Add all offline dependencies to the cache&#13;
  51. return cache.addAll(REQUIRED_FILES);&#13;
  52. })&#13;
  53. .then(function() {&#13;
  54. // At this point everything has been cached&#13;
  55. return self.skipWaiting();&#13;
  56. })&#13;
  57. );&#13;
  58. });&#13;
  59. </code></pre>
  60. <p>The <code>fetch</code> event of a service worker is fired for every single request the page makes.  The <code>fetch</code> event also allows you to serve alternate content than was actually requested.  For the purposes of offline content, however, our <code>fetch</code> listener will be very simple:  if the file is cached, return it from cache; if not, retrieve the file from server:</p>
  61. <pre><code class="language-javascript">&#13;
  62. self.addEventListener('fetch', function(event) {&#13;
  63. event.respondWith(&#13;
  64. caches.match(event.request)&#13;
  65. .then(function(response) {&#13;
  66. // Cache hit - return the response from the cached version&#13;
  67. if (response) {&#13;
  68. return response;&#13;
  69. }&#13;
  70. &#13;
  71. // Not in cache - return the result from the live server&#13;
  72. // `fetch` is essentially a "fallback"&#13;
  73. return fetch(event.request);&#13;
  74. }&#13;
  75. )&#13;
  76. );&#13;
  77. });&#13;
  78. </code></pre>
  79. <p>The last part of this <code>service-worker.js</code> file is the <code>activate</code> event listener where we immediately claim the service worker so that the user doesn’t need to refresh the page to activate the service worker. The <code>activate event</code> fires when a previous version of a service worker (if any) has been replaced and the updated service worker takes control of the scope.</p>
  80. <pre><code class="language-javascript">&#13;
  81. self.addEventListener('activate', function(event) {&#13;
  82. // Calling claim() to force a "controllerchange" event on navigator.serviceWorker&#13;
  83. event.waitUntil(self.clients.claim());&#13;
  84. });&#13;
  85. </code></pre>
  86. <p>Essentially we don’t want to require the user to refresh the page for the service worker to begin — we want the service worker to activate upon initial page load.</p>
  87. <h3>Service worker registration</h3>
  88. <p>With the simple service worker created, it’s time to register the service worker:</p>
  89. <pre><code class="language-javascript">&#13;
  90. // Register the ServiceWorker&#13;
  91. navigator.serviceWorker.register('service-worker.js', {&#13;
  92. scope: '.'&#13;
  93. }).then(function(registration) {&#13;
  94. // The service worker has been registered!&#13;
  95. });&#13;
  96. </code></pre>
  97. <p>Remember that the goal of the recipe is to notify the user when required files have been cached.  To do that we’ll need to listen to the service worker’s <code>state</code>. When the <code>state</code> has become <code>activated</code>, we know that essential files have been cached, our app is ready to go offline, and we can notify our user:</p>
  98. <pre><code class="language-javascript">&#13;
  99. // Listen for claiming of our ServiceWorker&#13;
  100. navigator.serviceWorker.addEventListener('controllerchange', function(event) {&#13;
  101. // Listen for changes in the state of our ServiceWorker&#13;
  102. navigator.serviceWorker.controller.addEventListener('statechange', function() {&#13;
  103. // If the ServiceWorker becomes "activated", let the user know they can go offline!&#13;
  104. if (this.state === 'activated') {&#13;
  105. // Show the "You may now use offline" notification&#13;
  106. document.getElementById('offlineNotification').classList.remove('hidden');&#13;
  107. }&#13;
  108. });&#13;
  109. });&#13;
  110. </code></pre>
  111. <p>Testing the registration and verifying that the app works offline simply requires using the recipe! This recipe provides a button to load a random image by changing the image’s <code>src</code> attribute:</p>
  112. <pre><code class="language-javascript">&#13;
  113. // This file is required to make the "app" work offline&#13;
  114. document.querySelector('#randomButton').addEventListener('click', function() {&#13;
  115. var image = document.querySelector('#logoImage');&#13;
  116. var currentIndex = Number(image.src.match('random-([0-9])')[1]);&#13;
  117. var newIndex = getRandomNumber();&#13;
  118. &#13;
  119. // Ensure that we receive a different image than the current&#13;
  120. while (newIndex === currentIndex) {&#13;
  121. newIndex = getRandomNumber();&#13;
  122. }&#13;
  123. &#13;
  124. image.src = 'random-' + newIndex + '.png';&#13;
  125. &#13;
  126. function getRandomNumber() {&#13;
  127. return Math.floor(Math.random() * 6) + 1;&#13;
  128. }&#13;
  129. });&#13;
  130. </code></pre>
  131. <p>Changing the image’s <code>src</code> would trigger a network request for that image, but since we have the image cached by the service worker, there’s no need to make the network request.</p>
  132. <p>This recipe covers probably the most simple of offline cases: caching required static files for offline use.</p>
  133. <p>This recipe follows another simple use case: fetch a page via AJAX but respond with another cached HTML resource (<code>offline.html</code>) if the request fails.</p>
  134. <h3>The service worker</h3>
  135. <p>The <code>install</code> step of the service worker fetches the <code>offline.html</code> file and places it into a cache called <code>offline</code>:</p>
  136. <pre><code class="language-javascript">&#13;
  137. self.addEventListener('install', function(event) {&#13;
  138. // Put `offline.html` page into cache&#13;
  139. var offlineRequest = new Request('offline.html');&#13;
  140. event.waitUntil(&#13;
  141. fetch(offlineRequest).then(function(response) {&#13;
  142. return caches.open('offline').then(function(cache) {&#13;
  143. return cache.put(offlineRequest, response);&#13;
  144. });&#13;
  145. })&#13;
  146. );&#13;
  147. });&#13;
  148. </code></pre>
  149. <p>If that requests fails the service worker won’t register since nothing has been put into cache.</p>
  150. <p>The <code>fetch</code> listener listens for a request for the page and, upon failure, responds with the <code>offline.html</code> file we cached during the event registration:</p>
  151. <pre><code class="language-javascript">&#13;
  152. self.addEventListener('fetch', function(event) {&#13;
  153. // Only fall back for HTML documents.&#13;
  154. var request = event.request;&#13;
  155. // &amp;&amp; request.headers.get('accept').includes('text/html')&#13;
  156. if (request.method === 'GET') {&#13;
  157. // `fetch()` will use the cache when possible, to this examples&#13;
  158. // depends on cache-busting URL parameter to avoid the cache.&#13;
  159. event.respondWith(&#13;
  160. fetch(request).catch(function(error) {&#13;
  161. // `fetch()` throws an exception when the server is unreachable but not&#13;
  162. // for valid HTTP responses, even `4xx` or `5xx` range.&#13;
  163. return caches.open('offline').then(function(cache) {&#13;
  164. return cache.match('offline.html');&#13;
  165. });&#13;
  166. })&#13;
  167. );&#13;
  168. }&#13;
  169. // Any other handlers come here. Without calls to `event.respondWith()` the&#13;
  170. // request will be handled without the ServiceWorker.&#13;
  171. });&#13;
  172. </code></pre>
  173. <p>Notice we use <code>catch</code> to detect if the request has failed and that therefore we should respond with <code>offline.html</code> content.</p>
  174. <h3>Service Worker Registration</h3>
  175. <p>A service worker needs to be registered only once. This example shows how to bypass registration if it’s already been done by checking the presence of the <code>navigator.serviceWorker.controller</code> property; if the <code>controller</code> property doesn’t exist, we move on to registering the service worker.</p>
  176. <pre><code class="language-javascript">&#13;
  177. if (navigator.serviceWorker.controller) {&#13;
  178. // A ServiceWorker controls the site on load and therefor can handle offline&#13;
  179. // fallbacks.&#13;
  180. console.log('DEBUG: serviceWorker.controller is truthy');&#13;
  181. debug(navigator.serviceWorker.controller.scriptURL + ' (onload)', 'controller');&#13;
  182. }&#13;
  183. &#13;
  184. else {&#13;
  185. // Register the ServiceWorker&#13;
  186. console.log('DEBUG: serviceWorker.controller is falsy');&#13;
  187. navigator.serviceWorker.register('service-worker.js', {&#13;
  188. scope: './'&#13;
  189. }).then(function(reg) {&#13;
  190. debug(reg.scope, 'register');&#13;
  191. });&#13;
  192. }&#13;
  193. </code></pre>
  194. <p>With the service worker confirmed as registered, you can test the recipe (and trigger the new page request) by clicking the “refresh” link: (which then triggers a page refresh with a cache-busting parameter):</p>
  195. <pre><code class="language-javascript">&#13;
  196. // The refresh link needs a cache-busting URL parameter&#13;
  197. document.querySelector('#refresh').search = Date.now();&#13;
  198. </code></pre>
  199. <p>Providing the user an offline message instead of allowing the browser to show its own (sometimes ugly) message is an excellent way of keeping a dialog with the user about why the app isn’t available while they’re offline!</p>
  200. <h2>Go offline!</h2>
  201. <p>Service workers have moved offline experience and control into a powerful new space.  Today you can use the Service Worker API in Chrome and <a href="https://www.mozilla.org/en-US/firefox/developer/" target="_blank">Firefox Developer Edition</a>.  Many websites are using service workers today as you can see for yourself by going to <code>about:serviceworkers</code> in Firefox Developer Edition;  you’ll see a listing of installed service workers from websites you’ve visited!</p>
  202. <p><a href="https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2015/11/AboutServiceWorkers.png"><img src="https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2015/11/AboutServiceWorkers.png" alt="about:serviceworkers"/></a></p>
  203. <p>The <a href="https://serviceworke.rs" target="_blank">Service Worker Cookbook</a> is full of excellent, practical recipes and we continue to add more. Keep an eye out for the next post in this series, <em>At Your Service for More than Just appcache</em>, where you’ll learn about using the Service Worker API for more than just offline purposes.</p>