Tuesday, August 23, 2016

Cache Fetched AJAX Requests Locally: Wrapping the Fetch API

This article demonstrates how you implement a local cache of fetched requests so that if done repeatedly it reads from session storage instead. The advantage of this is that you don't need to have custom code for each resource you want cached.

Follow along if you want to look really cool at your next JavaScript dinner party, where you can show off various skills of juggling promises, state-of-the-art APIs and local storage.

The Fetch API

At this point you're hopefully familiar with fetch. It's a new native API in browsers to replace the old XMLHttpRequest API.

Can I Use fetch? Data on support for the fetch feature across the major browsers from caniuse.com.

Where it hasn't been perfectly implemented in all browsers, you can use GitHub's fetch polyfill (And if you have nothing to do all day, here's the Fetch Standard spec).

The Naïve Alternative

Suppose you know exactly which one resource you need to download and only want to download it once. You could use a global variable as your cache, something like this:

let origin = null
fetch('https://httpbin.org/get')
  .then(r => r.json())
  .then(information => {
    origin = information.origin  // your client's IP
  })

// need to delay to make sure the fetch has finished
setTimeout(() => {
  console.log('Your origin is ' + origin)
}, 3000)

On CodePen

That just relies on a global variable to hold the cached data. The immediate problem is that the cached data goes away if you reload the page or navigate to some new page.

Let's upgrade our first naive solution before we dissect its shortcomings.

fetch('https://httpbin.org/get')
  .then(r => r.json())
  .then(info => {
    sessionStorage.setItem('information', JSON.stringify(info))
  })

// need to delay to make sure the fetch has finished
setTimeout(() => {
  let info = JSON.parse(sessionStorage.getItem('information'))
  console.log('Your origin is ' + info.origin)
}, 3000)

On CodePen

The first an immediate problem is that fetch is promise-based, meaning we can't know for sure when it has finished, so to be certain we should not rely on its execution until its promise resolves.

The second problem is that this solution is very specific to a particular URL and a particular piece of cached data (key information in this example). What we want is a generic solution that is based on the URL instead.

First Implementation - Keeping It Simple

Let's put a wrapper around fetch that also returns a promise. The code that calls it probably doesn't care if the result came from the network or if it came from the local cache.

So imagine you used to do this:

fetch('https://httpbin.org/get')
  .then(r => r.json())
  .then(issues => {
    console.log('Your origin is ' + info.origin)
  })

On CodePen

And now you want to wrap that, so that repeated network calls can benefit from a local cache. Let's simply call it cachedFetch instead, so the code looks like this:

cachedFetch('https://httpbin.org/get')
  .then(r => r.json())
  .then(info => {
    console.log('Your origin is ' + info.origin)
  })

The first time that's run, it needs to resolve the request over the network and store the result in the cache. The second time it should draw directly from the local storage.

Let's start with the code that simply wraps the fetch function:

const cachedFetch = (url, options) => {
  return fetch(url, options)
}

On CodePen

This works, but is useless, of course. Let's implement the storing of the fetched data to start with.

const cachedFetch = (url, options) => {
  // Use the URL as the cache key to sessionStorage
  let cacheKey = url
  return fetch(url, options).then(response => {
    // let's only store in cache if the content-type is
    // JSON or something non-binary
    let ct = response.headers.get('Content-Type')
    if (ct && (ct.match(/application\/json/i) || ct.match(/text\//i))) {
      // There is a .json() instead of .text() but
      // we're going to store it in sessionStorage as
      // string anyway.
      // If we don't clone the response, it will be
      // consumed by the time it's returned. This
      // way we're being un-intrusive.
      response.clone().text().then(content => {
        sessionStorage.setItem(cacheKey, content)
      })
    }
    return response
  })
}

Continue reading %Cache Fetched AJAX Requests Locally: Wrapping the Fetch API%


by Peter Bengtsson via SitePoint

No comments:

Post a Comment