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.

5 年之前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. title: Creating Offline-First Web Apps with Service Workers
  2. url: https://auth0.com/blog/2015/10/30/creating-offline-first-web-apps-with-service-workers/
  3. hash_url: 7c43491dfbc3a003f3a99ab213917a79
  4. <p><strong>TL;DR:</strong> Serving web-app users a good offline experience can be tricky if they become disconnected from the internet. Providing offline functionality is important for UX, and some recent technologies make it easier for developers to accomplish it. In this article, we focus on the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API"><strong>service worker API</strong></a> and find out how to use it with a library called <a href="https://github.com/TalAter/UpUp"><strong>UpUp</strong></a> to make our apps offline-first.</p>
  5. <hr/>
  6. <p>Application users are no longer restricted to devices that are always connected to the internet, so it's more important than ever that applications can handle poor or no network connection. Ideally, apps should still work when the network connection is lost, with a mechanism for local data storage synced with a remote database. This kind of functionality is familiar in many ways; native applications provide mechanisms to accomplish it easily. We can achieve the same effects in mobile hybrid apps, as well as web apps in general, with some relatively new technologies.</p>
  7. <p>In this article, we'll explore the current state of offline-first applications and see what kind of approach to take to ensure a smooth user experience in our own apps, even when disconnected. We'll talk about <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API"><strong>service workers</strong></a> and how to use them either directly, or with a helper library called <a href="https://github.com/TalAter/UpUp"><strong>UpUp</strong></a>.</p>
  8. <h2>Service Workers for Offline-First Apps</h2>
  9. <p>We're all familiar with what happens when we become disconnected while using the web--we try to move forward on the page or in the app, and we're greeted with a message that tells us we can't. Most native mobile apps will still offer some functionality while offline, but a lot of web apps don't.</p>
  10. <p><img src="https://cdn.auth0.com/blog/offline-first/offline-first-1.jpg" alt="offline-first"/></p>
  11. <p>What we need, then, is a way for our app to detect when we don't have an internet connection, and then respond in a different and particular way. This is where the <strong>service worker API</strong> comes in. In fact, this technology’s main focus is making it easier for developers to provide users with good offline experiences.</p>
  12. <p>A service worker is a bit like a proxy server between the application and the browser, and it has quite a bit of power. With a service worker, we can completely take over the response from an HTTP request and alter it however we like. This is a key feature for serving an offline experience. Since we can detect when the user is disconnected, and we can respond to HTTP requests differently, we have a way of serving the user files and resources that have been saved locally when they are offline.</p>
  13. <p><img src="https://cdn.auth0.com/blog/offline-first/offline-first-diagram.png" alt="service-worker"/></p>
  14. <h2>Service Workers &gt; AppCache</h2>
  15. <p>The service worker API is an attempt to replace the <strong>HTML5 Application Cache</strong>. Nothing is perfect, but AppCache has a host of issues that frustrate developers trying to create offline experiences. One of the biggest issues is that apps won't work at all unless AppCache is set up just right, which means debugging is very tricky. With AppCache, only same-origin resources can be cached, and when it comes to updating resources, it's all or nothing—we can't update cached items individually.</p>
  16. <p>Service workers really shine when stacked up against AppCache. They give us a lot of fine-grained control, so we're able to customize the process of serving an offline experience. Some of this ability is because service workers use promises, which allow us to respond to both <code>success</code> and <code>error</code> conditions.</p>
  17. <h2>Registering a Service Worker</h2>
  18. <p>So how do we make use of service workers? We can access them through the <code>navigator</code> API and hook up new service workers with the <code>register</code> method.</p>
  19. <pre><code class="js">if('serviceWorker' in navigator) {
  20. navigator.serviceWorker
  21. .register('sw.js', { scope: './'})
  22. .then(function(registration) {
  23. console.log('Service worker registered!');
  24. })
  25. .catch(function(error) {
  26. console.log('There was an error!');
  27. });
  28. }
  29. </code></pre>
  30. <p>Service workers are tied to a particular scope, and the location of the service worker script in the project directory is important. If we want the above service worker to apply to any route in our application, we would need the <code>sw.js</code> script to be accessible at <code>http://example.com/sw.js</code>.</p>
  31. <p>With the service worker registered, we need to define what happens when certain events, such as <code>fetch</code>, occur. Instead of rolling out a full example on our own, let's make use of a small library called <a href="https://github.com/TalAter/UpUp">UpUp</a>. This library provides a service worker abstraction that makes it easier to simply define which resources we want available when the user is offline.</p>
  32. <h2>Offline-First Apps with UpUp</h2>
  33. <p>The first step is to create our app just as we would normally.</p>
  34. <pre><code class="html"> &lt;!-- index.html --&gt;
  35. ...
  36. &lt;nav class="navbar navbar-default"&gt;
  37. &lt;div class="container-fluid"&gt;
  38. &lt;a class="navbar-brand"&gt;UpUp App&lt;/a&gt;
  39. &lt;/div&gt;
  40. &lt;/nav&gt;
  41. &lt;div class="container"&gt;
  42. &lt;form&gt;
  43. &lt;div class="form-group"&gt;
  44. &lt;label&gt;New Todo&lt;/label&gt;
  45. &lt;input type="text" class="form-control" placeholder="Enter a new todo"&gt;
  46. &lt;/div&gt;
  47. &lt;button type="submit" class="btn btn-default"&gt;Submit&lt;/button&gt;
  48. &lt;/form&gt;
  49. &lt;/div&gt;
  50. ...
  51. </code></pre>
  52. <p><img src="https://cdn.auth0.com/blog/offline-first/offline-first-2.png" alt="offline-first upup"/></p>
  53. <p>UpUp lets us define what we want to serve the user when they are disconnected. We do this with the <code>start</code> method and we can pass in the <code>content-url</code> and an array of <code>assets</code> that should be used.</p>
  54. <pre><code class="html"> &lt;!-- index.html --&gt;
  55. ...
  56. &lt;script src="upup.min.js"&gt;&lt;/script&gt;
  57. &lt;script&gt;
  58. UpUp.start({
  59. 'content-url': 'templates/offline.html',
  60. 'assets': [
  61. 'css/bootstrap.min.css'
  62. // Other assets like images, JS libraries etc
  63. ]
  64. });
  65. &lt;/script&gt;
  66. ...
  67. </code></pre>
  68. <p>We can tell UpUp which specific template to use when the user is offline. The content we serve when when offline can be the same as when online, or we can customize it to let the user know they are disconnected.</p>
  69. <pre><code class="html"> &lt;!-- templates/offline.html --&gt;
  70. ...
  71. &lt;div class="alert alert-danger"&gt;
  72. You are currently offline, but you can keep working.
  73. &lt;/div&gt;
  74. ...
  75. </code></pre>
  76. <p><img src="https://cdn.auth0.com/blog/offline-first/offline-first-3.png" alt="offline-first upup"/></p>
  77. <blockquote><p>TIP: You don't need to unplug your modem to simulate being offline. Simply use the "Toggle Device Mode" with Chrome dev tools and select <strong>Network: Offline</strong>. <img src="https://cdn.auth0.com/blog/offline-first/offline-first-4.png" alt="offline-first service worker upup"/></p></blockquote>
  78. <p>If we need a framework for our app, we can bring in the necessary JavaScript within the <code>assets</code> array so that it is retrieved when offline.</p>
  79. <p>So that works, but what exactly is UpUp doing to achieve this? Looking at the UpUp service worker will give us an idea.</p>
  80. <pre><code class="js">// src/upup.sw.js
  81. self.addEventListener('fetch', function(event) {
  82. event.respondWith(
  83. // try to return untouched request from network first
  84. fetch(event.request.url, { mode: 'no-cors' }).catch(function() {
  85. // if it fails, try to return request from the cache
  86. return caches.match(event.request).then(function(response) {
  87. if (response) {
  88. return response;
  89. }
  90. // if not found in cache, return default offline content
  91. if (event.request.headers.get('accept').includes('text/html')) {
  92. return caches.match('sw-offline-content');
  93. }
  94. })
  95. })
  96. );
  97. });
  98. </code></pre>
  99. <p>UpUp is listening for <code>fetch</code> events and first tries to return a request from the network. If that fails, it looks to the cache to resolve the request; if that fails too, it serves the offline content we registered.</p>
  100. <p>The service worker itself is wired up with the <code>start</code> method we saw earlier.</p>
  101. <pre><code class="js">// upup.js
  102. start: function(settings) {
  103. this.addSettings(settings);
  104. // register the service worker
  105. _serviceWorker.register(_settings.script, {scope: './'}).then(function(registration) {
  106. // Registration was successful
  107. if (_debugState) {
  108. console.log('ServiceWorker registration successful with scope: %c'+registration.scope, _debugStyle);
  109. }
  110. ...
  111. </code></pre>
  112. <h2>What About Data?</h2>
  113. <p>Native apps naturally provide ways for collecting data while the user is offline. This data can be synced with a remote database once a connection is re-established. When it comes to the web, however, data synchronization isn't as easy.</p>
  114. <p>We might be inclined to roll our own solutions, but dealing with timestamps, revisions, conflict resolution, and consistency can be a lot of work. Fortunately, there are some great solutions for collecting data locally for syncing later.</p>
  115. <h3>PouchDB</h3>
  116. <p><a href="http://pouchdb.com/">PouchDB</a> is an open source local data storage library that can be set up with <a href="http://couchdb.apache.org/">CouchDB</a> to automatically sync data. PouchDB emulates CouchDB very closely, so the API between the two looks and feels quite similar.</p>
  117. <p>PouchDB makes it trivial to set up a local and remote database and to have them automatically sync with one another. Local databases use the browser's IndexedDB to store data.</p>
  118. <pre><code class="js">
  119. // Local databases are created by just providing a name
  120. var local = new PouchDB('todos');
  121. // Remote databases are created by providing a path to CouchDB
  122. var remote = new PouchDB('http://localhost:5984/todos');
  123. </code></pre>
  124. <p>PouchDB uses a simple promise-based API for storing and retrieving documents.</p>
  125. <pre><code class="js">
  126. // Store a document
  127. var todo = {
  128. "_id": "todo1",
  129. "name": "Go to the store"
  130. }
  131. local.put(todo);
  132. // Retrieve a document
  133. local.get('todo1').then(function(data) {
  134. console.log(data);
  135. }).catch(function(error) {
  136. console.log('There was an error');
  137. });
  138. </code></pre>
  139. <p>When it comes to syncing, we can have one-way or two-way sync, and we can choose to have the databases replicate continuously or just at a time we specify. In many cases, we'll set up live replication that accounts for a user dropping in and out of network coverage. To do this, we just need to tell PouchDB that we want it to retry syncing.</p>
  140. <pre><code class="js">local.sync(remote, {
  141. live: true,
  142. retry: true
  143. });
  144. </code></pre>
  145. <h2>Aside: Authentication is Easy with Auth0</h2>
  146. <p>Auth0 issues <a href="http://jwt.io">JSON Web Tokens</a> on every login for your users. This means that you can have a solid <a href="https://auth0.com/docs/identityproviders">identity infrastructure</a>, including <a href="https://auth0.com/docs/sso/single-sign-on">single sign-on</a>, user management, support for social (Facebook, Github, Twitter, etc.), enterprise (Active Directory, LDAP, SAML, etc.) and your own database of users with just a few lines of code.</p>
  147. <p>You can use <a href="https://auth0.com/docs/libraries/lock">Lock</a> for your offline-first web app. With Lock, showing a login screen is as simple as including the <strong>auth0-lock</strong> library and then calling it in your app.</p>
  148. <pre><code class="js">// Initialize Auth0Lock with your `clientID` and `domain`
  149. var lock = new Auth0Lock('xxxxxx', '&lt;account&gt;.auth0.com');
  150. // and deploy it
  151. var login = document.querySelector('a#login')
  152. login.onclick = function (e) {
  153. e.preventDefault();
  154. lock.show(function onLogin(err, profile, id_token) {
  155. if (err) {
  156. // There was an error logging the user in
  157. return alert(err.message);
  158. }
  159. // User is logged in
  160. });
  161. };
  162. </code></pre>
  163. <p><img src="https://i.cloudup.com/6opoEX_Z9z.png" alt="lock auth0"/></p>
  164. <p>In the case of an offline-first app, authenticating the user against a remote database won't be possible when network connectivity is lost. However, with service workers and a library like UpUp, you have full control over which pages and scripts are loaded when the user is offline. This means you can configure your <code>offline.html</code> file to display a useful message stating the user needs to regain connectivity to login again instead of displaying the Lock login screen.</p>
  165. <h2>Wrapping Up</h2>
  166. <p>With so many app users relying on mobile, and with spotty networks in many places, it's becoming more and more essential to give our users a decent offline experience. The <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API"><strong>service worker API</strong></a> helps greatly, and really outdoes <strong>AppCache</strong>, because we have much more control over what happens. While the service worker API is relatively straightforward to use, setting up an offline app is even easier with abstractions such as the one provided by <a href="https://github.com/TalAter/UpUp">UpUp</a>.</p>
  167. <p>Focusing on the offline experience is not always feasible, perhaps because other features take priority. However, providing usability while offline is valuable and can even be a key differentiator between your app and others.</p>