A place to cache linked articles (think custom and personal wayback machine)
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

4 роки тому
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. title: Making a Simple Site Work Offline with ServiceWorker
  2. url: https://css-tricks.com/serviceworker-for-offline/
  3. hash_url: cafe9b16715f3da507547bbde95f89ac
  4. <p><em class="explanation">When <a href="https://bevacqua.io/">Nicolas Bevacqua</a> (of <a href="https://ponyfoo.com/">Pony Foo</a>) started talking about a potential guest post, I knew right away we should do something with offline. Nicolas has been writing a lot about the ServiceWorker API and offline stuff is one of the things it was made for. Rather than a theoretical look with code snippets, I thought we could combine that with <a href="https://css-tricks.com/examples/Simple-Offline-Site/">a demo website</a> with it all working. So that's what we did - take it away Nicolas!</em></p>
  5. <p><span id="more-210533"/></p>
  6. <p>I’ve been playing around with ServiceWorker a lot recently, so when Chris asked me to write an article about it I couldn’t have been more thrilled. ServiceWorker is the most impactful modern web technology since Ajax. It’s an API that lives inside the browser and sits between your web pages and your application servers. Once installed and activated, a ServiceWorker can programmatically determine how to respond to requests for resources from your origin, even when the browser is offline. ServiceWorker can be used to power the so-called “Offline-First” web.</p>
  7. <p>ServiceWorker is a progressive technology, and in this article I’ll show you how to take a website and make it available offline for humans who are using a modern browser while leaving humans with unsupported browsers unaffected. </p>
  8. <p>Here's a silent, 26 second video of a supporting browser (Chrome) going offline and the final demo site still working:</p>
  9. <p><iframe src="https://www.youtube.com/embed/9WdMKdQHBBE" frameborder="0" allowfullscreen="">VIDEO</iframe></p>
  10. <h3>Browser Support</h3>
  11. <p>Today, ServiceWorker has browser support in Google Chrome, Opera, and in Firefox behind a configuration flag. Microsoft is <a href="https://twitter.com/jacobrossi/status/608291251121618944">likely to work on it</a> soon. There’s no official word from Apple’s Safari yet. </p>
  12. <p>Jake Archibald has a <a href="https://jakearchibald.github.io/isserviceworkerready/">page tracking the support of all the ServiceWorker-related technologies</a>.</p>
  13. <figure id="post-234765" class="align-right media-234765"><img src="https://cdn.css-tricks.com/wp-content/uploads/2015/11/sw-browsers.png" alt=""/></figure>
  14. <p>Given the fact that you can implement this stuff in a progressive enhancement style (doesn't affect unsupported browsers), it's a great opportunity to get ahead of the pack. The ones that are supported are going to greatly appreciate it.</p>
  15. <p>Before getting started, I should point out a couple of things for you to take into account.</p>
  16. <h3>Secure Connections Only</h3>
  17. <p>You should know that there’s a few hard requirements when it comes to ServiceWorker. First and foremost, <strong>your site needs to be served over a secure connection</strong>. If you’re still serving your site over HTTP, it might be a good excuse to implement HTTPS.</p>
  18. <figure id="post-210536" class="align-none media-210536"><img src="https://cdn.css-tricks.com/wp-content/uploads/2015/11/service-worker1.png" alt=""/><br/>
  19. <figcaption>HTTPS is required for ServiceWorker</figcaption>
  20. </figure>
  21. <p>You could use a CDN proxy like CloudFlare to serve traffic securely. Remember to find and fix mixed content warnings as some browsers may warn your customers about your site being unsafe, otherwise.</p>
  22. <p>While the spec for HTTP/2 doesn’t inherently enforce encrypted connections, browsers intend to implement HTTP/2 and similar technologies <em>only</em> over HTTPS. The ServiceWorker specification, on the other hand, <a href="http://www.w3.org/TR/service-workers/#security-considerations">recommends browser implementation over HTTPS</a>. Browsers have also hinted at marking sites served over unencrypted connections as insecure. Search engines penalize unencrypted results.</p>
  23. <p>“HTTPS only” is the browsers way of saying <em>“this is important, you should do this”.</em></p>
  24. <h3>A <code>Promise</code> Based API</h3>
  25. <p>The future of web browser API implementations is <code>Promise</code>-heavy. The <code>fetch</code> API, for example, sprinkles sweet <code>Promise</code>-based sugar on top of <code>XMLHttpRequest</code>. ServiceWorker makes occasional use of <code>fetch</code>, but there’s also worker registration, caching, and message passing, all of which are Promise-based.</p>
  26. <figure id="post-210537" class="align-none media-210537"><img src="https://cdn.css-tricks.com/wp-content/uploads/2015/11/promises.png" alt=""/></figure>
  27. <p>Whether or not you are a fan of promises, they are here to stay, so you better get used to them.</p>
  28. <h3>Registering Your First ServiceWorker</h3>
  29. <p>I worked together with Chris on the simplest possible practical demonstration of how to use ServiceWorker. He implemented a simple website (static HTML, CSS, JavaScript, and images) and asked me to add offline support. I felt like that’d be a great opportunity to display how easy and unobtrusive it is to add offline capabilities to an existing website.</p>
  30. <p>If you’d like to skip to the end, take a look at the <a href="https://github.com/chriscoyier/Simple-Offline-Site/commit/4928bfe074ec39ce72c27bb9078b8ed50672e938">this commit to the demo site</a> on GitHub.</p>
  31. <p>The first step is to register the ServiceWorker. Instead of blindly attempting the registration, we feature-detect that ServiceWorker is available. </p>
  32. <pre rel="JavaScript"><code class="language-javascript">if ('serviceWorker' in navigator) {&#13;
  33. &#13;
  34. }</code></pre>
  35. <p>The following piece of code demonstrates how we would install a ServiceWorker. The JavaScript resource passed to <code>.register</code> will be executed in the context of a ServiceWorker. Note how registration returns a <code>Promise</code> so that you can track whether or not the ServiceWorker registration was successful. I preceded logging statements with <code>CLIENT</code>: to make it visually easier for me to figure out whether a logging statement was coming from a web page or the ServiceWorker script.</p>
  36. <pre rel="JavaScript"><code class="language-javascript">// ServiceWorker is a progressive technology. Ignore unsupported browsers&#13;
  37. if ('serviceWorker' in navigator) {&#13;
  38. console.log('CLIENT: service worker registration in progress.');&#13;
  39. navigator.serviceWorker.register('/service-worker.js').then(function() {&#13;
  40. console.log('CLIENT: service worker registration complete.');&#13;
  41. }, function() {&#13;
  42. console.log('CLIENT: service worker registration failure.');&#13;
  43. });&#13;
  44. } else {&#13;
  45. console.log('CLIENT: service worker is not supported.');&#13;
  46. }</code></pre>
  47. <p>The endpoint to the <code>service-worker.js</code> file is quite important. If the script were served from, say, <code>/js/service-worker.js</code> then the ServiceWorker would only be able to intercept requests in the <code>/js/</code> context, but it’d be blind to resources like <code>/other</code>. This is typically an issue because you usually scope your JavaScript files in a <code>/js/</code>, <code>/public/</code>, <code>/assets/</code>, or similar “directory”, whereas you’ll want to serve the ServiceWorker script from the domain root in most cases.</p>
  48. <p>That was, in fact, the only necessary change to your web application code, provided that you had already implemented HTTPS. At this point, supporting browsers will issue a request for <code>/service-worker.js</code> and attempt to install the worker.</p>
  49. <p>How should you structure the <code>service-worker.js</code> file, then?</p>
  50. <h3>Putting Together A ServiceWorker</h3>
  51. <p>ServiceWorker is event-driven and <strong>your code should aim to be stateless</strong>. That’s because when a ServiceWorker isn’t being used it’s shut down, losing all state. You have no control over that, so it’s best to avoid any long-term dependence on in-memory state.</p>
  52. <p>Below, I listed the most notable events you’ll have to handle in a ServiceWorker.</p>
  53. <ul>
  54. <li>The <code>install</code> event fires when a ServiceWorker is first fetched. This is your chance to prime the ServiceWorker cache with the fundamental resources that should be available even while users are offline.</li>
  55. <li>The <code>fetch</code> event fires whenever a request originates from your ServiceWorker scope, and you’ll get a chance to intercept the request and respond immediately, without going to the network.</li>
  56. <li>The <code>activate</code> event fires after a successful installation. You can use it to phase out older versions of the worker. We’ll look at a basic example where we deleted stale cache entries.</li>
  57. </ul>
  58. <p>Let’s go over each event and look at examples of how they could be handled.</p>
  59. <h3>Installing Your ServiceWorker</h3>
  60. <p>A version number is useful when updating the worker logic, allowing you to remove outdated cache entries <a href="https://ponyfoo.com/articles/getting-started-with-serviceworker?verify=0b80097456ce81cfcd2ffb5157eb395a#phasing-out-older-serviceworker-versions">during the activation step</a>, as we’ll see a bit later. We’ll use the following version number as a prefix when creating cache stores.</p>
  61. <pre rel="JavaScript"><code class="language-javascript">var version = 'v1::';</code></pre>
  62. <p>You can use <code>addEventListener</code> to register an event handler for the <code>install</code> event. Using <code>event.waitUntil</code> blocks the installation process on the provided <code>p</code> promise. If the promise is rejected because, for instance, one of the resources failed to be downloaded, the service worker won’t be installed. Here, you can leverage the promise returned from opening a cache with <code>caches.open(name)</code> and then mapping that into <code>cache.addAll(resources)</code>, which downloads and stores responses for the provided resources.</p>
  63. <pre rel="JavaScript"><code class="language-javascript">self.addEventListener("install", function(event) {&#13;
  64. console.log('WORKER: install event in progress.');&#13;
  65. event.waitUntil(&#13;
  66. /* The caches built-in is a promise-based API that helps you cache responses,&#13;
  67. as well as finding and deleting them.&#13;
  68. */&#13;
  69. caches&#13;
  70. /* You can open a cache by name, and this method returns a promise. We use&#13;
  71. a versioned cache name here so that we can remove old cache entries in&#13;
  72. one fell swoop later, when phasing out an older service worker.&#13;
  73. */&#13;
  74. .open(version + 'fundamentals')&#13;
  75. .then(function(cache) {&#13;
  76. /* After the cache is opened, we can fill it with the offline fundamentals.&#13;
  77. The method below will add all resources we've indicated to the cache,&#13;
  78. after making HTTP requests for each of them.&#13;
  79. */&#13;
  80. return cache.addAll([&#13;
  81. '/',&#13;
  82. '/css/global.css',&#13;
  83. '/js/global.js'&#13;
  84. ]);&#13;
  85. })&#13;
  86. .then(function() {&#13;
  87. console.log('WORKER: install completed');&#13;
  88. })&#13;
  89. );&#13;
  90. });</code></pre>
  91. <p>Once the install step succeeds, the <code>activate</code> event fires. This helps us <a href="#phasing-out-older-serviceworker-versions">phase out an older ServiceWorker</a>, and we’ll look at it later. For now, let’s focus on the <code>fetch</code> event, which is a bit more interesting.</p>
  92. <h3>Intercepting Fetch Requests</h3>
  93. <p>The <code>fetch</code> event fires whenever a page controlled by this service worker requests a resource. This isn’t limited to <code>fetch</code> or even <code>XMLHttpRequest</code>. Instead, it comprehends even the request for the HTML page on first load, as well as JS and CSS resources, fonts, any images, etc. Note also that requests made against other origins will also be caught by the <code>fetch</code> handler of the ServiceWorker. For instance, requests made against <code>i.imgur.com</code> – the CDN for a popular image hosting site – would also be caught by our service worker as long as the request originated on one of the clients (e.g browser tabs) controlled by the worker.</p>
  94. <p>Just like <code>install</code>, we can block the <code>fetch</code> event by passing a promise to <code>event.respondWith(p)</code>, and when the promise fulfills the worker will respond with that instead of the default action of going to the network. We can use <code>caches.match</code> to look for cached responses, and return those responses instead of going to the network.</p>
  95. <p>As described in the comments, here we’re using an “eventually fresh” caching pattern where we return whatever is stored on the cache but always try to fetch a resource again from the network regardless, to keep the cache updated. If the response we served to the user is stale, they’ll get a fresh response the next time they request the resource. If the network request fails, it’ll try to recover by attempting to serve a hardcoded <code>Response</code>.</p>
  96. <pre rel="JavaScript"><code class="language-javascript">self.addEventListener("fetch", function(event) {&#13;
  97. console.log('WORKER: fetch event in progress.');&#13;
  98. &#13;
  99. /* We should only cache GET requests, and deal with the rest of method in the&#13;
  100. client-side, by handling failed POST,PUT,PATCH,etc. requests.&#13;
  101. */&#13;
  102. if (event.request.method !== 'GET') {&#13;
  103. /* If we don't block the event as shown below, then the request will go to&#13;
  104. the network as usual.&#13;
  105. */&#13;
  106. console.log('WORKER: fetch event ignored.', event.request.method, event.request.url);&#13;
  107. return;&#13;
  108. }&#13;
  109. /* Similar to event.waitUntil in that it blocks the fetch event on a promise.&#13;
  110. Fulfillment result will be used as the response, and rejection will end in a&#13;
  111. HTTP response indicating failure.&#13;
  112. */&#13;
  113. event.respondWith(&#13;
  114. caches&#13;
  115. /* This method returns a promise that resolves to a cache entry matching&#13;
  116. the request. Once the promise is settled, we can then provide a response&#13;
  117. to the fetch request.&#13;
  118. */&#13;
  119. .match(event.request)&#13;
  120. .then(function(cached) {&#13;
  121. /* Even if the response is in our cache, we go to the network as well.&#13;
  122. This pattern is known for producing "eventually fresh" responses,&#13;
  123. where we return cached responses immediately, and meanwhile pull&#13;
  124. a network response and store that in the cache.&#13;
  125. Read more:&#13;
  126. https://ponyfoo.com/articles/progressive-networking-serviceworker&#13;
  127. */&#13;
  128. var networked = fetch(event.request)&#13;
  129. // We handle the network request with success and failure scenarios.&#13;
  130. .then(fetchedFromNetwork, unableToResolve)&#13;
  131. // We should catch errors on the fetchedFromNetwork handler as well.&#13;
  132. .catch(unableToResolve);&#13;
  133. &#13;
  134. /* We return the cached response immediately if there is one, and fall&#13;
  135. back to waiting on the network as usual.&#13;
  136. */&#13;
  137. console.log('WORKER: fetch event', cached ? '(cached)' : '(network)', event.request.url);&#13;
  138. return cached || networked;&#13;
  139. &#13;
  140. function fetchedFromNetwork(response) {&#13;
  141. /* We copy the response before replying to the network request.&#13;
  142. This is the response that will be stored on the ServiceWorker cache.&#13;
  143. */&#13;
  144. var cacheCopy = response.clone();&#13;
  145. &#13;
  146. console.log('WORKER: fetch response from network.', event.request.url);&#13;
  147. &#13;
  148. caches&#13;
  149. // We open a cache to store the response for this request.&#13;
  150. .open(version + 'pages')&#13;
  151. .then(function add(cache) {&#13;
  152. /* We store the response for this request. It'll later become&#13;
  153. available to caches.match(event.request) calls, when looking&#13;
  154. for cached responses.&#13;
  155. */&#13;
  156. cache.put(event.request, cacheCopy);&#13;
  157. })&#13;
  158. .then(function() {&#13;
  159. console.log('WORKER: fetch response stored in cache.', event.request.url);&#13;
  160. });&#13;
  161. &#13;
  162. // Return the response so that the promise is settled in fulfillment.&#13;
  163. return response;&#13;
  164. }&#13;
  165. &#13;
  166. /* When this method is called, it means we were unable to produce a response&#13;
  167. from either the cache or the network. This is our opportunity to produce&#13;
  168. a meaningful response even when all else fails. It's the last chance, so&#13;
  169. you probably want to display a "Service Unavailable" view or a generic&#13;
  170. error response.&#13;
  171. */&#13;
  172. function unableToResolve () {&#13;
  173. /* There's a couple of things we can do here.&#13;
  174. - Test the Accept header and then return one of the `offlineFundamentals`&#13;
  175. e.g: `return caches.match('/some/cached/image.png')`&#13;
  176. - You should also consider the origin. It's easier to decide what&#13;
  177. "unavailable" means for requests against your origins than for requests&#13;
  178. against a third party, such as an ad provider&#13;
  179. - Generate a Response programmaticaly, as shown below, and return that&#13;
  180. */&#13;
  181. &#13;
  182. console.log('WORKER: fetch request failed in both cache and network.');&#13;
  183. &#13;
  184. /* Here we're creating a response programmatically. The first parameter is the&#13;
  185. response body, and the second one defines the options for the response.&#13;
  186. */&#13;
  187. return new Response('&lt;h1&gt;Service Unavailable&lt;/h1&gt;', {&#13;
  188. status: 503,&#13;
  189. statusText: 'Service Unavailable',&#13;
  190. headers: new Headers({&#13;
  191. 'Content-Type': 'text/html'&#13;
  192. })&#13;
  193. });&#13;
  194. }&#13;
  195. })&#13;
  196. );&#13;
  197. });</code></pre>
  198. <p>There’s several more strategies, some of which I discuss in <a href="https://ponyfoo.com/articles/serviceworker-revolution">an article about ServiceWorker strategies on my blog</a>.</p>
  199. <p>As promised, let’s look at the code you can use to phase out older versions of your ServiceWorker script.</p>
  200. <h3>Phasing Out Older ServiceWorker Versions</h3>
  201. <p>The <code>activate</code> event fires after a service worker has been successfully installed. It is most useful when phasing out an older version of a service worker, as at this point you know that the new worker was installed correctly. In this example, we delete old caches that don’t match the <code>version</code> for the worker we just finished installing.</p>
  202. <pre rel="JavaScript"><code class="language-javascript">self.addEventListener("activate", function(event) {&#13;
  203. /* Just like with the install event, event.waitUntil blocks activate on a promise.&#13;
  204. Activation will fail unless the promise is fulfilled.&#13;
  205. */&#13;
  206. console.log('WORKER: activate event in progress.');&#13;
  207. &#13;
  208. event.waitUntil(&#13;
  209. caches&#13;
  210. /* This method returns a promise which will resolve to an array of available&#13;
  211. cache keys.&#13;
  212. */&#13;
  213. .keys()&#13;
  214. .then(function (keys) {&#13;
  215. // We return a promise that settles when all outdated caches are deleted.&#13;
  216. return Promise.all(&#13;
  217. keys&#13;
  218. .filter(function (key) {&#13;
  219. // Filter by keys that don't start with the latest version prefix.&#13;
  220. return !key.startsWith(version);&#13;
  221. })&#13;
  222. .map(function (key) {&#13;
  223. /* Return a promise that's fulfilled&#13;
  224. when each outdated cache is deleted.&#13;
  225. */&#13;
  226. return caches.delete(key);&#13;
  227. })&#13;
  228. );&#13;
  229. })&#13;
  230. .then(function() {&#13;
  231. console.log('WORKER: activate completed.');&#13;
  232. })&#13;
  233. );&#13;
  234. });</code></pre>
  235. <hr/>
  236. <p>You should look at <a href="https://github.com/chriscoyier/Simple-Offline-Site">the full code on the GitHub repository</a> and the <a href="https://css-tricks.com/examples/Simple-Offline-Site/">demo site</a>!</p>