A place to cache linked articles (think custom and personal wayback machine)
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

index.md 37KB

4 år sedan
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. title: Taking the web offline with service workers
  2. url: https://mobiforge.com/design-development/taking-web-offline-service-workers
  3. hash_url: 37423996e74c25551e0f500cdabb85a7
  4. <p>You’re probably already familiar with the idea of offline web apps, web apps that can continue to work in the face of intermittent network connectivity. This concept has been around for a while, and various technologies have been developed along the way to achieve offline web apps, (Google) Gears, and Appcache for example, but none of these addressed the offline challenge quite as well as service workers. </p>
  5. <h2><a name="user-content-what-is-a-service-worker" href="#what-is-a-service-worker" aria-hidden="true"></a>What is a service worker?</h2>
  6. <p>Service workers provide a way for webpages to run scripts in the background <em>even when the page is not open</em>. A service worker can act as a proxy to a web page, intercepting requests and controlling responses, and this makes them well suited to dealing with many exciting web features, such as offline capabilities, background syncing, and <a href="https://mobiforge.com/design-development/web-push-notifications">push notifications</a>. Features like these have traditionally given native apps an edge over web apps, but with service workers, all kinds of previously impossible things are now possible on the web. This means service workers are a very big deal!</p>
  7. <h2><a name="user-content-what-are-offline-web-apps" href="#what-are-offline-web-apps" aria-hidden="true"></a>What are offline web apps?</h2>
  8. <p>The term <em>offline web app</em> can mean different things to different people. A good description of what is meant by an <em>offline</em> web app is given in this article: <a href="http://www.html5rocks.com/en/tutorials/offline/whats-offline/">What’s offline and why should I care?</a> Sometimes called <em>online-offline</em>, what we’re really talking about in this article is a class of web app that can function, at least to some useful degree, with an intermittent network connection.</p>
  9. <h2><a name="user-content-why-are-offline-web-apps-desirable" href="#why-are-offline-web-apps-desirable" aria-hidden="true"></a>Why are offline web apps desirable?</h2>
  10. <p>Two main reason are availability and performance:</p>
  11. <ul>
  12. <li><strong>Availability</strong> A web app can still work without a network connection, thus making it available</li>
  13. <li><strong>Performance</strong> Because a web app makes content offline, by caching requests and responses as we’ll see later, this means that it can be much quicker to retrieve and present content to the user, often without having to make a network request</li>
  14. </ul>
  15. <h2><a name="user-content-offline-web-app-use-cases" href="#offline-web-app-use-cases" aria-hidden="true"></a>Offline web app use cases</h2>
  16. <p>Now with some idea of what an offline webapp is, we can list a few use cases:</p>
  17. <ul>
  18. <li>Simple case: caching to improve performance and availability</li>
  19. <li>Collaborative document writing</li>
  20. <li>Communcation: for example composing email offline, which sends when online again</li>
  21. </ul>
  22. <p>A list of interesting offline projects and use cases is given on the <a href="https://github.com/w3c-webmob/ServiceWorkersDemos">W3C Web and Mobile (WebMob) Interest Group’s GitHub page</a>.</p>
  23. <p>However, there is no doubt that to endow a web app with offline capabilities is more complex than to build a web app without. There are cache policies to consider, issues around stale content, when to update, and how to notify users that there is newer content, all of which must be considered so that the user experience is not accidentally adversely affected. For example, updating a piece of content <em>while</em> a user is reading it might be jarring if the user was not expecting it. As <a href="http://www.html5rocks.com/en/tutorials/offline/whats-offline/">pointed out by Michael Mahemoff</a>, you should be able to justify the complexity of making your web app available offline. </p>
  24. <p>In this article we’ll show how to add basic offline capabilities to a webapp, which sort of corresponds with the offline magazine app suggestion on the WebMob Service Workers Demo page given above, although what we develop here will be more of a proof of concept that a full blown offline solution.</p>
  25. <h2>Why not just use the browser cache?</h2>
  26. <p>Sure, browsers cache stuff all the time. The advantages here are in persistence and control. Browser caches are easily overwritten while an application cache is more persistent. Badly configured servers often force the client to refetch things unecessarily, for example hosting providers you don't have control over. Service workers, with a full caching API, allow the client full control, and can make smarter decisions about what and when to cache. Many of us have experienced the case where we get on a plane with a page loaded in our browser, only to find that either a browser setting or cache header forces a reload attempt when you then go to read the page, a reload that has no hope of succeeding, and your page is gone.</p>
  27. <h2><a name="user-content-getting-started-with-service-workers" href="#getting-started-with-service-workers" aria-hidden="true"></a>Getting started with service workers</h2>
  28. <p>Let’s get the service worker basics out of the way. </p>
  29. <h3><a name="user-content-https" href="#https" aria-hidden="true"></a>HTTPS</h3>
  30. <p>Firstly, HTTPS is required for service workers. Since service workers control webpages and how they respond to a client, HTTPS is needed to needed to prevent hijacking.</p>
  31. <h3><a name="user-content-registering-a-service-worker" href="#registering-a-service-worker" aria-hidden="true"></a>Registering a service worker</h3>
  32. <p>To register a service worker, you need to include a reference to the service worker JavaScript file in your web page. You should check for service worker support first:</p>
  33. <div class="geshifilter"><pre class="geshifilter-javascript"><span>if</span> <span>(</span><span>'serviceWorker'</span> <span>in</span> navigator<span>)</span> <span>{</span>
  34. navigator.<span>serviceWorker</span>.<span>register</span><span>(</span><span>'/sw.js'</span><span>)</span>.<span>then</span><span>(</span><span>function</span><span>(</span><span>)</span> <span>{</span>
  35. <span>// Success</span>
  36. <span>}</span><span>)</span>.<span>catch</span><span>(</span><span>function</span><span>(</span><span>)</span> <span>{</span>
  37. <span>// Fail :(</span>
  38. <span>}</span><span>)</span>;
  39. <span>}</span></pre></div>
  40. <p>Scope refers to the pages the service worker can control. In the above, the service worker can control <em>any</em> under the root and its subdirectories.</p>
  41. <h3><a name="user-content-lifecycle" href="#lifecycle" aria-hidden="true"></a>Lifecycle</h3>
  42. <p>The lifecyle of a service worker is a little bit more complex than this, but the two main events we are interested in are </p>
  43. <ol class="numbered">
  44. <li>Installing - perform some tasks at install time to prepare your service worker</li>
  45. <li>Activation - perform some tasks at activation time</li>
  46. </ol>
  47. <p>Implicit in the above is that a service worker can be installed but not active. On first page-load the service worker will be installed, but it won’t be until the next page request that it actually takes control of the page. This default behaviour can be overridden, and a service worker can take control of a page immediately by making calls to the <span class="geshifilter"><code class="geshifilter-text">skipWaiting()</code></span> and <span class="geshifilter"><code class="geshifilter-text">clients.claim()</code></span> methods.</p>
  48. <p>Service workers can be stopped and restarted as they are needed. This means that global variables don’t exist across restarts. If you need to preserve state then you should use IndexedDB, which is available in service workers.</p>
  49. <h3><a name="user-content-fetching-and-caching" href="#fetching-and-caching" aria-hidden="true"></a>Fetching and caching</h3>
  50. <p>Two key components of getting service workers to build offline apps are the <em>cache</em>, and <em>fetch</em> APIs. The cache allows us to store requests and responses, while fetch allows us the intercept requests, and serve up cached responses. Using cache and fetch together enables us to concoct all kinds of interesting offline scenarios. See <a href="https://jakearchibald.com/2014/offline-cookbook/">Jake Archibald’s Offline Cookbook</a> for an excellent hands-on review of various caching policies you might use.</p>
  51. <h3><a name="user-content-service-worker-cache-api" href="#service-worker-cache-api" aria-hidden="true"></a>Service Worker Cache API</h3>
  52. <p>Service workers bring a new cache API to the table. Let’s have a quick look at this. With the <em>CacheStorage</em> object we can <span class="geshifilter"><code class="geshifilter-text">open</code></span> (and name), and <span class="geshifilter"><code class="geshifilter-text">delete</code></span> individual caches. With individual caches, typical operations are to <span class="geshifilter"><code class="geshifilter-text">add</code></span> and <span class="geshifilter"><code class="geshifilter-text">remove</code></span> items. So, let’s open a cache and and store some pages.</p>
  53. <div class="geshifilter"><pre class="geshifilter-javascript"> <span>// Add the addAll method</span>
  54. importScripts<span>(</span><span>'sw-cache-addall.js'</span><span>)</span>;
  55.  
  56. <span>// A list of paths to cache</span>
  57. <span>var</span> paths = <span>[</span>
  58. <span>'/'</span>,
  59. <span>'/css/main.css'</span>,
  60. <span>'/js/main.js'</span>,
  61. <span>'/about.htm'</span>
  62. <span>]</span>;
  63.  
  64. <span>// Open the cache (and name it)</span>
  65. caches.<span>open</span><span>(</span><span>'offline-v1'</span><span>)</span>.<span>then</span><span>(</span><span>function</span><span>(</span>cache<span>)</span> <span>{</span>
  66. <span>return</span> cache.<span>addAll</span><span>(</span>paths<span>)</span>;
  67. <span>}</span><span>)</span></pre></div>
  68. <h3><a name="user-content-fetch-requests-with-service-workers" href="#fetch-requests-with-service-workers" aria-hidden="true"></a>Fetch requests with service workers</h3>
  69. <p>By listening for <span class="geshifilter"><code class="geshifilter-text">fetch</code></span> events from web pages under its control, a service worker can intercept, manipulate and respond to requests for those pages. For instance, the service worker can try to pull content from its cache, and if that fails then go to the network for the original request:</p>
  70. <div class="geshifilter"><pre class="geshifilter-javascript">self.<span>addEventListener</span><span>(</span><span>'fetch'</span>, <span>function</span><span>(</span>event<span>)</span> <span>{</span>
  71. event.<span>respondWith</span><span>(</span>
  72. caches.<span>match</span><span>(</span>event.<span>request</span><span>)</span>.<span>then</span><span>(</span><span>function</span><span>(</span>response<span>)</span> <span>{</span>
  73. <span>return</span> response <span>||</span> fetch<span>(</span>event.<span>request</span><span>)</span>;
  74. <span>}</span><span>)</span>
  75. <span>)</span>;
  76. <span>}</span><span>)</span>;</pre></div>
  77. <h3><a name="user-content-communicating-with-service-workers-postmessage" href="#communicating-with-service-workers-postmessage" aria-hidden="true"></a>Communicating with Service Workers: postMessage</h3>
  78. <p>Service workers control pages, but they don’t have access to the DOM. However, service workers and web pages can communicate by sending messages to each other via the <span class="geshifilter"><code class="geshifilter-text">postMessage()</code></span> method. To send a message from webpage to service worker you could use:</p>
  79. <div class="geshifilter"><pre class="geshifilter-javascript"> navigator.<span>serviceWorker</span>.<span>controller</span>.<span>postMessage</span><span>(</span><span>{</span><span>'command'</span>: <span>'say-hello!'</span><span>}</span><span>)</span></pre></div>
  80. <p>Note that the structure and content of the message is entirely up to you. </p>
  81. <p>And to send a message from service worker to a page that it controls, you could use:</p>
  82. <div class="geshifilter"><pre class="geshifilter-javascript"> client.<span>postMessage</span><span>(</span><span>{</span><span>'message'</span>:<span>'hello!'</span><span>}</span><span>)</span>;</pre></div>
  83. <p>You’ll need to get a reference to the client page, as a service worker might control multiple pages.</p>
  84. <p>To receive a message in either page or service worker, we need to set up a listener for the message event. In a webpage we set it up as:</p>
  85. <div class="geshifilter"><pre class="geshifilter-javascript">navigator.<span>serviceWorker</span>.<span>addEventListener</span><span>(</span><span>'message'</span>, <span>function</span><span>(</span>event<span>)</span> <span>{</span>
  86. <span>//do something with the message event.data</span>
  87. <span>}</span><span>)</span></pre></div>
  88. <p>In the service worker we set it up with:</p>
  89. <div class="geshifilter"><pre class="geshifilter-javascript">self.<span>addEventListener</span><span>(</span><span>'message'</span>, <span>function</span><span>(</span>event<span>)</span> <span>{</span>
  90. <span>//do something with the message event.data</span>
  91. <span>}</span><span>)</span></pre></div>
  92. <p>And for completeness, to send a message to a service worker from a page, <em>and</em> to receive a response from the service worker, then you must also provide a <em>message channel</em> in the original message that the service worker can use to reply via. You can see an example of this in this <a href="https://googlechrome.github.io/samples/service-worker/post-message/">Service Worker two way postMessage example</a> article.</p>
  93. <p>Additionally, since a service worker intercepts requests, it’s also possible to communicate with the service worker via HTTP request, with the service worker responding perhaps with a JSON response.</p>
  94. <h2><a name="user-content-building-offline-web-apps-with-server-workers" href="#building-offline-web-apps-with-server-workers" aria-hidden="true"></a>Building offline web apps with server workers</h2>
  95. <p>After that quick introduction to service workers, you should now have a rough idea what they are, and how to use them, and you’re hopefully convinced of the benefits of offline-capable web apps; now it's time to build an offline web app with service workers.</p>
  96. <p>As a working demo, we’ll add some offline capabilities to mobiForge so that it will have <em>opt-in offline-reading</em> capabilities. It will work as follows:</p>
  97. <ol class="numbered">
  98. <li>User opts-in by via checkbox</li>
  99. <li>Web page sends message to service worker</li>
  100. <li>Service worker fetches recent content and caches it</li>
  101. <li>For each subsequent request, service worker checks cache first, and falls back to network if content is not found. If network is unavailable and page is not available offline, a new ‘not available offline’ page is displayed.</li>
  102. </ol>
  103. <h2>Service worker offline reading demo</h2>
  104. <p>The impatient reader can click through to the demo now; after opting in, kill the network on your device, and follow the link to the offline content.</p>
  105. <p><a href="/page/service-workers-offline-opt-demo" target="_blank">Launch offline reading demo!</a></p>
  106. <h2>Implementing offline reading with service workers</h2>
  107. <p>First we register the service worker on our web page:</p>
  108. <div class="geshifilter"><pre class="geshifilter-javascript"><span>if</span> <span>(</span><span>'serviceWorker'</span> <span>in</span> navigator<span>)</span> <span>{</span>
  109. navigator.<span>serviceWorker</span>.<span>register</span><span>(</span><span>'/sw.js'</span><span>)</span>.<span>then</span><span>(</span><span>function</span><span>(</span><span>)</span> <span>{</span>
  110. console.<span>log</span><span>(</span><span>'Service worker registered'</span><span>)</span>;
  111. <span>}</span><span>)</span>.<span>catch</span><span>(</span><span>function</span><span>(</span><span>)</span> <span>{</span>
  112. console.<span>log</span><span>(</span><span>'Service worker registration failed'</span><span>)</span>;
  113. <span>}</span><span>)</span>;
  114. <span>}</span>
  115. <span>else</span> <span>{</span>
  116. console.<span>log</span><span>(</span><span>'Service worker not supported'</span><span>)</span>;
  117. <span>}</span></pre></div>
  118. <p>We’ll want to cache some static resources when the service worker is installed, but we’ll come to that later; let’s stick with the web page code for now.</p>
  119. <p>Next, we’ll build the user interface. It’s just a simple checkbox for an all-or-nothing opt-in. A better interface might allow the user to specify exactly what content he or she wanted to make available offline, by topic or category, or by author, for example.</p>
  120. <p>Because a service worker can be terminated and restarted as needed, you can’t rely on it to store state across restarts. If you need persistent data, then you can use IndexedDB. Since we only want to store a simple boolean in this example <span class="geshifilter"><code class="geshifilter-text">opt-in</code></span> to keep things simple we’ll just use <span class="geshifilter"><code class="geshifilter-text">localStorage</code></span> to store this state, but in general if you have state information to store it would probably be better to use IndexedDB.</p>
  121. <div class="geshifilter"><pre class="geshifilter-xml"> <span><span>&lt;div</span> <span>id</span>=<span>"go-offline"</span><span>&gt;</span></span><span><span>&lt;/div<span>&gt;</span></span></span></pre></div>
  122. <p>And the JavaScript:</p>
  123. <div class="geshifilter"><pre class="geshifilter-javascript"><span>var</span> optedIn = <span>false</span>;
  124. window.<span>onload</span> = <span>function</span><span>(</span><span>)</span> <span>{</span>
  125.  
  126. <span>if</span><span>(</span><span>'serviceWorker'</span> <span>in</span> navigator<span>)</span> <span>{</span>
  127. document.<span>getElementById</span><span>(</span><span>'go-offline'</span><span>)</span>.<span>innerHTML</span> = <span>'&lt;h4&gt;mobiForge offline reading&lt;/h4&gt;&lt;input type="checkbox" name="go-offline" id="go-offline-status" value="false" /&gt;&lt;label for="go-offline-status"&gt;Make recent articles available for offline reading&lt;/label&gt;&lt;div id="off-line-msg"&gt;&lt;/div&gt;'</span>;
  128.  
  129. checkOptedIn<span>(</span><span>)</span>;
  130.  
  131. document.<span>getElementById</span><span>(</span><span>"go-offline-status"</span><span>)</span>.<span>addEventListener</span><span>(</span><span>'click'</span>, <span>function</span><span>(</span><span>)</span><span>{</span>
  132. console.<span>log</span><span>(</span><span>'start/stop fetching'</span><span>)</span>;
  133. optInOut<span>(</span><span>)</span>;
  134. <span>}</span><span>)</span>;
  135.  
  136. <span>}</span>
  137. <span>else</span> document.<span>getElementById</span><span>(</span><span>'go-offline'</span><span>)</span>.<span>innerHTML</span> = <span>'ServiceWorker not supported :-('</span>;
  138.  
  139. <span>}</span>;</pre></div>
  140. <p>Note that we need to check the opt-in status with <span class="geshifilter"><code class="geshifilter-text">checkOptedIn</code></span>. This just checks the value of the localStorage item with the following code, and sets the checkbox appropriately:</p>
  141. <div class="geshifilter"><pre class="geshifilter-javascript"><span>function</span> checkOptedIn<span>(</span><span>)</span> <span>{</span>
  142. console.<span>log</span><span>(</span><span>'checking opted in...'</span><span>)</span>
  143. <span>if</span><span>(</span><span>!!</span>localStorage.<span>getItem</span><span>(</span><span>'offlineoptin'</span><span>)</span><span>)</span> <span>{</span>
  144. optedIn = <span>true</span>;
  145. document.<span>getElementById</span><span>(</span><span>"go-offline-status"</span><span>)</span>.<span>checked</span> = <span>true</span>;
  146. console.<span>log</span><span>(</span><span>'opted in'</span><span>)</span>;
  147. <span>return</span>;
  148. <span>}</span>
  149. <span>else</span> console.<span>log</span><span>(</span><span>'not opted in'</span><span>)</span>;
  150. <span>}</span></pre></div>
  151. <p>Note also that we added a click handler for the check box, which call <span class="geshifilter"><code class="geshifilter-text">optInOut</code></span>. Recall from earlier that pages and service workers can communicate by posting messages to each other. To allow a user to opt-in to offline, we’ll provide a checkbox which, when checked, will post a message to the service worker to make the most recent content available offline. We could also have a <em>Make this article available offline</em> associated with each article, in addition to, or instead of our <em>Make all recent content available offline</em> checkbox. This is left as an exercise for the reader.</p>
  152. <div class="geshifilter"><pre class="geshifilter-javascript"><span>function</span> optInOut<span>(</span><span>)</span> <span>{</span>
  153. <span>if</span><span>(</span><span>!</span>optedIn<span>)</span> <span>{</span>
  154. optedIn = <span>true</span>;
  155. localStorage.<span>setItem</span><span>(</span><span>'offlineoptin'</span>, <span>'1'</span><span>)</span>;
  156. navigator.<span>serviceWorker</span>.<span>controller</span>.<span>postMessage</span><span>(</span><span>{</span><span>'command'</span>: <span>'offline-opt-in'</span><span>}</span><span>)</span>;
  157. <span>}</span>
  158. <span>else</span> <span>{</span>
  159. optedIn = <span>false</span>;
  160. localStorage.<span>removeItem</span><span>(</span><span>'offlineoptin'</span><span>)</span>;
  161. navigator.<span>serviceWorker</span>.<span>controller</span>.<span>postMessage</span><span>(</span><span>{</span><span>'command'</span>: <span>'offline-opt-out'</span><span>}</span><span>)</span>;
  162. <span>}</span>
  163. <span>}</span></pre></div>
  164. <p>So that’s the interface out of the way, and communication is set up with the service worker. Let’s look at the service worker code.</p>
  165. <h2><a name="user-content-service-worker-implementation" href="#service-worker-implementation" aria-hidden="true"></a>Service worker implementation</h2>
  166. <p>The service worker needs to cache some basic static resources. When the user opts in, it should also cache the most recent content. It will pull this from the home page of the site which contains a stream of the latest posts. We’ll cache this page, plus each of the latest posts, and their associated assets.</p>
  167. <p>We cache some basic static resources, such as images, scripts, and css that will be needed just to display every page. These resources are cached during the install event that was mentioned earlier; <span class="geshifilter"><code class="geshifilter-text">waitUntil</code></span> ensures that the pages are cached before installation is completed:</p>
  168. <div class="geshifilter"><pre class="geshifilter-javascript"> <span>// Add the missing addAll functionality</span>
  169. importScripts<span>(</span><span>'sw-cache-addall.js'</span><span>)</span>;
  170.  
  171. <span>// A list of paths to cache</span>
  172. <span>var</span> paths = <span>[</span>
  173. <span>'/'</span>,
  174. <span>'/css/main.css'</span>,
  175. <span>'/js/main.js'</span>,
  176. <span>'/img/logo.png'</span>,
  177. <span>'/about.htm'</span>
  178. <span>]</span>;
  179.  
  180. self.<span>addEventListener</span><span>(</span><span>'install'</span>, <span>function</span><span>(</span>event<span>)</span> <span>{</span>
  181. event.<span>waitUntil</span><span>(</span>
  182. caches.<span>open</span><span>(</span><span>'offline-v1'</span><span>)</span>
  183. .<span>then</span><span>(</span><span>function</span><span>(</span>cache<span>)</span> <span>{</span>
  184. <span>return</span> cache.<span>addAll</span><span>(</span>paths<span>)</span>;
  185. <span>}</span><span>)</span>
  186. <span>)</span>;
  187. event.<span>waitUntil</span><span>(</span>self.<span>skipWaiting</span><span>(</span><span>)</span><span>)</span>;
  188. <span>}</span><span>)</span>;</pre></div>
  189. <p>Next, we’ll listen out for messages from the web page, using the <span class="geshifilter"><code class="geshifilter-text">message</code></span> event. We saw how to do this earlier:</p>
  190. <div class="geshifilter"><pre class="geshifilter-javascript">self.<span>addEventListener</span><span>(</span><span>'message'</span>, <span>function</span><span>(</span>event<span>)</span> <span>{</span>
  191. <span>//do something with the message event.data</span>
  192. <span>}</span><span>)</span></pre></div>
  193. <p>To make the recent content available offline, we’ll fetch and cache the home page, as well as each of the recent articles listed on that page. We’ll scrape the HTML to get these links. This is a bit ugly; ideally, instead of scraping, it would be better to have an API that we could simply query for content.</p>
  194. <p>We can pull out the links we need with a regular expression. Because we’re in control of the site, we don't need to worry that it will be brittle.</p>
  195. <p>For each URL we cache, we’ll want to cache its image assets, or it will look broken when served from cache, so we must take a look at the response, and also add any images included in the content to the cache. We perform this in a function <span class="geshifilter"><code class="geshifilter-text">fetchAndCache</code></span>:</p>
  196. <div class="geshifilter"><pre class="geshifilter-javascript"><span>function</span> fetchAndCache<span>(</span>url, cache<span>)</span> <span>{</span>
  197. <span>return</span> fetch<span>(</span>url<span>)</span>.<span>then</span><span>(</span><span>function</span> <span>(</span>response<span>)</span> <span>{</span>
  198. <span>if</span> <span>(</span>response.<span>status</span> <span>&lt;</span> <span>400</span><span>)</span> <span>{</span>
  199. console.<span>log</span><span>(</span><span>'got '</span>+url<span>)</span>;
  200. cache.<span>put</span><span>(</span>url, response.<span>clone</span><span>(</span><span>)</span><span>)</span>;
  201. <span>}</span>
  202. <span>return</span> response.<span>text</span><span>(</span><span>)</span>;
  203. <span>}</span><span>)</span>.<span>then</span><span>(</span><span>function</span><span>(</span>text<span>)</span> <span>{</span>
  204. <span>var</span> pattern = <span>/</span>img src=<span>(</span>?:\<span>'|<span>\"</span>)<span>\/</span>((?:files|img)<span>\/</span>[^<span>\'</span><span>\"</span>]+)<span>\"</span>/g;
  205. var assets = getMatches(text, pattern, 1);
  206. return cache.addAll(assets);
  207. })
  208. }</span></pre></div>
  209. <p>This code is outlined:</p>
  210. <div class="geshifilter"><pre class="geshifilter-javascript"> self.<span>addEventListener</span><span>(</span><span>'message'</span>, <span>function</span><span>(</span>event<span>)</span> <span>{</span>
  211. console.<span>log</span><span>(</span><span>'SW got message:'</span>+event.<span>data</span>.<span>command</span><span>)</span>;
  212. <span>switch</span> <span>(</span>event.<span>data</span>.<span>command</span><span>)</span> <span>{</span>
  213. <span>case</span> <span>'offline-opt-in'</span>:
  214.  
  215. <span>// Cache homepage, and parse the top links from it</span>
  216. caches.<span>open</span><span>(</span><span>'static-v1'</span><span>)</span>.<span>then</span><span>(</span><span>function</span><span>(</span>cache<span>)</span> <span>{</span>
  217. fetch<span>(</span><span>'/'</span><span>)</span>.<span>then</span><span>(</span><span>function</span><span>(</span>response<span>)</span> <span>{</span>
  218. fetchAndCache<span>(</span><span>'/'</span>, cache<span>)</span>;
  219. <span>return</span> response.<span>text</span><span>(</span><span>)</span>;
  220. <span>}</span><span>)</span>
  221. .<span>then</span><span>(</span><span>function</span><span>(</span>text<span>)</span> <span>{</span>
  222. <span>var</span> pattern = <span>/</span>a href=\<span>"([^<span>\'</span><span>\"</span>]+)<span>\"</span> class=<span>\"</span>item-title<span>\"</span>/g;
  223. var urls = getMatches(text, pattern, 1);
  224. console.log('caching: ' + urls);
  225.  
  226. for(var i=0;i&lt;urls.length;i++) {
  227. console.log('fetching '+urls[i]);
  228. fetchAndCache(urls[i], cache);
  229. }
  230.  
  231. })
  232. });
  233. break;
  234. }
  235. });</span></pre></div>
  236. <p>Another helper function is used help pull out page and image URLs:</p>
  237. <div class="geshifilter"><pre class="geshifilter-javascript"><span>//helper</span>
  238. <span>function</span> getMatches<span>(</span>string, regex, index<span>)</span> <span>{</span>
  239. index <span>||</span> <span>(</span>index = <span>1</span><span>)</span>; <span>// default to the first capturing group</span>
  240. <span>var</span> matches = <span>[</span><span>]</span>;
  241. <span>var</span> match;
  242. <span>while</span> <span>(</span>match = regex.<span>exec</span><span>(</span>string<span>)</span><span>)</span> <span>{</span>
  243. matches.<span>push</span><span>(</span>match<span>[</span>index<span>]</span><span>)</span>;
  244. <span>}</span>
  245. <span>return</span> matches;
  246. <span>}</span></pre></div>
  247. <p>Now it’s time to start intercepting requests. We saw how to do this earlier, by listening out for the <span class="geshifilter"><code class="geshifilter-text">fetch</code></span> event. We need to think a bit about our caching policy at this point. For this example, we’ll keep it super simple: for the home page, we’ll implement <em>network, then cache, then fallback</em>. For all other pages, we’ll implement <em>cache, then network, then fallback</em>. The reasoning is: the home page is likely to change more often than article content. So, we don’t want the user to miss any new content when there is an network connection available. For the articles, we go straight to cache. A better solution would be to implement <em>cache, then network</em> and notify the user if there is newer content. But we’ll keep it simple here—this is more a proof of concept than a full-fledged offline solution.</p>
  248. <div class="geshifilter"><pre class="geshifilter-javascript">self.<span>addEventListener</span><span>(</span><span>'fetch'</span>, <span>function</span><span>(</span>event<span>)</span> <span>{</span>
  249. <span>var</span> requestURL = <span>new</span> URL<span>(</span>event.<span>request</span>.<span>url</span><span>)</span>;
  250.  
  251. <span>// Network, then cache, then fallback for home page</span>
  252. <span>if</span><span>(</span>requestURL==<span>'/'</span><span>)</span> <span>{</span>
  253. event.<span>respondWith</span><span>(</span>
  254. fetch<span>(</span>event.<span>request</span><span>)</span>.<span>then</span><span>(</span><span>function</span><span>(</span><span>)</span> <span>{</span>
  255. <span>return</span> caches.<span>match</span><span>(</span>event.<span>request</span><span>)</span>;
  256. <span>}</span><span>)</span>.<span>catch</span><span>(</span><span>function</span><span>(</span><span>)</span> <span>{</span>
  257. <span>return</span> caches.<span>match</span><span>(</span><span>'/page/content-not-available-offline'</span><span>)</span>;
  258. <span>}</span><span>)</span>
  259. <span>)</span>;
  260. <span>}</span>
  261.  
  262. <span>// Cache, then network, then fallback for other urls</span>
  263. event.<span>respondWith</span><span>(</span>
  264. caches.<span>match</span><span>(</span>event.<span>request</span><span>)</span>.<span>then</span><span>(</span><span>function</span><span>(</span>response<span>)</span> <span>{</span>
  265. <span>return</span> response <span>||</span> fetch<span>(</span>event.<span>request</span><span>)</span>;
  266. <span>}</span><span>)</span>.<span>catch</span><span>(</span><span>function</span><span>(</span><span>)</span> <span>{</span>
  267. <span>return</span> caches.<span>match</span><span>(</span><span>'/page/content-not-available-offline'</span><span>)</span>;
  268. <span>}</span><span>)</span>
  269. <span>)</span>;
  270. <span>}</span><span>)</span>;</pre></div>
  271. <p>And that about wraps it up. Note that this implementation is not perfect; it’s a partial offline solution retro-fitted onto an existing website. It required a little ugly scraping of links from the homepage. If we were building this from the ground up, it would be worth considering to implement a simple content API that we could query for content without HTML page template, and which we could query for new or updated content. We could also do a better job of ensuring necessary scripts and other assets are available offline, and we could also consider hiding functionality that is not available offline, such as the social sharing buttons. But despite these shortcomings, it still serves as a relatively accessible proof of concept of the power of service workers and in this case, their ability to build compelling offline experiences.</p>
  272. <h2><a name="user-content-caching-patterns" href="#caching-patterns" aria-hidden="true"></a>Caching patterns</h2>
  273. <p>Many applications may have unique caching requirements. Should you go to cache first, network first, or both at the same time? We chose a fairly simple approach for this example.Jake Archibald lists several useful patterns in his <a href="https://jakearchibald.com/2014/offline-cookbook/">offline cookbook</a> article. Every application will have its own needs, so choose, or develop, a caching solution that’s appropriate for your application.</p>
  274. <h2><a name="user-content-debugging" href="#debugging" aria-hidden="true"></a>Debugging</h2>
  275. <p>Two all important URLs for debugging service workers are</p>
  276. <ul>
  277. <li>chrome://inspect/#service-workers</li>
  278. <li>chrome://serviceworker-internals/</li>
  279. </ul>
  280. <p>You can also view what caches have been opened, and what URLs are currently cached in them.
  281. </p>
  282. <p class="image-caption"><img src="https://mobiforge.com/files/service-worker-cache.jpg"/><br/>Remote debugging device, showing cache contents</p>
  283. <p>You can also view registered service workers right from the Chrome Developer Tools. However, sometimes the service worker information pane disappears from Chrome developer tools altogether—be prepared to shut down Chrome and restart to bring it back!</p>
  284. <h2><a name="user-content-updating-service-workers" href="#updating-service-workers" aria-hidden="true"></a>Updating service workers</h2>
  285. <p>When you’re developing, you might run into trouble with the page loading the old service worker code. The trick here is that a reload won’t necessarily load the new service worker. Instead, close the current tab altogether, and then load the page again.</p>
  286. <p>It can also be useful to unregister the registered service worker in the Developer Tools before reloading. And remember, unless you're using <span class="geshifilter"><code class="geshifilter-text">skipWaiting()</code></span>/<span class="geshifilter"><code class="geshifilter-text">clients.claim()</code></span> your sevice worker won't take control until the next time the page is loaded.</p>
  287. <p>Read more about updating service workers here: <a href="https://jakearchibald.com/2014/service-worker-first-draft/#updating-service-workers">Updating service workers</a>.</p>
  288. <h2>Browser support and usage in the wild</h2>
  289. <p>While service workers aren't universally supported yet, most of the main browsers are either working on bringing support, or have it on a roadmap. The notable exception is Safari.</p>
  290. <p>Jake Archibald's <a href="https://jakearchibald.github.io/isserviceworkerready/">Is ServiceWorker Ready?</a> page gives a very detailed run down of the level of support in the main browsers. A quick summary would be that right now there are usable implementations in Chrome, Firefox, and Opera; Microsoft's Edge browser is likely to build support soon, but there are no signs right now that Apple will bring service worker support to Safari any time soon.</p>
  291. <p class="image-caption"><img src="https://mobiforge.com/files/service-worker-support.png"/><br/>Service Worker browser support (adapted from<a href="https://jakearchibald.github.io/isserviceworkerready/">Is ServiceWorker Ready?</a>)</p>
  292. <p>Despite only partial support, service workers are popping up in websites everywhere. Just take a look at <a href="chrome://serviceworker-internals/">chrome://serviceworker-internals/</a> and you might be surprised to see the number of service workers listed from various sites that you've recently visited. With well planned usage, a site can function without service workers where it's not supported, but will benefit from all that service workers have to offer where it is!</p>
  293. <h2><a name="user-content-links-and-credits" href="#links-and-credits" aria-hidden="true"></a>Links and credits</h2>
  294. <ul>
  295. <li><a href="http://www.w3.org/TR/service-workers/">http://www.w3.org/TR/service-workers/</a></li>
  296. <li><a href="https://github.com/slightlyoff/ServiceWorker/blob/master/explainer.md">https://github.com/slightlyoff/ServiceWorker/blob/master/explainer.md</a></li>
  297. <li><a href="https://jakearchibald.com/2014/service-worker-first-draft/">https://jakearchibald.com/2014/service-worker-first-draft/</a></li>
  298. <li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers">https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers</a></li>
  299. <li><a href="https://jakearchibald.com/2014/offline-cookbook/">https://jakearchibald.com/2014/offline-cookbook/</a></li>
  300. </ul>
  301. <p>Once again, documentation by Matt Gaunt and Jake Archibald at Google has been indispensable!</p>