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.html 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang=fr>
  4. <!-- Has to be within the first 1024 bytes, hence before the <title>
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset=utf-8>
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name=viewport content="width=device-width,minimum-scale=1,initial-scale=1,shrink-to-fit=no">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>Creating Offline-First Web Apps with Service Workers (archive) — David Larlet</title>
  13. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  14. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons/apple-touch-icon.png">
  15. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons/favicon-32x32.png">
  16. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons/favicon-16x16.png">
  17. <link rel="manifest" href="/manifest.json">
  18. <link rel="mask-icon" href="/static/david/icons/safari-pinned-tab.svg" color="#5bbad5">
  19. <link rel="shortcut icon" href="/static/david/icons/favicon.ico">
  20. <meta name="apple-mobile-web-app-title" content="David Larlet">
  21. <meta name="application-name" content="David Larlet">
  22. <meta name="msapplication-TileColor" content="#da532c">
  23. <meta name="msapplication-config" content="/static/david/icons/browserconfig.xml">
  24. <meta name="theme-color" content="#f0f0ea">
  25. <!-- That good ol' feed, subscribe :p. -->
  26. <link rel=alternate type="application/atom+xml" title=Feed href="/david/log/">
  27. <meta name="robots" content="noindex, nofollow">
  28. <meta content="origin-when-cross-origin" name="referrer">
  29. <!-- Canonical URL for SEO purposes -->
  30. <link rel="canonical" href="https://auth0.com/blog/2015/10/30/creating-offline-first-web-apps-with-service-workers/">
  31. <style>
  32. /* http://meyerweb.com/eric/tools/css/reset/ */
  33. html, body, div, span,
  34. h1, h2, h3, h4, h5, h6, p, blockquote, pre,
  35. a, abbr, address, big, cite, code,
  36. del, dfn, em, img, ins,
  37. small, strike, strong, tt, var,
  38. dl, dt, dd, ol, ul, li,
  39. fieldset, form, label, legend,
  40. table, caption, tbody, tfoot, thead, tr, th, td,
  41. article, aside, canvas, details, embed,
  42. figure, figcaption, footer, header, hgroup,
  43. menu, nav, output, ruby, section, summary,
  44. time, mark, audio, video {
  45. margin: 0;
  46. padding: 0;
  47. border: 0;
  48. font-size: 100%;
  49. font: inherit;
  50. vertical-align: baseline;
  51. }
  52. /* HTML5 display-role reset for older browsers */
  53. article, aside, details, figcaption, figure,
  54. footer, header, hgroup, menu, nav, section { display: block; }
  55. body { line-height: 1; }
  56. blockquote, q { quotes: none; }
  57. blockquote:before, blockquote:after,
  58. q:before, q:after {
  59. content: '';
  60. content: none;
  61. }
  62. table {
  63. border-collapse: collapse;
  64. border-spacing: 0;
  65. }
  66. /* http://practicaltypography.com/equity.html */
  67. /* https://calendar.perfplanet.com/2016/no-font-face-bulletproof-syntax/ */
  68. /* https://www.filamentgroup.com/lab/js-web-fonts.html */
  69. @font-face {
  70. font-family: 'EquityTextB';
  71. src: url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff2') format('woff2'),
  72. url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff') format('woff');
  73. font-weight: 300;
  74. font-style: normal;
  75. font-display: swap;
  76. }
  77. @font-face {
  78. font-family: 'EquityTextB';
  79. src: url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff2') format('woff2'),
  80. url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff') format('woff');
  81. font-weight: 300;
  82. font-style: italic;
  83. font-display: swap;
  84. }
  85. @font-face {
  86. font-family: 'EquityTextB';
  87. src: url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff2') format('woff2'),
  88. url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff') format('woff');
  89. font-weight: 700;
  90. font-style: normal;
  91. font-display: swap;
  92. }
  93. @font-face {
  94. font-family: 'ConcourseT3';
  95. src: url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff2') format('woff2'),
  96. url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff') format('woff');
  97. font-weight: 300;
  98. font-style: normal;
  99. font-display: swap;
  100. }
  101. /* http://practice.typekit.com/lesson/caring-about-opentype-features/ */
  102. body {
  103. /* http://www.cssfontstack.com/ Palatino 99% Win 86% Mac */
  104. font-family: "EquityTextB", Palatino, serif;
  105. background-color: #f0f0ea;
  106. color: #07486c;
  107. font-kerning: normal;
  108. -moz-osx-font-smoothing: grayscale;
  109. -webkit-font-smoothing: subpixel-antialiased;
  110. text-rendering: optimizeLegibility;
  111. font-variant-ligatures: common-ligatures contextual;
  112. font-feature-settings: "kern", "liga", "clig", "calt";
  113. }
  114. pre, code, kbd, samp, var, tt {
  115. font-family: 'TriplicateT4c', monospace;
  116. }
  117. em {
  118. font-style: italic;
  119. color: #323a45;
  120. }
  121. strong {
  122. font-weight: bold;
  123. color: black;
  124. }
  125. nav {
  126. background-color: #323a45;
  127. color: #f0f0ea;
  128. display: flex;
  129. justify-content: space-around;
  130. padding: 1rem .5rem;
  131. }
  132. nav:last-child {
  133. border-bottom: 1vh solid #2d7474;
  134. }
  135. nav a {
  136. color: #f0f0ea;
  137. }
  138. nav abbr {
  139. border-bottom: 1px dotted white;
  140. }
  141. h1 {
  142. border-top: 1vh solid #2d7474;
  143. border-bottom: .2vh dotted #2d7474;
  144. background-color: #e3e1e1;
  145. color: #323a45;
  146. text-align: center;
  147. padding: 5rem 0 4rem 0;
  148. width: 100%;
  149. font-family: 'ConcourseT3';
  150. display: flex;
  151. flex-direction: column;
  152. }
  153. h1.single {
  154. padding-bottom: 10rem;
  155. }
  156. h1 span {
  157. position: absolute;
  158. top: 1vh;
  159. left: 20%;
  160. line-height: 0;
  161. }
  162. h1 span a {
  163. line-height: 1.7;
  164. padding: 1rem 1.2rem .6rem 1.2rem;
  165. border-radius: 0 0 6% 6%;
  166. background: #2d7474;
  167. font-size: 1.3rem;
  168. color: white;
  169. text-decoration: none;
  170. }
  171. h2 {
  172. margin: 4rem 0 1rem;
  173. border-top: .2vh solid #2d7474;
  174. padding-top: 1vh;
  175. }
  176. h3 {
  177. text-align: center;
  178. margin: 3rem 0 .75em;
  179. }
  180. hr {
  181. height: .4rem;
  182. width: .4rem;
  183. border-radius: .4rem;
  184. background: #07486c;
  185. margin: 2.5rem auto;
  186. }
  187. time {
  188. display: bloc;
  189. margin-left: 0 !important;
  190. }
  191. ul, ol {
  192. margin: 2rem;
  193. }
  194. ul {
  195. list-style-type: square;
  196. }
  197. a {
  198. text-decoration-skip-ink: auto;
  199. text-decoration-thickness: 0.05em;
  200. text-underline-offset: 0.09em;
  201. }
  202. article {
  203. max-width: 50rem;
  204. display: flex;
  205. flex-direction: column;
  206. margin: 2rem auto;
  207. }
  208. article.single {
  209. border-top: .2vh dotted #2d7474;
  210. margin: -6rem auto 1rem auto;
  211. background: #f0f0ea;
  212. padding: 2rem;
  213. }
  214. article p:last-child {
  215. margin-bottom: 1rem;
  216. }
  217. p {
  218. padding: 0 .5rem;
  219. margin-left: 3rem;
  220. }
  221. p + p,
  222. figure + p {
  223. margin-top: 2rem;
  224. }
  225. blockquote {
  226. background-color: #e3e1e1;
  227. border-left: .5vw solid #2d7474;
  228. display: flex;
  229. flex-direction: column;
  230. align-items: center;
  231. padding: 1rem;
  232. margin: 1.5rem;
  233. }
  234. blockquote cite {
  235. font-style: italic;
  236. }
  237. blockquote p {
  238. margin-left: 0;
  239. }
  240. figure {
  241. border-top: .2vh solid #2d7474;
  242. background-color: #e3e1e1;
  243. text-align: center;
  244. padding: 1.5rem 0;
  245. margin: 1rem 0 0;
  246. font-size: 1.5rem;
  247. width: 100%;
  248. }
  249. figure img {
  250. max-width: 250px;
  251. max-height: 250px;
  252. border: .5vw solid #323a45;
  253. padding: 1px;
  254. }
  255. figcaption {
  256. padding: 1rem;
  257. line-height: 1.4;
  258. }
  259. aside {
  260. display: flex;
  261. flex-direction: column;
  262. background-color: #e3e1e1;
  263. padding: 1rem 0;
  264. border-bottom: .2vh solid #07486c;
  265. }
  266. aside p {
  267. max-width: 50rem;
  268. margin: 0 auto;
  269. }
  270. /* https://fvsch.com/code/css-locks/ */
  271. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  272. font-size: 1rem;
  273. line-height: calc( 1.5em + 0.2 * 1rem );
  274. }
  275. h1 {
  276. font-size: 1.9rem;
  277. line-height: calc( 1.2em + 0.2 * 1rem );
  278. }
  279. h2 {
  280. font-size: 1.6rem;
  281. line-height: calc( 1.3em + 0.2 * 1rem );
  282. }
  283. h3 {
  284. font-size: 1.35rem;
  285. line-height: calc( 1.4em + 0.2 * 1rem );
  286. }
  287. @media (min-width: 20em) {
  288. /* The (100vw - 20rem) / (50 - 20) part
  289. resolves to 0-1rem, depending on the
  290. viewport width (between 20em and 50em). */
  291. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  292. font-size: calc( 1rem + .6 * (100vw - 20rem) / (50 - 20) );
  293. line-height: calc( 1.5em + 0.2 * (100vw - 50rem) / (20 - 50) );
  294. margin-left: 0;
  295. }
  296. h1 {
  297. font-size: calc( 1.9rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  298. line-height: calc( 1.2em + 0.2 * (100vw - 50rem) / (20 - 50) );
  299. }
  300. h2 {
  301. font-size: calc( 1.5rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  302. line-height: calc( 1.3em + 0.2 * (100vw - 50rem) / (20 - 50) );
  303. }
  304. h3 {
  305. font-size: calc( 1.35rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  306. line-height: calc( 1.4em + 0.2 * (100vw - 50rem) / (20 - 50) );
  307. }
  308. }
  309. @media (min-width: 50em) {
  310. /* The right part of the addition *must* be a
  311. rem value. In this example we *could* change
  312. the whole declaration to font-size:2.5rem,
  313. but if our baseline value was not expressed
  314. in rem we would have to use calc. */
  315. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  316. font-size: calc( 1rem + .6 * 1rem );
  317. line-height: 1.5em;
  318. }
  319. p, li, pre, details {
  320. margin-left: 3rem;
  321. }
  322. h1 {
  323. font-size: calc( 1.9rem + 1.5 * 1rem );
  324. line-height: 1.2em;
  325. }
  326. h2 {
  327. font-size: calc( 1.5rem + 1.5 * 1rem );
  328. line-height: 1.3em;
  329. }
  330. h3 {
  331. font-size: calc( 1.35rem + 1.5 * 1rem );
  332. line-height: 1.4em;
  333. }
  334. figure img {
  335. max-width: 500px;
  336. max-height: 500px;
  337. }
  338. }
  339. figure.unsquared {
  340. margin-bottom: 1.5rem;
  341. }
  342. figure.unsquared img {
  343. height: inherit;
  344. }
  345. @media print {
  346. body { font-size: 100%; }
  347. a:after { content: " (" attr(href) ")"; }
  348. a, a:link, a:visited, a:after {
  349. text-decoration: underline;
  350. text-shadow: none !important;
  351. background-image: none !important;
  352. background: white;
  353. color: black;
  354. }
  355. abbr[title] { border-bottom: 0; }
  356. abbr[title]:after { content: " (" attr(title) ")"; }
  357. img { page-break-inside: avoid; }
  358. @page { margin: 2cm .5cm; }
  359. h1, h2, h3 { page-break-after: avoid; }
  360. p3 { orphans: 3; widows: 3; }
  361. img {
  362. max-width: 250px !important;
  363. max-height: 250px !important;
  364. }
  365. nav, aside { display: none; }
  366. }
  367. ul.with_columns {
  368. column-count: 1;
  369. }
  370. @media (min-width: 20em) {
  371. ul.with_columns {
  372. column-count: 2;
  373. }
  374. }
  375. @media (min-width: 50em) {
  376. ul.with_columns {
  377. column-count: 3;
  378. }
  379. }
  380. ul.with_two_columns {
  381. column-count: 1;
  382. }
  383. @media (min-width: 20em) {
  384. ul.with_two_columns {
  385. column-count: 1;
  386. }
  387. }
  388. @media (min-width: 50em) {
  389. ul.with_two_columns {
  390. column-count: 2;
  391. }
  392. }
  393. .gallery {
  394. display: flex;
  395. flex-wrap: wrap;
  396. justify-content: space-around;
  397. }
  398. .gallery figure img {
  399. margin-left: 1rem;
  400. margin-right: 1rem;
  401. }
  402. .gallery figure figcaption {
  403. font-family: 'ConcourseT3'
  404. }
  405. footer {
  406. font-family: 'ConcourseT3';
  407. display: flex;
  408. flex-direction: column;
  409. border-top: 3px solid white;
  410. padding: 4rem 0;
  411. background-color: #07486c;
  412. color: white;
  413. }
  414. footer > * {
  415. max-width: 50rem;
  416. margin: 0 auto;
  417. }
  418. footer a {
  419. color: #f1c40f;
  420. }
  421. footer .avatar {
  422. width: 200px;
  423. height: 200px;
  424. border-radius: 50%;
  425. float: left;
  426. -webkit-shape-outside: circle();
  427. shape-outside: circle();
  428. margin-right: 2rem;
  429. padding: 2px 5px 5px 2px;
  430. background: white;
  431. border-left: 1px solid #f1c40f;
  432. border-top: 1px solid #f1c40f;
  433. border-right: 5px solid #f1c40f;
  434. border-bottom: 5px solid #f1c40f;
  435. }
  436. </style>
  437. <h1>
  438. <span><a id="jumper" href="#jumpto" title="Un peu perdu ?">?</a></span>
  439. Creating Offline-First Web Apps with Service Workers (archive)
  440. <time>Pour la pérennité des contenus liés. Non-indexé, retrait sur simple email.</time>
  441. </h1>
  442. <section>
  443. <article>
  444. <h3><a href="https://auth0.com/blog/2015/10/30/creating-offline-first-web-apps-with-service-workers/">Source originale du contenu</a></h3>
  445. <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>
  446. <hr/>
  447. <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>
  448. <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>
  449. <h2>Service Workers for Offline-First Apps</h2>
  450. <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>
  451. <p><img src="https://cdn.auth0.com/blog/offline-first/offline-first-1.jpg" alt="offline-first"/></p>
  452. <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>
  453. <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>
  454. <p><img src="https://cdn.auth0.com/blog/offline-first/offline-first-diagram.png" alt="service-worker"/></p>
  455. <h2>Service Workers &gt; AppCache</h2>
  456. <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>
  457. <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>
  458. <h2>Registering a Service Worker</h2>
  459. <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>
  460. <pre><code class="js">if('serviceWorker' in navigator) {
  461. navigator.serviceWorker
  462. .register('sw.js', { scope: './'})
  463. .then(function(registration) {
  464. console.log('Service worker registered!');
  465. })
  466. .catch(function(error) {
  467. console.log('There was an error!');
  468. });
  469. }
  470. </code></pre>
  471. <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>
  472. <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>
  473. <h2>Offline-First Apps with UpUp</h2>
  474. <p>The first step is to create our app just as we would normally.</p>
  475. <pre><code class="html"> &lt;!-- index.html --&gt;
  476. ...
  477. &lt;nav class="navbar navbar-default"&gt;
  478. &lt;div class="container-fluid"&gt;
  479. &lt;a class="navbar-brand"&gt;UpUp App&lt;/a&gt;
  480. &lt;/div&gt;
  481. &lt;/nav&gt;
  482. &lt;div class="container"&gt;
  483. &lt;form&gt;
  484. &lt;div class="form-group"&gt;
  485. &lt;label&gt;New Todo&lt;/label&gt;
  486. &lt;input type="text" class="form-control" placeholder="Enter a new todo"&gt;
  487. &lt;/div&gt;
  488. &lt;button type="submit" class="btn btn-default"&gt;Submit&lt;/button&gt;
  489. &lt;/form&gt;
  490. &lt;/div&gt;
  491. ...
  492. </code></pre>
  493. <p><img src="https://cdn.auth0.com/blog/offline-first/offline-first-2.png" alt="offline-first upup"/></p>
  494. <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>
  495. <pre><code class="html"> &lt;!-- index.html --&gt;
  496. ...
  497. &lt;script src="upup.min.js"&gt;&lt;/script&gt;
  498. &lt;script&gt;
  499. UpUp.start({
  500. 'content-url': 'templates/offline.html',
  501. 'assets': [
  502. 'css/bootstrap.min.css'
  503. // Other assets like images, JS libraries etc
  504. ]
  505. });
  506. &lt;/script&gt;
  507. ...
  508. </code></pre>
  509. <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>
  510. <pre><code class="html"> &lt;!-- templates/offline.html --&gt;
  511. ...
  512. &lt;div class="alert alert-danger"&gt;
  513. You are currently offline, but you can keep working.
  514. &lt;/div&gt;
  515. ...
  516. </code></pre>
  517. <p><img src="https://cdn.auth0.com/blog/offline-first/offline-first-3.png" alt="offline-first upup"/></p>
  518. <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>
  519. <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>
  520. <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>
  521. <pre><code class="js">// src/upup.sw.js
  522. self.addEventListener('fetch', function(event) {
  523. event.respondWith(
  524. // try to return untouched request from network first
  525. fetch(event.request.url, { mode: 'no-cors' }).catch(function() {
  526. // if it fails, try to return request from the cache
  527. return caches.match(event.request).then(function(response) {
  528. if (response) {
  529. return response;
  530. }
  531. // if not found in cache, return default offline content
  532. if (event.request.headers.get('accept').includes('text/html')) {
  533. return caches.match('sw-offline-content');
  534. }
  535. })
  536. })
  537. );
  538. });
  539. </code></pre>
  540. <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>
  541. <p>The service worker itself is wired up with the <code>start</code> method we saw earlier.</p>
  542. <pre><code class="js">// upup.js
  543. start: function(settings) {
  544. this.addSettings(settings);
  545. // register the service worker
  546. _serviceWorker.register(_settings.script, {scope: './'}).then(function(registration) {
  547. // Registration was successful
  548. if (_debugState) {
  549. console.log('ServiceWorker registration successful with scope: %c'+registration.scope, _debugStyle);
  550. }
  551. ...
  552. </code></pre>
  553. <h2>What About Data?</h2>
  554. <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>
  555. <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>
  556. <h3>PouchDB</h3>
  557. <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>
  558. <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>
  559. <pre><code class="js">
  560. // Local databases are created by just providing a name
  561. var local = new PouchDB('todos');
  562. // Remote databases are created by providing a path to CouchDB
  563. var remote = new PouchDB('http://localhost:5984/todos');
  564. </code></pre>
  565. <p>PouchDB uses a simple promise-based API for storing and retrieving documents.</p>
  566. <pre><code class="js">
  567. // Store a document
  568. var todo = {
  569. "_id": "todo1",
  570. "name": "Go to the store"
  571. }
  572. local.put(todo);
  573. // Retrieve a document
  574. local.get('todo1').then(function(data) {
  575. console.log(data);
  576. }).catch(function(error) {
  577. console.log('There was an error');
  578. });
  579. </code></pre>
  580. <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>
  581. <pre><code class="js">local.sync(remote, {
  582. live: true,
  583. retry: true
  584. });
  585. </code></pre>
  586. <h2>Aside: Authentication is Easy with Auth0</h2>
  587. <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>
  588. <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>
  589. <pre><code class="js">// Initialize Auth0Lock with your `clientID` and `domain`
  590. var lock = new Auth0Lock('xxxxxx', '&lt;account&gt;.auth0.com');
  591. // and deploy it
  592. var login = document.querySelector('a#login')
  593. login.onclick = function (e) {
  594. e.preventDefault();
  595. lock.show(function onLogin(err, profile, id_token) {
  596. if (err) {
  597. // There was an error logging the user in
  598. return alert(err.message);
  599. }
  600. // User is logged in
  601. });
  602. };
  603. </code></pre>
  604. <p><img src="https://i.cloudup.com/6opoEX_Z9z.png" alt="lock auth0"/></p>
  605. <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>
  606. <h2>Wrapping Up</h2>
  607. <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>
  608. <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>
  609. </article>
  610. </section>
  611. <nav id="jumpto">
  612. <p>
  613. <a href="/david/blog/">Accueil du blog</a> |
  614. <a href="https://auth0.com/blog/2015/10/30/creating-offline-first-web-apps-with-service-workers/">Source originale</a> |
  615. <a href="/david/stream/2019/">Accueil du flux</a>
  616. </p>
  617. </nav>
  618. <footer>
  619. <div>
  620. <img src="/static/david/david-larlet-avatar.jpg" loading="lazy" class="avatar" width="200" height="200">
  621. <p>
  622. Bonjour/Hi!
  623. Je suis <a href="/david/" title="Profil public">David&nbsp;Larlet</a>, je vis actuellement à Montréal et j’alimente cet espace depuis 15 ans. <br>
  624. Si tu as apprécié cette lecture, n’hésite pas à poursuivre ton exploration. Par exemple via les <a href="/david/blog/" title="Expériences bienveillantes">réflexions bimestrielles</a>, la <a href="/david/stream/2019/" title="Pensées (dés)articulées">veille hebdomadaire</a> ou en t’abonnant au <a href="/david/log/" title="S’abonner aux publications via RSS">flux RSS</a> (<a href="/david/blog/2019/flux-rss/" title="Tiens c’est quoi un flux RSS ?">so 2005</a>).
  625. </p>
  626. <p>
  627. Je m’intéresse à la place que je peux avoir dans ce monde. En tant qu’humain, en tant que membre d’une famille et en tant qu’associé d’une coopérative. De temps en temps, je fais aussi des <a href="https://github.com/davidbgk" title="Principalement sur Github mais aussi ailleurs">trucs techniques</a>. Et encore plus rarement, <a href="/david/talks/" title="En ce moment je laisse plutôt la place aux autres">j’en parle</a>.
  628. </p>
  629. <p>
  630. Voici quelques articles choisis :
  631. <a href="/david/blog/2019/faire-equipe/" title="Accéder à l’article complet">Faire équipe</a>,
  632. <a href="/david/blog/2018/bivouac-automnal/" title="Accéder à l’article complet">Bivouac automnal</a>,
  633. <a href="/david/blog/2018/commodite-effondrement/" title="Accéder à l’article complet">Commodité et effondrement</a>,
  634. <a href="/david/blog/2017/donnees-communs/" title="Accéder à l’article complet">Des données aux communs</a>,
  635. <a href="/david/blog/2016/accompagner-enfant/" title="Accéder à l’article complet">Accompagner un enfant</a>,
  636. <a href="/david/blog/2016/senior-developer/" title="Accéder à l’article complet">Senior developer</a>,
  637. <a href="/david/blog/2016/illusion-sociale/" title="Accéder à l’article complet">L’illusion sociale</a>,
  638. <a href="/david/blog/2016/instantane-scopyleft/" title="Accéder à l’article complet">Instantané Scopyleft</a>,
  639. <a href="/david/blog/2016/enseigner-web/" title="Accéder à l’article complet">Enseigner le Web</a>,
  640. <a href="/david/blog/2016/simplicite-defaut/" title="Accéder à l’article complet">Simplicité par défaut</a>,
  641. <a href="/david/blog/2016/minimalisme-esthetique/" title="Accéder à l’article complet">Minimalisme et esthétique</a>,
  642. <a href="/david/blog/2014/un-web-omni-present/" title="Accéder à l’article complet">Un web omni-présent</a>,
  643. <a href="/david/blog/2014/manifeste-developpeur/" title="Accéder à l’article complet">Manifeste de développeur</a>,
  644. <a href="/david/blog/2013/confort-convivialite/" title="Accéder à l’article complet">Confort et convivialité</a>,
  645. <a href="/david/blog/2013/testament-numerique/" title="Accéder à l’article complet">Testament numérique</a>,
  646. et <a href="/david/blog/" title="Accéder aux archives">bien d’autres…</a>
  647. </p>
  648. <p>
  649. On peut <a href="mailto:david%40larlet.fr" title="Envoyer un courriel">échanger par courriel</a>. Si éventuellement tu souhaites que l’on travaille ensemble, tu devrais commencer par consulter le <a href="http://larlet.com">profil dédié à mon activité professionnelle</a> et/ou contacter directement <a href="http://scopyleft.fr/">scopyleft</a>, la <abbr title="Société coopérative et participative">SCOP</abbr> dont je fais partie depuis six ans. Je recommande au préalable de lire <a href="/david/blog/2018/cout-site/" title="Attention ce qui va suivre peut vous choquer">combien coûte un site</a> et pourquoi je suis plutôt favorable à une <a href="/david/pro/devis/" title="Discutons-en !">non-demande de devis</a>.
  650. </p>
  651. <p>
  652. Je ne traque pas ta navigation mais mon
  653. <abbr title="Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33.184162340">hébergeur</abbr>
  654. conserve des logs d’accès.
  655. </p>
  656. </div>
  657. </footer>
  658. <script type="text/javascript">
  659. ;(_ => {
  660. const jumper = document.getElementById('jumper')
  661. jumper.addEventListener('click', e => {
  662. e.preventDefault()
  663. const anchor = e.target.getAttribute('href')
  664. const targetEl = document.getElementById(anchor.substring(1))
  665. targetEl.scrollIntoView({behavior: 'smooth'})
  666. })
  667. })()
  668. </script>