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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912
  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>An Offline Experience 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://brandonrozek.com/2015/11/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. An Offline Experience 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://brandonrozek.com/2015/11/service-workers/">Source originale du contenu</a></h3>
  445. <p>I’m excited to say that I’ve written my first service worker for brandonrozek.com. What is a service worker? A service worker provides an extra layer between the client and the server. The exciting part about this is that you can use service workers to deliver an offline experience. (Cached versions of your site, offline pages, etc.)</p>
  446. <p>Service workers are currently supported in Chrome, Opera, and Firefox nightly. You don’t have to worry too much about browser support because the Service Worker spec was written in a <a href="http://alistapart.com/article/understandingprogressiveenhancement">progressively enchanced</a> way meaning it won’t break your existing site :)</p>
  447. <h2 id="caveats"><a href="#caveats" name="caveats"></a>Caveats</h2>
  448. <p>You need HTTPS to be able to use service workers on your site. This is mainly for security reasons. Imagine if a third party can control all of the networking requests on your site?</p>
  449. <p>If you don’t want to go out and buy a SSL Certificate, there are a couple free ways to go about this.<br/>
  450. 1) <a href="https://www.cloudflare.com/">Cloudflare</a><br/>
  451. 2) <a href="https://letsencrypt.org/">Let’s Encrypt</a></p>
  452. <p>Service workers are promise heavy. Promises contain a then clause which runs code asynchronously. If you’re not accustomed to this idea please check out this <a href="https://ponyfoo.com/articles/es6-promises-in-depth">post</a> by Nicolas Bevacqua.</p>
  453. <p>Now onto making the service worker! If you want to skip to the final code scroll down to the bottom. Unless you don’t like my syntax highlighting, then you can check out this <a href="https://gist.github.com/brandonrozek/0cf038df40a913fda655">gist</a>.</p>
  454. <h2 id="register-the-service-worker"><a href="#register-the-service-worker" name="register-the-service-worker"></a>Register the service worker</h2>
  455. <p>Place <code>service-worker.js</code> on the root of your site. This is so the service worker can access all the files in the site.</p>
  456. <p>Then in your main javascript file, register the service worker.</p>
  457. <pre><code class='language-javascript' rel='Script.js'>
  458. if (navigator.serviceWorker) {
  459. navigator.serviceWorker.register('/serviceworker.js', {
  460. scope: '/'
  461. });
  462. }
  463. </code></pre>
  464. <h2 id="install-the-service-worker"><a href="#install-the-service-worker" name="install-the-service-worker"></a>Install the service worker</h2>
  465. <p>The first time the service worker runs, it emits the <code>install</code> event. At this time, we can load the visitor’s cache with some resources for when they’re offline.</p>
  466. <p>Every so often, I like to change up the theme of the site. So I have version numbers attached to my files. I would also like to invalidate my cache with this version number. So at the top of the file I added</p>
  467. <pre><code rel='service-worker.js' class='language-javascript'>
  468. var version = 'v2.0.24:';
  469. </code></pre>
  470. <p>Now, to specify which files I want the service worker to cache for offline use. I thought my home page and my offline page would be good enough.</p>
  471. <pre><code rel='service-worker.js' class='language-javascript'>
  472. var offlineFundamentals = [
  473. '/',
  474. '/offline/'
  475. ];
  476. </code></pre>
  477. <p>Since <code class='language-javascript'>cache.addAll()</code> hasn’t been implemented yet in any of the browsers, and the polyfill implementation didn’t work for my needs. I pieced together my own.</p>
  478. <pre><code rel='service-worker.js' class='language-javascript'>
  479. var updateStaticCache = function() {
  480. return caches.open(version + 'fundamentals').then(function(cache) {
  481. return Promise.all(offlineFundamentals.map(function(value) {
  482. var request = new Request(value);
  483. var url = new URL(request.url);
  484. if (url.origin != location.origin) {
  485. request = new Request(value, {mode: 'no-cors'});
  486. }
  487. return fetch(request).then(function(response) {
  488. var cachedCopy = response.clone();
  489. return cache.put(request, cachedCopy);
  490. });
  491. }))
  492. })
  493. };
  494. </code></pre>
  495. <p>Let’s go through this chunk of code.</p>
  496. <ol>
  497. <li>Open the cache called <code class='language-javascript'>'v2.0.24:fundamentals'</code></li>
  498. <li>Go through all of the <code class='language-javascript'>offlineFundamental</code>‘s URLs</li>
  499. <ul>
  500. <li>Does the file I ask for come from the same domain as my site?
  501. <li>No. Then, make the request ‘no-cors’ (I had difficulty getting my asset files in cors mode. If the cors headers are included in the response, then you can take out this line)</li>
  502. <li>Fetch the file from the network and then cache it.</li>
  503. </ul>
  504. </ol>
  505. <p>Now we call it when the <code>install</code> event is fired.</p>
  506. <pre><code rel='service-worker.js' class='language-javascript'>
  507. self.addEventListener("install", function(event) {
  508. event.waitUntil(updateStaticCache())
  509. })
  510. </code></pre>
  511. <p>With this we now cached all the files in the offlineFundamentals array during the install step.</p>
  512. <h2 id="clear-out-the-old-cache"><a href="#clear-out-the-old-cache" name="clear-out-the-old-cache"></a>Clear out the old cache</h2>
  513. <p>Since we’re caching everything. If you change one of the files, your visitor wouldn’t get the changed file. Wouldn’t it be nice to remove old files from the visitor’s cache?</p>
  514. <p>Every time the service worker finishes the install step, it releases an <code>activate</code> event. We can use this to look and see if there are any old cache containers on the visitor’s computer.</p>
  515. <p>From <a href="https://ponyfoo.com/articles/serviceworker-revolution">Nicolas’ code</a>. Thanks for sharing :)</p>
  516. <pre><code rel='service-worker.js' class='language-javascript'>
  517. var clearOldCaches = function() {
  518. return caches.keys().then(function(keys) {
  519. return Promise.all(
  520. keys
  521. .filter(function (key) {
  522. return key.indexOf(version) != 0;
  523. })
  524. .map(function (key) {
  525. return caches.delete(key);
  526. })
  527. );
  528. })
  529. }
  530. </code></pre>
  531. <ol>
  532. <li>Check the names of each of the cache containers</li>
  533. <li>If they don’t start with the correct version number
  534. <ul>
  535. <li>Delete that cache container</li>
  536. </ul>
  537. </ol>
  538. <p>Call the function when the <code>activate</code> event fires.</p>
  539. <pre><code rel='service-worker.js' class='language-javascript'>
  540. self.addEventListener("activate", function(event) {
  541. event.waitUntil(clearOldCaches())
  542. });
  543. </code></pre>
  544. <h2 id="intercepting-fetch-requests"><a href="#intercepting-fetch-requests" name="intercepting-fetch-requests"></a>Intercepting fetch requests</h2>
  545. <p>The cool thing about service worker’s is that it can handle file requests. We could cache all files requested for offline use, and if a fetch for a resource failed, then the service worker can look for it in the cache or provide an alternative.</p>
  546. <p>This is a large section, so I’m going to attempt to break it down as much as I can.</p>
  547. <h3 id="limit-the-cache"><a href="#limit-the-cache" name="limit-the-cache"></a>Limit the cache</h3>
  548. <p>If the visitor started browsing all of the pages on my site, his or her cache would start to get bloated with files. To not burden my visitors, I decided to only keep the latest 25 pages and latest 10 images in the cache.</p>
  549. <pre><code rel='service-worker.js' class='language-javascript'>
  550. var limitCache = function(cache, maxItems) {
  551. cache.keys().then(function(items) {
  552. if (items.length &gt; maxItems) {
  553. cache.delete(items[0]);
  554. }
  555. })
  556. }
  557. </code></pre>
  558. <p>We’ll call it later in the code.</p>
  559. <h3 id="fetch-from-network-and-cache"><a href="#fetch-from-network-and-cache" name="fetch-from-network-and-cache"></a>Fetch from network and cache</h3>
  560. <p>Every time I fetch a file from the network I throw it into a specific cache container. <code class='language-javascript'>'pages'</code> for HTML files, <code class='language-javascript'>'images'</code> for CSS files, and <code class='language-javascript'>'assets'</code> for any other file. This is so I can handle the cache limiting above easier.</p>
  561. <p>Defined within the <code>fetch</code> event.</p>
  562. <pre><code rel='service-worker.js' class='language-javascript'>
  563. var fetchFromNetwork = function(response) {
  564. var cacheCopy = response.clone();
  565. if (event.request.headers.get('Accept').indexOf('text/html') != -1) {
  566. caches.open(version + 'pages').then(function(cache) {
  567. cache.put(event.request, cacheCopy).then(function() {
  568. limitCache(cache, 25);
  569. })
  570. });
  571. } else if (event.request.headers.get('Accept').indexOf('image') != -1) {
  572. caches.open(version + 'images').then(function(cache) {
  573. cache.put(event.request, cacheCopy).then(function() {
  574. limitCache(cache, 10);
  575. });
  576. });
  577. } else {
  578. caches.open(version + 'assets').then(function add(cache) {
  579. cache.put(event.request, cacheCopy);
  580. });
  581. }
  582. return response;
  583. }
  584. </code></pre>
  585. <h3 id="when-the-network-fails"><a href="#when-the-network-fails" name="when-the-network-fails"></a>When the network fails</h3>
  586. <p>There are going to be times where the visitor cannot access the website. Maybe they went in a tunnel while they were riding a train? Or maybe your site went down.</p>
  587. <p>I thought it would be nice for my reader’s to be able to look over my blog posts again regardless of an internet connection. So I provide a fall-back.</p>
  588. <p>Defined within the <code>fetch</code> event.</p>
  589. <pre><code rel='service-worker.js' class='language-javascript'>
  590. var fallback = function() {
  591. if (event.request.headers.get('Accept').indexOf('text/html') != -1) {
  592. return caches.match(event.request).then(function (response) {
  593. return response || caches.match('/offline/');
  594. })
  595. } else if (event.request.headers.get('Accept').indexOf('image') != -1) {
  596. return new Response('&lt;svg width="400" height="300" role="img" aria-labelledby="offline-title" viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg"&gt;&lt;title id="offline-title"&gt;Offline&lt;/title&gt;&lt;g fill="none" fill-rule="evenodd"&gt;&lt;path fill="#D8D8D8" d="M0 0h400v300H0z"/&gt;&lt;text fill="#9B9B9B" font-family="Helvetica Neue,Arial,Helvetica,sans-serif" font-size="72" font-weight="bold"&gt;&lt;tspan x="93" y="172"&gt;offline&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;/svg&gt;', { headers: { 'Content-Type': 'image/svg+xml' }});
  597. }
  598. }
  599. </code></pre>
  600. <ol>
  601. <li>Is the request for a HTML file?
  602. <ul>
  603. <li>Show the <a href="https://brandonrozek.com/offline/">offline</a> page.</li>
  604. </ul>
  605. <li>Is the request for an image?
  606. <ul>
  607. <li>Show a place-holder image (Courtesy of <a href="https://adactio.com/journal/9775">Jeremy Keith</a>)</li>
  608. </ul>
  609. </ol>
  610. <h3 id="handle-the-request"><a href="#handle-the-request" name="handle-the-request"></a>Handle the request</h3>
  611. <p>First off, I’m only handling GET requests.</p>
  612. <pre><code rel='service-worker.js' class='language-javascript'>
  613. if (event.request.method != 'GET') {
  614. return;
  615. }
  616. </code></pre>
  617. <p>For HTML files, grab the file from the network. If that fails, then look for it in the cache.<br/>
  618. <em>Network then cache strategy<br/>
  619. </em></p>
  620. <pre><code rel='service-worker.js' class='language-javascript'>
  621. if (event.request.headers.get('Accept').indexOf('text/html') != -1) {
  622. event.respondWith(fetch(event.request).then(fetchFromNetwork, fallback));
  623. return;
  624. }
  625. </code></pre>
  626. <p>For non-HTML files, follow this series of steps</p>
  627. <ol>
  628. <li>Check the cache</li>
  629. <li>Does a cache exist for this file?</li>
  630. <ul>
  631. <li>Yes. Then show it</li>
  632. <li>No. Then grab it from the network and cache it.</li>
  633. </ul>
  634. </ol>
  635. <p><em>Cache then network strategy<br/>
  636. </em></p>
  637. <pre><code rel='service-worker.js' class='language-javascript'>
  638. event.respondWith(
  639. caches.match(event.request).then(function(cached) {
  640. return cached || fetch(event.request).then(fetchFromNetwork, fallback);
  641. })
  642. )
  643. </code></pre>
  644. <p>For different stategy’s, take a look at Jake Archibald’s <a href="https://jakearchibald.com/2014/offline-cookbook/">offline cookbook</a>.</p>
  645. <h2 id="conclusion"><a href="#conclusion" name="conclusion"></a>Conclusion</h2>
  646. <p>With all of that, we now have a fully functioning offline-capable website! I wouldn’t be able to implement this myself if it wasn’t for some of the awesome people I mentioned earlier sharing their experience. So share, share, share!</p>
  647. <p>With that sentiment, I’ll now share the full code for <code>service-worker.js</code></p>
  648. <pre><code rel='service-worker.js' class='language-javascript'>
  649. var version = 'v2.0.24:';
  650. var offlineFundamentals = [
  651. '/',
  652. '/offline/'
  653. ];
  654. //Add core website files to cache during serviceworker installation
  655. var updateStaticCache = function() {
  656. return caches.open(version + 'fundamentals').then(function(cache) {
  657. return Promise.all(offlineFundamentals.map(function(value) {
  658. var request = new Request(value);
  659. var url = new URL(request.url);
  660. if (url.origin != location.origin) {
  661. request = new Request(value, {mode: 'no-cors'});
  662. }
  663. return fetch(request).then(function(response) {
  664. var cachedCopy = response.clone();
  665. return cache.put(request, cachedCopy);
  666. });
  667. }))
  668. })
  669. };
  670. //Clear caches with a different version number
  671. var clearOldCaches = function() {
  672. return caches.keys().then(function(keys) {
  673. return Promise.all(
  674. keys
  675. .filter(function (key) {
  676. return key.indexOf(version) != 0;
  677. })
  678. .map(function (key) {
  679. return caches.delete(key);
  680. })
  681. );
  682. })
  683. }
  684. /*
  685. limits the cache
  686. If cache has more than maxItems then it removes the first item in the cache
  687. */
  688. var limitCache = function(cache, maxItems) {
  689. cache.keys().then(function(items) {
  690. if (items.length &gt; maxItems) {
  691. cache.delete(items[0]);
  692. }
  693. })
  694. }
  695. //When the service worker is first added to a computer
  696. self.addEventListener("install", function(event) {
  697. event.waitUntil(updateStaticCache())
  698. })
  699. //Service worker handles networking
  700. self.addEventListener("fetch", function(event) {
  701. //Fetch from network and cache
  702. var fetchFromNetwork = function(response) {
  703. var cacheCopy = response.clone();
  704. if (event.request.headers.get('Accept').indexOf('text/html') != -1) {
  705. caches.open(version + 'pages').then(function(cache) {
  706. cache.put(event.request, cacheCopy).then(function() {
  707. limitCache(cache, 25);
  708. })
  709. });
  710. } else if (event.request.headers.get('Accept').indexOf('image') != -1) {
  711. caches.open(version + 'images').then(function(cache) {
  712. cache.put(event.request, cacheCopy).then(function() {
  713. limitCache(cache, 10);
  714. });
  715. });
  716. } else {
  717. caches.open(version + 'assets').then(function add(cache) {
  718. cache.put(event.request, cacheCopy);
  719. });
  720. }
  721. return response;
  722. }
  723. //Fetch from network failed
  724. var fallback = function() {
  725. if (event.request.headers.get('Accept').indexOf('text/html') != -1) {
  726. return caches.match(event.request).then(function (response) {
  727. return response || caches.match('/offline/');
  728. })
  729. } else if (event.request.headers.get('Accept').indexOf('image') != -1) {
  730. return new Response('&lt;svg width="400" height="300" role="img" aria-labelledby="offline-title" viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg"&gt;&lt;title id="offline-title"&gt;Offline&lt;/title&gt;&lt;g fill="none" fill-rule="evenodd"&gt;&lt;path fill="#D8D8D8" d="M0 0h400v300H0z"/&gt;&lt;text fill="#9B9B9B" font-family="Helvetica Neue,Arial,Helvetica,sans-serif" font-size="72" font-weight="bold"&gt;&lt;tspan x="93" y="172"&gt;offline&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;/svg&gt;', { headers: { 'Content-Type': 'image/svg+xml' }});
  731. }
  732. }
  733. //This service worker won't touch non-get requests
  734. if (event.request.method != 'GET') {
  735. return;
  736. }
  737. //For HTML requests, look for file in network, then cache if network fails.
  738. if (event.request.headers.get('Accept').indexOf('text/html') != -1) {
  739. event.respondWith(fetch(event.request).then(fetchFromNetwork, fallback));
  740. return;
  741. }
  742. //For non-HTML requests, look for file in cache, then network if no cache exists.
  743. event.respondWith(
  744. caches.match(event.request).then(function(cached) {
  745. return cached || fetch(event.request).then(fetchFromNetwork, fallback);
  746. })
  747. )
  748. });
  749. //After the install event
  750. self.addEventListener("activate", function(event) {
  751. event.waitUntil(clearOldCaches())
  752. });
  753. </code></pre>
  754. </article>
  755. </section>
  756. <nav id="jumpto">
  757. <p>
  758. <a href="/david/blog/">Accueil du blog</a> |
  759. <a href="https://brandonrozek.com/2015/11/service-workers/">Source originale</a> |
  760. <a href="/david/stream/2019/">Accueil du flux</a>
  761. </p>
  762. </nav>
  763. <footer>
  764. <div>
  765. <img src="/static/david/david-larlet-avatar.jpg" loading="lazy" class="avatar" width="200" height="200">
  766. <p>
  767. Bonjour/Hi!
  768. 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>
  769. 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>).
  770. </p>
  771. <p>
  772. 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>.
  773. </p>
  774. <p>
  775. Voici quelques articles choisis :
  776. <a href="/david/blog/2019/faire-equipe/" title="Accéder à l’article complet">Faire équipe</a>,
  777. <a href="/david/blog/2018/bivouac-automnal/" title="Accéder à l’article complet">Bivouac automnal</a>,
  778. <a href="/david/blog/2018/commodite-effondrement/" title="Accéder à l’article complet">Commodité et effondrement</a>,
  779. <a href="/david/blog/2017/donnees-communs/" title="Accéder à l’article complet">Des données aux communs</a>,
  780. <a href="/david/blog/2016/accompagner-enfant/" title="Accéder à l’article complet">Accompagner un enfant</a>,
  781. <a href="/david/blog/2016/senior-developer/" title="Accéder à l’article complet">Senior developer</a>,
  782. <a href="/david/blog/2016/illusion-sociale/" title="Accéder à l’article complet">L’illusion sociale</a>,
  783. <a href="/david/blog/2016/instantane-scopyleft/" title="Accéder à l’article complet">Instantané Scopyleft</a>,
  784. <a href="/david/blog/2016/enseigner-web/" title="Accéder à l’article complet">Enseigner le Web</a>,
  785. <a href="/david/blog/2016/simplicite-defaut/" title="Accéder à l’article complet">Simplicité par défaut</a>,
  786. <a href="/david/blog/2016/minimalisme-esthetique/" title="Accéder à l’article complet">Minimalisme et esthétique</a>,
  787. <a href="/david/blog/2014/un-web-omni-present/" title="Accéder à l’article complet">Un web omni-présent</a>,
  788. <a href="/david/blog/2014/manifeste-developpeur/" title="Accéder à l’article complet">Manifeste de développeur</a>,
  789. <a href="/david/blog/2013/confort-convivialite/" title="Accéder à l’article complet">Confort et convivialité</a>,
  790. <a href="/david/blog/2013/testament-numerique/" title="Accéder à l’article complet">Testament numérique</a>,
  791. et <a href="/david/blog/" title="Accéder aux archives">bien d’autres…</a>
  792. </p>
  793. <p>
  794. 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>.
  795. </p>
  796. <p>
  797. Je ne traque pas ta navigation mais mon
  798. <abbr title="Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33.184162340">hébergeur</abbr>
  799. conserve des logs d’accès.
  800. </p>
  801. </div>
  802. </footer>
  803. <script type="text/javascript">
  804. ;(_ => {
  805. const jumper = document.getElementById('jumper')
  806. jumper.addEventListener('click', e => {
  807. e.preventDefault()
  808. const anchor = e.target.getAttribute('href')
  809. const targetEl = document.getElementById(anchor.substring(1))
  810. targetEl.scrollIntoView({behavior: 'smooth'})
  811. })
  812. })()
  813. </script>