offline – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Tue, 07 Jun 2022 20:06:33 +0000 en-US hourly 1 https://wordpress.org/?v=6.1.1 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 offline – CSS-Tricks https://css-tricks.com 32 32 45537868 Push and ye shall receive https://css-tricks.com/push-and-ye-shall-receive/ https://css-tricks.com/push-and-ye-shall-receive/#comments Tue, 20 Nov 2018 14:42:26 +0000 http://css-tricks.com/?p=278820 Sometimes the seesaw of web tech is fascinating. Service workers have arrived, and beyond offline networking (read Jeremy’s book) which is possibly their best feature, they can enable push notifications via the Push API.

I totally get …


Push and ye shall receive originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Sometimes the seesaw of web tech is fascinating. Service workers have arrived, and beyond offline networking (read Jeremy’s book) which is possibly their best feature, they can enable push notifications via the Push API.

I totally get the push (pun intended) to make that happen. There is an omnipresent sentiment that we want the web to win, as there should be in this industry. Losing on the web means losing to native apps on all the different platforms out there. Native apps aren’t evil or anything — they are merely competitive and exclusionary in a way the web isn’t. Making the web a viable platform for any type of “app” is a win for us and a win for humans.

One of the things native apps do well is push notifications which gives them a competitive advantage. Some developers choose native for stuff like that. But now that we actually have them on the web, there is pushback from the community and even from the browsers themselves. Firefox supports them, then rolled out a user setting to entirely block them.

We’re seeing articles like Moses Kim’s Don’t @ me:

Push notifications are a classic example of good UX intentions gone bad because we know no bounds.

Very few people are singing the praises of push notifications. And yet! Jeremy Keith wrote up a great experiment by Sebastiaan Andeweg. Rather than an obnoxious and intrusive push notification…

Here’s what Sebastiaan wanted to investigate: what if that last step weren’t so intrusive? Here’s the alternate flow he wanted to test:

  1. A website prompts the user for permission to send push notifications.
  2. The user grants permission.
  3. A whole lot of complicated stuff happens behinds the scenes.
  4. Next time the website publishes something relevant, it fires a push message containing the details of the new URL.
  5. The user’s service worker receives the push message (even if the site isn’t open).
  6. The service worker fetches the contents of the URL provided in the push message and caches the page. Silently.

It worked.

Imagine a PWA podcast app that works offline and silently receives and caches new podcasts. Sweet. Now we need a permissions model that allows for silent notifications.

Update: Here’s Sebastiaan Andeweg’s follow up article PushAPI without Notifications where he goes into the thinking, code, and demo behind all this.


Push and ye shall receive originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/push-and-ye-shall-receive/feed/ 2 278820
Service Worker Cookbook https://css-tricks.com/service-worker-cookbook/ Fri, 25 May 2018 20:54:50 +0000 http://css-tricks.com/?p=271633 I stumbled upon this site the other day from Mozilla that’s a collection of recipes to get started with a Service Worker — from caching strategies and notifications to providing an offline fallback to your users, this little cookbook has …


Service Worker Cookbook originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I stumbled upon this site the other day from Mozilla that’s a collection of recipes to get started with a Service Worker — from caching strategies and notifications to providing an offline fallback to your users, this little cookbook has it all.

You can also check out our guide to making a simple site work offline and the offline site that resulted from it.

To Shared LinkPermalink on CSS-Tricks


Service Worker Cookbook originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
271633
Offline *Only* Viewing https://css-tricks.com/offline-only-viewing/ https://css-tricks.com/offline-only-viewing/#comments Thu, 08 Feb 2018 19:42:46 +0000 http://css-tricks.com/?p=266339 It made the rounds a while back that Chris Bolin built a page of his personal website that could only be viewed while you are offline. Now he has a whole magazine around this same concept called The Disconnect!


Offline *Only* Viewing originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
It made the rounds a while back that Chris Bolin built a page of his personal website that could only be viewed while you are offline.

This page itself is an experiment in that vein: What if certain content required us to disconnect? What if readers had access to that glorious focus that makes devouring a novel for hours at a time so satisfying? What if creators could pair that with the power of modern devices? Our phones and laptops are amazing platforms for inventive content—if only we could harness our own attention.

Now Bolin has a whole magazine around this same concept called The Disconnect!

The Disconnect is an offline-only, digital magazine of commentary, fiction, and poetry. Each issue forces you to disconnect from the internet, giving you a break from constant distractions and relentless advertisements.

I believe it’s some Service Worker trickery to serve different files depending on the state of the network. Usually, Service Workers are meant to serve cached files when the network is off or slow such as to make the website continue to work. This flips that logic on its head, preventing files from being served until the network is off.


Offline *Only* Viewing originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/offline-only-viewing/feed/ 4 266339
Making your web app work offline, Part 2: The Implementation https://css-tricks.com/making-web-app-work-offline-part-2-implementation/ https://css-tricks.com/making-web-app-work-offline-part-2-implementation/#comments Thu, 07 Dec 2017 14:33:39 +0000 http://css-tricks.com/?p=263437 This two-part series is a gentle, high-level introduction to offline web development. In Part 1 we got a basic service worker running, which caches our application resources. Now let’s extend it to support offline.

Article Series:

  1. The Setup
  2. The Implementation


Making your web app work offline, Part 2: The Implementation originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
This two-part series is a gentle, high-level introduction to offline web development. In Part 1 we got a basic service worker running, which caches our application resources. Now let’s extend it to support offline.

Article Series:

  1. The Setup
  2. The Implementation (you are here!)

Making an `offline.htm` file

Next, lets add some code to detect when the application is offline, and if so, redirect our users to a (cached) `offline.htm`.

But wait, if the service worker file is generated automatically, how do we go about adding in our own code, manually? Well, we can add an entry for importScripts, which tells our service worker to import the scripts we specify. It does this through the service worker’s native importScripts function, which is well-named. And we’ll also add our `offline.htm` file to our statically cached list of files. The new files are highlighted below:

new SWPrecacheWebpackPlugin({
    mergeStaticsConfig: true,
    filename: "service-worker.js",
    importScripts: ["../sw-manual.js"], 
    staticFileGlobs: [
      //...
      "offline.htm"
    ],
    // the rest of the config is unchanged
  })

Now, let’s go in our `sw-manual.js` file, and add code to load the cached `offline.htm` file when the user is offline.

toolbox.router.get(/books$/, handleMain);
toolbox.router.get(/subjects$/, handleMain);
toolbox.router.get(/localhost:3000\/$/, handleMain);
toolbox.router.get(/mylibrary.io$/, handleMain);

function handleMain(request) {
  return fetch(request).catch(() => {
    return caches.match("react-redux/offline.htm", { ignoreSearch: true });
  });
}

We’ll use the toolbox.router object we saw before to catch all our top-level routes, and if the main page doesn’t load from the network, send back the (hopefully cached) `offline.htm` file.

This is one of the few times in this post you’ll see promises being used directly, instead of with the async syntax, mainly because in this case it’s actually easier to just tack on a .catch(), rather than set up a try{} catch{} block.

The `offline.htm` file will be pretty basic, just some HTML that reads cached books from IndexedDB, and displays them in a rudimentary table. But before showing that, let’s walk through how to actually use IndexedDB (if you want to just see it now, it’s here)

Hello World, IndexedDB

IndexedDB is an in-browser database. It’s ideal for enabling offline functionality since it can be accessed without network connectivity, but it’s by no means limited to that.

The API pre-dates Promises, so it’s callback based. We’ll go through everything with the native API, but in practice, you’ll likely want to wrap and simplify it, either with your own helper methods which wrap the functionality with Promises, or with a third-party utility.

Let me repeat: the API for IndexedDB is awful. Here’s Jake Archibald saying he wouldn’t even teach it directly

We’ll still go over it because I really want you to see everything as it is, but please don’t let it scare you away. There’s plenty of simplifying abstractions out there, for example dexie and idb.

Setting up our database

Let’s add code to sw-manual that subscribes to the service worker’s activate event, and checks to see if we already have an IndexedDB setup; if not, we’ll create, and then fill it with data.

First, the creating bit.

self.addEventListener("activate", () => {
  //1 is the version of IDB we're opening
  let open = indexedDB.open("books", 1);

  //should only be called the first time, when version 1 does not exist
  open.onupgradeneeded = evt => {
    let db = open.result;
    //this callback should only ever be called upon creation of our IDB, when an upgrade is needed
    //for version 1, but to be doubly safe, and also to demonstrade this, we'll check to see
    //if the stores exist
    if (!db.objectStoreNames.contains("books") || !db.objectStoreNames.contains("syncInfo")) {
      if (!db.objectStoreNames.contains("books")) {
        let bookStore = db.createObjectStore("books", { keyPath: "_id" });
        bookStore.createIndex("imgSync", "imgSync", { unique: false });
      }
      if (!db.objectStoreNames.contains("syncInfo")) {
        db.createObjectStore("syncInfo", { keyPath: "id" });
        evt.target.transaction
          .objectStore("syncInfo")
          .add({ id: 1, lastImgSync: null, lastImgSyncStarted: null, lastLoadStarted: +new Date(), lastLoad: null });
      }
      evt.target.transaction.oncomplete = fullSync;
    }
  };
});

The code’s messy and manual; as I said, you’ll likely want to add some abstractions in practice. Some of the key points: we check for the objectStores (tables) we’ll be using, and create them as needed. Note that we can even create indexes, which we can see on the books store, with the imgSync index. We also create a syncInfo store (table) which we’ll use to store information on when we last synced our data, so we don’t pester our servers too frequently, asking for updates.

When the transaction has completed, at the very bottom, we call the fullSync method, which loads all our data. Let’s see what that looks like.

Performing an initial sync

Below is the relevant portion of the syncing code, which makes repeated calls to our endpoint to load our books, page by page, adding each result to IDB along the way. Again, this is using zero abstractions, so expect a lot of bloat.

See this GitHub gist for the full code, which includes some additional error handling, and code which runs when the last page is finished.

function fullSyncPage(db, page) {
  let pageSize = 50;
  doFetch("/book/offlineSync", { page, pageSize })
    .then(resp => resp.json())
    .then(resp => {
      if (!resp.books) return;
      let books = resp.books;
      let i = 0;
      putNext();

      function putNext() { //callback for an insertion, with indicators it hasn't had images cached yet
        if (i < pageSize) {
          let book = books[i++];
          let transaction = db.transaction("books", "readwrite");
          let booksStore = transaction.objectStore("books");
          //extend the book with the imgSync indicated, add it, and on success, do this for the next book
          booksStore.add(Object.assign(book, { imgSync: 0 })).onsuccess = putNext;
        } else {
          //either load the next page, or call loadDone()
        }
      }
    });
}

The putNext() function is where the real work is done. This serves as the callback for each successful insertion’s success. In real life we’d hopefully have a nice method that adds each book, wrapped in a promise, so we could do a simple for of loop, and await each insertion. But this is the “vanilla” solution or at least one of them.

We modify each book before inserting it, to set the imgSync property to 0, to indicate that this book has not had its image cached, yet.

And after we’ve exhausted the last page, and there are no more results, we call loadDone(), to set some metadata indicating the last time we did a full data sync.

In real life, this would be a good time to sync all those images, but let’s instead do it on-demand by the web app itself, in order to demonstrate another feature of service workers.

Communicating between the web app, and service worker

Let’s just pretend it would be a good idea to have the books’ covers load the next time the user visits our page when the service worker is running. Let’s have our web app send a message to the service worker, and we’ll have the service worker receive it, and then sync the book covers.

From our app code, we attempt to send a message to a running service worker, instructing it to sync images.

In the web app:

if ("serviceWorker" in navigator) {
  try {
    navigator.serviceWorker.controller.postMessage({ command: "sync-images" });
  } catch (er) {}
}

In `sw-manual.js`:

self.addEventListener("message", evt => {
  if (evt.data && evt.data.command == "sync-images") {
    let open = indexedDB.open("books", 1);

    open.onsuccess = evt => {
      let db = open.result;
      if (db.objectStoreNames.contains("books")) {
        syncImages(db);
      }
    };
  }
});

In sw-manual we have code to catch that message, and call the syncImages() method. Let’s look at that, next.

function syncImages(db) {
  let tran = db.transaction("books");
  let booksStore = tran.objectStore("books");
  let idx = booksStore.index("imgSync");
  let booksCursor = idx.openCursor(0);
  let booksToUpdate = [];

  //a cursor's onsuccess callback will fire for EACH item that's read from it
  booksCursor.onsuccess = evt => {
    let cursor = evt.target.result;
    //if (!cursor) means the cursor has been exhausted; there are no more results
    if (!cursor) return runIt();

    let book = cursor.value;
    booksToUpdate.push({ _id: book._id, smallImage: book.smallImage });
    //read the next item from the cursor
    cursor.continue();
  };

  async function runIt() {
    if (!booksToUpdate.length) return;

    for (let book of booksToUpdate) {
      try {
        //fetch, and cache the book's image 
        await preCacheBookImage(book);
        let tran = db.transaction("books", "readwrite");
        let booksStore = tran.objectStore("books");
        //now save the updated book - we'll wrap the IDB callback-based opertion in
        //a manual promise, so we can await it
        await new Promise(res => {
          let req = booksStore.get(book._id);
          req.onsuccess = ({ target: { result: bookToUpdate } }) => {
            bookToUpdate.imgSync = 1;
            booksStore.put(bookToUpdate);
            res();
          };
          req.onerror = () => res();
        });
      } catch (er) {
        console.log("ERROR", er);
      }
    }
  }
}

We’re cracking open the imageSync index from before, and reading all books that have a zero, which means they haven’t had their images sync’d yet. The booksCursor.onsuccess will be called over and over again, until there are no books left; I’m using this to put them all into an array, at which point I call the runIt() method, which runs through them, calling preCacheBookImage() for each. This method will cache the image, and if there are no unforeseen errors, update the book in IDB to indicate that imgSync is now 1.

If you’re wondering why in the world I’m going through the trouble to save all the books from the cursor into an array, before calling runIt(), rather than just walking through the results of the cursor, and caching and updating as I go, well — it turns out transactions in IndexedDB are a bit weird. They complete when you yield to the event loop unless you yield to the event loop in a method provided by the transaction. So if we leave the event loop to go do other things, like make a network request to pull down an image, then the cursor’s transaction will complete, and we’ll get an error if we try to continue reading from it later.

Manually updating the cache.

Let’s wrap this up, and look at the preCacheBookImage method which actually pulls down a cover image, and adds it to the relevant cache, (but only if it’s not there already.)

async function preCacheBookImage(book) {
  let smallImage = book.smallImage;
  if (!smallImage) return;

  let cachedImage = await caches.match(smallImage);
  if (cachedImage) return;

  if (/https:\/\/s3.amazonaws.com\/my-library-cover-uploads/.test(smallImage)) {
    let cache = await caches.open("local-images1");
    let img = await fetch(smallImage, { mode: "no-cors" });
    await cache.put(smallImage, img);
  }
}

If the book has no image, we’re done. Next, we check if it’s cached already — if so, we’re done. Lastly, we inspect the URL, and figure out which cache it belongs in.

The local-images1 cache name is the same from before, which we set up in our dynamic cache. If the image in question isn’t already there, we fetch it, and add it to cache. Each cache operation returns a promise, so the async/await syntax simplifies things nicely.

Testing it out

The way it’s set up, if we clear our service worker either in dev tools, below, or by just opening a fresh incognito window…

…then the first time we view our app, all our books will get saved to IndexedDB.

When we refresh, the image sync will happen. So if we start on a page that’s already pulling down these images, we’ll see our normal service worker saving them to cache (ahem, assuming we delay the ajax call to give our Service Worker a chance to install), which is what these events are in our network tab.

Then, if we navigate elsewhere and refresh, we won’t see any network requests for those image, since our sync method is already finding everything in cache.

If we clear our service workers again, and start on this same page, which is not otherwise pulling these images down, then refresh, we’ll see the network requests to pull down, and sync these images to cache.

Then if we navigate back to the page that uses these images, we won’t see the calls to cache these images, since they’re already cached; moreover, we’ll see these images being retrieved from cache by the service worker.

Both our runtimeCaching provided by sw-toolbox, and our own manual code are working together, off of the same cache.

It works!

As promised, here’s the `offline.htm` page

<div style="padding: 15px">
  <h1>Offline</h1>
  <table class="table table-condescend table-striped">
    <thead>
      <tr>
        <th></th>
        <th>Title</th>
        <th>Author</th>
      </tr>
    </thead>
    <tbody id="booksTarget">
      <!--insertion will happen here-->
    </tbody>
  </table>
</div>
let open = indexedDB.open("books");
open.onsuccess = evt => {
  let db = open.result;
  let transaction = db.transaction("books", "readonly");
  let booksStore = transaction.objectStore("books");
  var request = booksStore.openCursor();
  let rows = ``;
  request.onsuccess = function(event) {
    var cursor = event.target.result;
    if(cursor) {
      let book = cursor.value;
      rows += `
        <tr>
          <td><img src="${book.smallImage}" /></td>
          <td>${book.title}</td>
          <td>${Array.isArray(book.authors) ? book.authors.join("<br/>") : book.authors}</td>
        </tr>`;
      cursor.continue();
    } else {
      document.getElementById("booksTarget").innerHTML = rows;
    }
  };
}

Now let’s tell Chrome to pretend to be offline, and test it out:

Cool!

Where to, from here?

We’re barely scratching the surface. Your users can update these data from multiple devices, and each one will need to keep in sync somehow. You could either periodically wipe your IDB tables and re-sync; have the user manually trigger a re-sync when they want; or you could get really ambitious and try to log all your mutations on your server, and have each service worker on each device request all changes that happened since the last time it ran, in order to sync up.

The most interesting solution here is PouchDB, which does this syncing for you; the catch is it’s designed to work with CouchDB, which you may or may not be using.

Syncing local changes

For one last piece of code, let’s consider an easier problem to solve: syncing your IndexedDB with changes that are made right this minute, by your user who’s using your web app. We can already intercept fetch requests in the service worker, so it should be easy to listen for the right mutation endpoint, run it, then then peak at the results and update IndexedDB accordingly. Let’s take a look.

toolbox.router.post(/graphql/, request => {
  //just run the request as is
  return fetch(request).then(response => {
    //clone it by necessity 
    let respClone = response.clone();
    //do this later - get the response back to our user NOW
    setTimeout(() => {
      respClone.json().then(resp => {
        //this graphQL endpoint is for lots of things - inspect the data response to see
        //which operation we just ran
        if (resp && resp.data && resp.data.updateBook && resp.data.updateBook.Book) {
          syncBook(resp.data.updateBook.Book);
        }
      }, 5);
    });
    //return the response to our user NOW, before the IDB syncing
    return response;
  });
});

function syncBook(book) {
  let open = indexedDB.open("books", 1);

  open.onsuccess = evt => {
    let db = open.result;
    if (db.objectStoreNames.contains("books")) {
      let tran = db.transaction("books", "readwrite");
      let booksStore = tran.objectStore("books");
      booksStore.get(book._id).onsuccess = ({ target: { result: bookToUpdate } }) => {
        //update the book with the new values
        ["title", "authors", "isbn"].forEach(prop => (bookToUpdate[prop] = book[prop]));
        //and save it
        booksStore.put(bookToUpdate);
      };
    }
  };
}

This may seem a bit more involved than you were hoping. We can only read the fetch response once, and our application thread will also need to read it, so we’ll first clone the response. Then, we’ll run a setTimeout() so we can return the original response to the web application/user as quickly as possible, and do what we need thereafter. Don’t just rely on the promise in respClone.json() to do this, since promises use microtasks. I’ll let Jake Archibald explain what exactly that means, but the short of it is that they can starve the main event loop. I’m not quite smart enough to be certain whether that applies here, so I just went with the safe approach of setTimeout.

Since I’m using GraphQL, the responses are in a predictable format, and it’s easy to see if I just performed the operation I’m interested in, and if so I can re-sync the affected data.

Further reading

Literally everything here is explained in wonderful depth in this book by Tal Ater. If you’re interested in learning more, you can’t beat that as a learning resource.

For some more immediate, quick resources, here’s an MDN article on IndexedDB, and a service workers introduction, and offline cookbook, both from Google.

Parting thoughts

Giving your user useful things to do with your web app when they don’t even have network connectivity is an amazing new ability web developers have. As you’ve seen though, it’s no easy task. Hopefully this post has given you a realistic idea of what to expect, and a decent introduction to the things you’ll need to do to accomplish this.

Article Series:

  1. The Setup
  2. The Implementation (you are here!)

Making your web app work offline, Part 2: The Implementation originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/making-web-app-work-offline-part-2-implementation/feed/ 2 263437
Making your web app work offline, Part 1: The Setup https://css-tricks.com/making-your-web-app-work-offline-part-1/ https://css-tricks.com/making-your-web-app-work-offline-part-1/#comments Wed, 06 Dec 2017 14:57:27 +0000 http://css-tricks.com/?p=263322 This two-part series is a gentle introduction to offline web development. Getting a web application to do something while offline is surprisingly tricky, requiring a lot of things to be in place and functioning correctly. We’re going to cover all …


Making your web app work offline, Part 1: The Setup originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
This two-part series is a gentle introduction to offline web development. Getting a web application to do something while offline is surprisingly tricky, requiring a lot of things to be in place and functioning correctly. We’re going to cover all of these pieces from a high level, with working examples. This post is an overview, but there are plenty of more-detailed resources listed throughout.

Article Series:

  1. The Setup (you are here!)
  2. The Implementation

Basic approach

I’ll be making heavy use of JavaScript’s async/await syntax. It’s supported in all major browsers and Node, and greatly simplifies Promise-based code. The link above explains async well, but in a nutshell they allow you to resolve a promise, and access its value directly in code with await, rather than calling .then and accessing the value in the callback, which often leads to the dreaded “rightward drift.”

What are we building?

We’ll be extending an existing booklist project to sync the current user’s books to IndexedDB, and create a simplified offline page that’ll show even when the user has no network connectivity.

Starting with a service worker

The one non-negotiable thing you need for offline development is a service worker. A service worker is a background process that can, among other things, intercept network requests; redirect them; short circuit them by returning cached responses; or execute them as normal and do custom things with the response, like caching.

Basic caching

Probably the first, most basic, yet high impact thing you’ll do with a service worker is have it cache your application’s resources. Service worker and the cache it uses are extremely low-level primitives; everything is manual. In order to properly cache your resources you’ll need to fetch and add them to a cache, but then you’ll also need to track changes to these resources. You’ll track when they change, remove the prior version, and fetch and update the new one.

In practice, this means your service worker code will need to be generated as part of a build step, which hashes your files, and generates a file that’s smart enough to record these changes between versions, and update caches as needed.

Abstractions to the rescue

This is extremely tedious and error-prone code that you’d likely never want to write yourself. Luckily some smart people have written abstractions to help, namely sw-precache, and sw-toolbox by the great people at Google. Note, Google has since deprecated these tools in favor of the newer Workbox. I’ve yet to move my code over since sw-* works so well, but in any event the ideas are the same, and I’m told the conversion is easy. And it’s worth mentioning that sw-precache currently has about 30,000 downloads per day, so it’s still widely used.

Hello World, sw-precache

Let’s jump right in. We’re using webpack, and as webpack goes, there’s a plugin, so let’s check that out first.

// inside your webpack config
new SWPrecacheWebpackPlugin({
  mergeStaticsConfig: true,
  filename: "service-worker.js",
  staticFileGlobs: [ //static resources to cache
    "static/bootstrap/css/bootstrap-booklist-build.css",
    ...
  ],
  ignoreUrlParametersMatching: /./,
  stripPrefixMulti: { //any paths that need adjusting
    "static/": "react-redux/static/", 
    ...
  },
  ...
})

By default ALL of the bundles webpack makes will be precached. We’re also manually providing some paths to static resources I want cached in the staticFileGlobs property, and I’m adjusting some paths in stripPrefixMulti.

// inside your webpack config
const getCache = ({ name, pattern, expires, maxEntries }) => ({
  urlPattern: pattern,
  handler: "cacheFirst",
  options: {
    cache: {
      maxEntries: maxEntries || 500,
      name: name,
      maxAgeSeconds: expires || 60 * 60 * 24 * 365 * 2 //2 years
    },
    successResponses: /0|[123].*/
  }
});

new SWPrecacheWebpackPlugin({
  ...
  runtimeCaching: [ //pulls in sw-toolbox and caches dynamically based on a pattern
    getCache({ pattern: /^https:\/\/images-na.ssl-images-amazon.com/, name: "amazon-images1" }),
    getCache({ pattern: /book\/searchBooks/, name: "book-search", expires: 60 * 7 }), //7 minutes
    ...
  ]
})

Adding the runtimeCaching section to our SWPrecacheWebpackPlugin pulls in sw-toolbox and lets us cache urls matching a certain pattern, dynamically, as needed—with getCache helping keep the boilerplate to a minimum.

Hello World, sw-toolbox

The entire service worker file that’s generated is pretty big, but let’s just look at a small piece, namely one of the dynamic caches from above:

toolbox.router.get(/^https:\/\/images-na.ssl-images-amazon.com/, toolbox.cacheFirst, {
  cache: { maxEntries: 500, name: "amazon-images1", maxAgeSeconds: 63072000 },
  successResponses: /0|[123].*/
});

sw-toolbox has provided us with a nice, high-level router object we can use to hook into various URL requests, MVC-style. We’ll use this to setup offline shortly.

Don’t forget to register the service worker

And, of course, the existence of the service worker file that’s generated above is of no use by itself; it needs to be registered. The code looks like this, but be sure to either have it inside an onload listener, or some other place that’ll be guaranteed to run after the page has loaded.

if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("/service-worker.js");
}

There we have it! We got a basic service worker running, which caches our application resources. Tune in tomorrow when we extend it to support offline.

Article Series:

  1. The Setup (you are here!)
  2. The Implementation

Making your web app work offline, Part 1: The Setup originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/making-your-web-app-work-offline-part-1/feed/ 4 263322
Implementing “Save For Offline” with Service Workers https://css-tricks.com/implementing-save-offline-service-workers/ Tue, 31 Jan 2017 12:46:08 +0000 http://css-tricks.com/?p=250728 A straightforward tutorial by Una Kravets on caching assets and individually requested articles with Service Workers for offline reading.

I’m curious what the best practice will become. It’s possible that asking users to click something is it. Also possible: passively …


Implementing “Save For Offline” with Service Workers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
A straightforward tutorial by Una Kravets on caching assets and individually requested articles with Service Workers for offline reading.

I’m curious what the best practice will become. It’s possible that asking users to click something is it. Also possible: passively caching articles based on recently published, currently viewing, or related to currently viewing.

To Shared LinkPermalink on CSS-Tricks


Implementing “Save For Offline” with Service Workers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
250728
Instant Loading: Building Offline-First Progressive Web Apps https://css-tricks.com/instant-loading-building-offline-first-progressive-web-apps/ Thu, 02 Jun 2016 14:08:55 +0000 http://css-tricks.com/?p=242261 There was a lot of great talks from Google’s I/O event this year, and Jake Archibald’s talk on building offline-first is certainly one of them. The DevTools in 2016 talk is great too.

Or, decide for yourself! There is a


Instant Loading: Building Offline-First Progressive Web Apps originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
There was a lot of great talks from Google’s I/O event this year, and Jake Archibald’s talk on building offline-first is certainly one of them. The DevTools in 2016 talk is great too.

Or, decide for yourself! There is a playlist of all of them and Robert Nyman rounded up everything as well.

Also, if you’re interested in hearing more about this “Progressive Web Apps” stuff and the debate around it…

To Shared LinkPermalink on CSS-Tricks


Instant Loading: Building Offline-First Progressive Web Apps originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://www.youtube.com/embed/cmGr0RszHc8 Instant Loading: Building offline-first Progressive Web Apps - Google I/O 2016 nonadult 242261
Progressive Web Apps: The Long Game https://css-tricks.com/progressive-web-apps-long-game/ Mon, 21 Mar 2016 12:57:22 +0000 http://css-tricks.com/?p=239599 Remy Sharp attended Google’s first Progressive Web Apps event:

What’s stood out for me was how a relatively small amount of JavaScript and some well considered code can truly create an offline first experience that doesn’t just rival it’s native


Progressive Web Apps: The Long Game originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Remy Sharp attended Google’s first Progressive Web Apps event:

What’s stood out for me was how a relatively small amount of JavaScript and some well considered code can truly create an offline first experience that doesn’t just rival it’s native counterparts, but IMHO stands head and shoulders above. The load time was instant, for a web app, driven by regular HTML, CSS and JavaScript. It was fast, and all worked irrespective of the connectivity.

To Shared LinkPermalink on CSS-Tricks


Progressive Web Apps: The Long Game originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
239599
Making a Simple Site Work Offline with ServiceWorker https://css-tricks.com/serviceworker-for-offline/ https://css-tricks.com/serviceworker-for-offline/#comments Tue, 10 Nov 2015 14:51:13 +0000 http://css-tricks.com/?p=210533 I’ve been playing around with ServiceWorker a lot recently, so when Chris asked me to write an article about it I couldn’t have been more thrilled. ServiceWorker is the most impactful modern web technology since Ajax. It’s an API that …


Making a Simple Site Work Offline with ServiceWorker originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I’ve been playing around with ServiceWorker a lot recently, so when Chris asked me to write an article about it I couldn’t have been more thrilled. ServiceWorker is the most impactful modern web technology since Ajax. It’s an API that lives inside the browser and sits between your web pages and your application servers. Once installed and activated, a ServiceWorker can programmatically determine how to respond to requests for resources from your origin, even when the browser is offline. ServiceWorker can be used to power the so-called “Offline First” web.

ServiceWorker is a progressive technology, and in this article, I’ll show you how to take a website and make it available offline for humans who are using a modern browser while leaving humans with unsupported browsers unaffected.

Here’s a silent, 26 second video of a supporting browser (Chrome) going offline and the final demo site still working:

If you would like to just look at the code, there is a Simple Offline Site repo we’ve built for this. You can see the entire thing as a CodePen Project, and it’s even a full on demo website.

Browser Support

Today, ServiceWorker has browser support in Google Chrome, Opera, and in Firefox behind a configuration flag. Microsoft is likely to work on it soon. There’s no official word from Apple’s Safari yet.

Jake Archibald has a page tracking the support of all the ServiceWorker-related technologies.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
4544No1711.1

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
10810710811.3-11.4

Given the fact that you can implement this stuff in a progressive enhancement style (doesn’t affect unsupported browsers), it’s a great opportunity to get ahead of the pack. The ones that are supported are going to greatly appreciate it.

Before getting started, I should point out a couple of things for you to take into account.

Secure Connections Only

You should know that there are a few hard requirements when it comes to ServiceWorker. First and foremost, your site needs to be served over a secure connection. If you’re still serving your site over HTTP, it might be a good excuse to implement HTTPS.

HTTPS is required for ServiceWorker

You could use a CDN proxy like CloudFlare to serve traffic securely. Remember to find and fix mixed content warnings as some browsers may warn your customers about your site being unsafe, otherwise.

While the spec for HTTP/2 doesn’t inherently enforce encrypted connections, browsers intend to implement HTTP/2 and similar technologies only over HTTPS. The ServiceWorker specification, on the other hand, recommends browser implementation over HTTPS. Browsers have also hinted at marking sites served over unencrypted connections as insecure. Search engines penalize unencrypted results.

“HTTPS only” is the browsers way of saying “this is important, you should do this”.

A Promise Based API

The future of web browser API implementations is Promise-heavy. The fetch API, for example, sprinkles sweet Promise-based sugar on top of XMLHttpRequest. ServiceWorker makes occasional use of fetch, but there’s also worker registration, caching, and message passing, all of which are Promise-based.

Whether or not you are a fan of promises, they are here to stay, so you better get used to them.

Registering Your First ServiceWorker

I worked together with Chris on the simplest possible practical demonstration of how to use ServiceWorker. He implemented a simple website (static HTML, CSS, JavaScript, and images) and asked me to add offline support. I felt like that would be a great opportunity to display how easy and unobtrusive it is to add offline capabilities to an existing website.

If you’d like to skip to the end, take a look at the this commit to the demo site on GitHub.

The first step is to register the ServiceWorker. Instead of blindly attempting the registration, we feature-detect that ServiceWorker is available.

if ('serviceWorker' in navigator) {

}

The following piece of code demonstrates how we would install a ServiceWorker. The JavaScript resource passed to .register will be executed in the context of a ServiceWorker. Note how registration returns a Promise so that you can track whether or not the ServiceWorker registration was successful. I preceded logging statements with CLIENT: to make it visually easier for me to figure out whether a logging statement was coming from a web page or the ServiceWorker script.

// ServiceWorker is a progressive technology. Ignore unsupported browsers
if ('serviceWorker' in navigator) {
  console.log('CLIENT: service worker registration in progress.');
  navigator.serviceWorker.register('/service-worker.js').then(function() {
    console.log('CLIENT: service worker registration complete.');
  }, function() {
    console.log('CLIENT: service worker registration failure.');
  });
} else {
  console.log('CLIENT: service worker is not supported.');
}

The endpoint to the service-worker.js file is quite important. If the script were served from, say, /js/service-worker.js then the ServiceWorker would only be able to intercept requests in the /js/ context, but it’d be blind to resources like /other. This is typically an issue because you usually scope your JavaScript files in a /js/, /public/, /assets/, or similar “directory”, whereas you’ll want to serve the ServiceWorker script from the domain root in most cases.

That was, in fact, the only necessary change to your web application code, provided that you had already implemented HTTPS. At this point, supporting browsers will issue a request for /service-worker.js and attempt to install the worker.

How should you structure the service-worker.js file, then?

Putting Together A ServiceWorker

ServiceWorker is event-driven and your code should aim to be stateless. That’s because when a ServiceWorker isn’t being used it’s shut down, losing all state. You have no control over that, so it’s best to avoid any long-term dependence on the in-memory state.

Below, I listed the most notable events you’ll have to handle in a ServiceWorker.

  • The install event fires when a ServiceWorker is first fetched. This is your chance to prime the ServiceWorker cache with the fundamental resources that should be available even while users are offline.
  • The fetch event fires whenever a request originates from your ServiceWorker scope, and you’ll get a chance to intercept the request and respond immediately, without going to the network.
  • The activate event fires after a successful installation. You can use it to phase out older versions of the worker. We’ll look at a basic example where we deleted stale cache entries.

Let’s go over each event and look at examples of how they could be handled.

Installing Your ServiceWorker

A version number is useful when updating the worker logic, allowing you to remove outdated cache entries during the activation step, as we’ll see a bit later. We’ll use the following version number as a prefix when creating cache stores.

var version = 'v1::';

You can use addEventListener to register an event handler for the install event. Using event.waitUntil blocks the installation process on the provided p promise. If the promise is rejected because, for instance, one of the resources failed to be downloaded, the service worker won’t be installed. Here, you can leverage the promise returned from opening a cache with caches.open(name) and then mapping that into cache.addAll(resources), which downloads and stores responses for the provided resources.

self.addEventListener("install", function(event) {
  console.log('WORKER: install event in progress.');
  event.waitUntil(
    /* The caches built-in is a promise-based API that helps you cache responses,
       as well as finding and deleting them.
    */
    caches
      /* You can open a cache by name, and this method returns a promise. We use
         a versioned cache name here so that we can remove old cache entries in
         one fell swoop later, when phasing out an older service worker.
      */
      .open(version + 'fundamentals')
      .then(function(cache) {
        /* After the cache is opened, we can fill it with the offline fundamentals.
           The method below will add all resources we've indicated to the cache,
           after making HTTP requests for each of them.
        */
        return cache.addAll([
          '/',
          '/css/global.css',
          '/js/global.js'
        ]);
      })
      .then(function() {
        console.log('WORKER: install completed');
      })
  );
});

Once the install step succeeds, the activate event fires. This helps us phase out an older ServiceWorker, and we’ll look at it later. For now, let’s focus on the fetch event, which is a bit more interesting.

Intercepting Fetch Requests

The fetch event fires whenever a page controlled by this service worker requests a resource. This isn’t limited to fetch or even XMLHttpRequest. Instead, it comprehends even the request for the HTML page on first load, as well as JS and CSS resources, fonts, any images, etc. Note also that requests made against other origins will also be caught by the fetch handler of the ServiceWorker. For instance, requests made against i.imgur.com – the CDN for a popular image hosting site – would also be caught by our service worker as long as the request originated on one of the clients (e.g browser tabs) controlled by the worker.

Just like install, we can block the fetch event by passing a promise to event.respondWith(p), and when the promise fulfills the worker will respond with that instead of the default action of going to the network. We can use caches.match to look for cached responses, and return those responses instead of going to the network.

As described in the comments, here we’re using an “eventually fresh” caching pattern where we return whatever is stored on the cache but always try to fetch a resource again from the network regardless, to keep the cache updated. If the response we served to the user is stale, they’ll get a fresh response the next time they request the resource. If the network request fails, it’ll try to recover by attempting to serve a hardcoded Response.

self.addEventListener("fetch", function(event) {
  console.log('WORKER: fetch event in progress.');

  /* We should only cache GET requests, and deal with the rest of method in the
     client-side, by handling failed POST,PUT,PATCH,etc. requests.
  */
  if (event.request.method !== 'GET') {
    /* If we don't block the event as shown below, then the request will go to
       the network as usual.
    */
    console.log('WORKER: fetch event ignored.', event.request.method, event.request.url);
    return;
  }
  /* Similar to event.waitUntil in that it blocks the fetch event on a promise.
     Fulfillment result will be used as the response, and rejection will end in a
     HTTP response indicating failure.
  */
  event.respondWith(
    caches
      /* This method returns a promise that resolves to a cache entry matching
         the request. Once the promise is settled, we can then provide a response
         to the fetch request.
      */
      .match(event.request)
      .then(function(cached) {
        /* Even if the response is in our cache, we go to the network as well.
           This pattern is known for producing "eventually fresh" responses,
           where we return cached responses immediately, and meanwhile pull
           a network response and store that in the cache.
           Read more:
           https://ponyfoo.com/articles/progressive-networking-serviceworker
        */
        var networked = fetch(event.request)
          // We handle the network request with success and failure scenarios.
          .then(fetchedFromNetwork, unableToResolve)
          // We should catch errors on the fetchedFromNetwork handler as well.
          .catch(unableToResolve);

        /* We return the cached response immediately if there is one, and fall
           back to waiting on the network as usual.
        */
        console.log('WORKER: fetch event', cached ? '(cached)' : '(network)', event.request.url);
        return cached || networked;

        function fetchedFromNetwork(response) {
          /* We copy the response before replying to the network request.
             This is the response that will be stored on the ServiceWorker cache.
          */
          var cacheCopy = response.clone();

          console.log('WORKER: fetch response from network.', event.request.url);

          caches
            // We open a cache to store the response for this request.
            .open(version + 'pages')
            .then(function add(cache) {
              /* We store the response for this request. It'll later become
                 available to caches.match(event.request) calls, when looking
                 for cached responses.
              */
              cache.put(event.request, cacheCopy);
            })
            .then(function() {
              console.log('WORKER: fetch response stored in cache.', event.request.url);
            });

          // Return the response so that the promise is settled in fulfillment.
          return response;
        }

        /* When this method is called, it means we were unable to produce a response
           from either the cache or the network. This is our opportunity to produce
           a meaningful response even when all else fails. It's the last chance, so
           you probably want to display a "Service Unavailable" view or a generic
           error response.
        */
        function unableToResolve () {
          /* There's a couple of things we can do here.
             - Test the Accept header and then return one of the `offlineFundamentals`
               e.g: `return caches.match('/some/cached/image.png')`
             - You should also consider the origin. It's easier to decide what
               "unavailable" means for requests against your origins than for requests
               against a third party, such as an ad provider
             - Generate a Response programmaticaly, as shown below, and return that
          */

          console.log('WORKER: fetch request failed in both cache and network.');

          /* Here we're creating a response programmatically. The first parameter is the
             response body, and the second one defines the options for the response.
          */
          return new Response('<h1>Service Unavailable</h1>', {
            status: 503,
            statusText: 'Service Unavailable',
            headers: new Headers({
              'Content-Type': 'text/html'
            })
          });
        }
      })
  );
});

There’s several more strategies, some of which I discuss in an article about ServiceWorker strategies on my blog.

As promised, let’s look at the code you can use to phase out older versions of your ServiceWorker script.

Phasing Out Older ServiceWorker Versions

The activate event fires after a service worker has been successfully installed. It is most useful when phasing out an older version of a service worker, as at this point you know that the new worker was installed correctly. In this example, we delete old caches that don’t match the version for the worker we just finished installing.

self.addEventListener("activate", function(event) {
  /* Just like with the install event, event.waitUntil blocks activate on a promise.
     Activation will fail unless the promise is fulfilled.
  */
  console.log('WORKER: activate event in progress.');

  event.waitUntil(
    caches
      /* This method returns a promise which will resolve to an array of available
         cache keys.
      */
      .keys()
      .then(function (keys) {
        // We return a promise that settles when all outdated caches are deleted.
        return Promise.all(
          keys
            .filter(function (key) {
              // Filter by keys that don't start with the latest version prefix.
              return !key.startsWith(version);
            })
            .map(function (key) {
              /* Return a promise that's fulfilled
                 when each outdated cache is deleted.
              */
              return caches.delete(key);
            })
        );
      })
      .then(function() {
        console.log('WORKER: activate completed.');
      })
  );
});

Reminder: there is a Simple Offline Site repo we’ve built for this. You can see the entire thing as a CodePen Project, and it’s even a full on demo website.


Making a Simple Site Work Offline with ServiceWorker originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/serviceworker-for-offline/feed/ 14 210533