This article is by guest authorPeter Bengtsson. SitePoint guest posts aim to bring you engaging content from prominent writers and speakers of the JavaScript community

More from this author

  • Smart Front-ends & Dumb Back-ends: Persisting State in AngularJS
  • Face Proximity Detection with JavaScript

This article demonstrates how you implement a local cache of fetched requestsso 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 withfetch. It’s a new native API in browsers to replace the oldXMLHttpRequestAPI.

Where it hasn’t been perfectly implemented in all browsers,you can useGitHub’s fetch polyfill(And if you have nothing to do all day,here’s theFetch 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 thatfetchis 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 (keyinformationin 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 aroundfetchthat 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 youusedto 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 itcachedFetchinstead,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 thefetchfunction:

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

On CodePen

This works,but is useless,of course. Let’s implement thestoringof 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
  })
}

On CodePen

There’s quite a lot going on here.

The first promise returned byfetchactually goes ahead and makes the GET request. If there are problems with CORS (Cross-Origin Resource Sharing) the.text(),.json()or.blob()methods won’t work.

The most interesting feature is that we have toclonetheResponseobject returned by the first promise. If we don’t do that,we’re injecting ourselves too much and when the final user of the promise tries to call.json()(for example) they’ll get this error:

TypeError: Body has already been consumed.

The other thing to notice is the carefulness around what the response type is: we only store the response if the status code is200andif the content type isapplication/jsonortext/*. This is becausesessionStoragecan only store text.

Here’s an example of using this:

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

cachedFetch('https://httpbin.org/html')
  .then(r => r.text())
  .then(document => {
    console.log('Document has ' + document.match(/<p>/).length + ' paragraphs')
  })

cachedFetch('https://httpbin.org/image/png')
  .then(r => r.blob())
  .then(image => {
    console.log('Image is ' + image.size + ' bytes')
  })

What’s neat about this solution so far is that it works,without interfering,for both JSONandHTML requests. And when it’s an image,it does not attempt to store that insessionStorage.

Second Implementation – Actually Return Cache Hits

So our first implementation just takes care ofstoringthe responses of requests. But if you call thecachedFetcha second time it doesn’t yet bother to try toretrieveanything fromsessionStorage. What we need to do is return,first of all,a promise and the promise needs to resolve aResponse object.

Let’s start with a very basic implementation:

const cachedFetch = (url,options) => {
  // Use the URL as the cache key to sessionStorage
  let cacheKey = url

  // START new cache HIT code
  let cached = sessionStorage.getItem(cacheKey)
  if (cached !== null) {
    // it was in sessionStorage! Yay!
    let response = new Response(new Blob([cached]))
    return Promise.resolve(response)
  }
  // END new cache HIT code

  return fetch(url,options).then(response => {
    // let's only store in cache if the content-type is
    // JSON or something non-binary
    if (response.status === 200) {
      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
  })
}

On CodePen

And it just works!

To see it in action,openthe CodePen for this codeand once you’re there open your browser’s Network tab in the developer tools. Press the “Run” button (top-right-ish corner of CodePen) a couple of times and you should see that only the image is being repeatedly requested over the network.

One thing that is neat about this solution is the lack of “callback spaghetti”. Since thesessionStorage.getItemcall is synchronous (aka. blocking),we don’t have to deal with “Was it in the local storage?” inside a promise or callback. And only if there was something there,do we return the cached result. If not,the if statement just carries on to the regular code.

Third Implementation – What About Expiry Times?

So far we’ve been usingsessionStoragewhich is just likelocalStorageexcept that thesessionStoragegets wiped clean when youstart a new tab. That means we’re riding a “natural way” of not caching things too long. If we were to uselocalStorageinstead and cache something,it’d simply get stuck there “forever” even if the remote content has changed. And that’s bad.

A better solution is to give theusercontrol instead. (The user in this case is the web developer using ourcachedFetchfunction). Like with storage such as Memcached or Redis on the server side,you set a lifetime specifying how long it should be cached.

For example,in Python (with Flask)

>>> from werkzeug.contrib.cache import MemcachedCache
>>> cache = MemcachedCache(['127.0.0.1:11211'])
>>> cache.set('key','value',10)
True
>>> cache.get('key')
'value'
>>> # waiting 10 seconds
...
>>> cache.get('key')
>>>

Now,neithersessionStoragenorlocalStoragehas this functionality built-in,so we have to implement it manually. We’ll do that by always taking note of the timestamp at the time of storing and use that to compare on a possible cache hit.

But before we do that,how is this going to look? How about something like this:

// Use a default expiry time,like 5 minutes
cachedFetch('https://httpbin.org/get')
  .then(r => r.json())
  .then(info => {
    console.log('Your origin is ' + info.origin)
  })

// Instead of passing options to `fetch` we pass an integer which is seconds
cachedFetch('https://httpbin.org/get',2 * 60)  // 2 min
  .then(r => r.json())
  .then(info => {
    console.log('Your origin is ' + info.origin)
  })

// Combined with fetch's options object but called with a custom name
let init = {
  mode: 'same-origin',seconds: 3 * 60 // 3 minutes
}
cachedFetch('https://httpbin.org/get',init)
  .then(r => r.json())
  .then(info => {
    console.log('Your origin is ' + info.origin)
  })

The crucial new thing we’re going to add is that every time we save the response data,wealsorecordwhenwe stored it. But note that Now we can also switch to the braver storage oflocalStorageinstead ofsessionStorage. Our custom expiry code will make sure we don’t get horribly stale cache hits in the otherwise persistentlocalStorage.

So here’s our final working solution:

const cachedFetch = (url,options) => {
  let expiry = 5 * 60 // 5 min default
  if (typeof options === 'number') {
    expiry = options
    options = undefined
  } else if (typeof options === 'object') {
    // I hope you didn't set it to 0 seconds
    expiry = options.seconds || expiry
  }
  // Use the URL as the cache key to sessionStorage
  let cacheKey = url
  let cached = localStorage.getItem(cacheKey)
  let whenCached = localStorage.getItem(cacheKey + ':ts')
  if (cached !== null && whenCached !== null) {
    // it was in sessionStorage! Yay!
    // Even though 'whenCached' is a string,this operation
    // works because the minus sign converts the
    // string to an integer and it will work.
    let age = (Date.Now() - whenCached) / 1000
    if (age < expiry) {
      let response = new Response(new Blob([cached]))
      return Promise.resolve(response)
    } else {
      // We need to clean up this old key
      localStorage.removeItem(cacheKey)
      localStorage.removeItem(cacheKey + ':ts')
    }
  }

  return fetch(url,it will be
        // consumed by the time it's returned. This
        // way we're being un-intrusive.
        response.clone().text().then(content => {
          localStorage.setItem(cacheKey,content)
          localStorage.setItem(cacheKey+':ts',Date.Now())
        })
      }
    }
    return response
  })
}

On CodePen

Future Implementation – Better,Fancier,Cooler

Not only are we avoiding hitting those web APIs excessively,the best part is thatlocalStorageis a gazillion times faster than relying on network. See this blog post for a comparison oflocalStorageversus XHR:localForage vs. XHR. It measures other things but basically concludes thatlocalStorageis really fast and disk-cache warm-ups are rare.

So how Could we further improve our solution?

Dealing with binary responses

Our implementation here doesn’t bother caching non-text things,like images,but there’s no reason it can’t. We would need a bit more code. In particular,we probably want to store more information about theBlob. Every response is a Blob basically. For text and JSON it’s just an array of strings. And thetypeandsizedoesn’t really matter because it’s something you can figure out from the string itself. For binary content the blob has to be converted to aArrayBuffer.

For the curIoUs,to see an extension of our implementation that supports images,check outthis CodePen.

Using hashed cache keys

Another potential improvement is to Trade space for speed by hashing every URL,which was what we used as a key,to something much smaller. In the examples above we’ve been using just a handful of really small and neat URLs (e.g.https://httpbin.org/get) but if you have really large URLs with lots of query string thingies and you have lots of them,it can really add up.

A solution to this is to usethis neat algorithmwhich is kNown to be safe and fast:

const hashstr = s => {
  let hash = 0;
  if (s.length == 0) return hash;
  for (let i = 0; i < s.length; i++) {
    let char = s.charCodeAt(i);
    hash = ((hash<<5)-hash)+char;
    hash = hash & hash; // Convert to 32bit integer
  }
  return hash;
}

If you like this,check outthis CodePen. If you inspect the storage in your web console you’ll see keys like557027443.

Conclusion

You Now have a working solution you can stick into your web apps,where perhaps you’re consuming a web API and you kNow the responses can be pretty well cached for your users.

One last thing that might be a natural extension of this prototype is to take it beyond an article and into a real,concrete project,with tests and aREADME,and publish it on npm – but that’s for another time!

Cache Fetched AJAX Requests Locally: Wrapping the Fetch API的更多相关文章

  1. 关于h5中的fetch方法解读(小结)

    这篇文章主要介绍了关于h5中的fetch方法解读(小结),fetch身为H5中的一个新对象,他的诞生,是为了取代ajax的存在而出现,有兴趣的可以了解一下

  2. ios – GL_APPLE_shader_framebuffer_fetch gl_lastFragData

    我该怎么做才能真正使用这个扩展?

  3. 应用程序关闭时的iOS任务

    我正在构建一个应用程序,通过ajax将文件上传到服务器.问题是用户很可能有时不会有互联网连接,并且客户希望在用户重新连接时安排ajax调用.这可能是用户在离线时安排文件上传并关闭应用程序.应用程序关闭时可以进行ajax调用吗?

  4. CoreData在Swift 3.0中的一点改变

    同时我们看到,在建立request的时候直接使用的是NSFetchRequest的纯构造器方式。作为代替我们使用context的另一个方法来完成:that‘sall,goodluck!;)

  5. Swift3.0 CoreData Fetch语法的一些改变

    在Swift3.0之前,我们可以这样fetch数据:不过在Swift3.0以后,要做一些调整,首先第1句修改为:在这里你必须明确指定一种泛型,在第2句修改如下:最后第3句不需要做改变.其实第1句也可以这么写:这样第3句就可以将类型转换删掉了:

  6. 如何在SWIFT中的IOS CORE-DATA请求中使用SQL GROUP BY和SUM函数?

    我有一个表(Transactions),其中包含包含Account_name和交易金额的记录.我想计算每个帐户的所有交易的总额,以“私人”开头,交易金额为>1000.我想按名称按降序排列帐户.所以sql请求会是这样的:我如何在Swift中使用Core-DATA来做到这一点.谢谢请记住,CoreData不是关系数据库,因此您应该认为实体不是“表”,对象不是“记录”.另请注意,按照惯例,属性名称不应以

  7. android – Phonegap本地构建 – jquery ajax错误:readystate 0 responsetext status 0 statustext error

    解决方法您是否在索引文件中包含了内容安全元标记?

  8. Ajax简单的异步交互及Ajax原生编写

    一提到异步交互大家就会说ajax,仿佛ajax这个技术已经成为了异步交互的代名词.那下面将研究ajax的核心对象

  9. Ajax跨域问题的解决办法汇总(推荐)

    本文给大家分享多种方法解决Ajax跨域问题,非常不错具有参考借鉴价值,感兴趣的朋友一起学习吧

  10. ajax编写简单的登录页面

    这篇文章主要为大家详细介绍了ajax编写简单登录页面的具体代码,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

随机推荐

  1. xe-ajax-mock 前端虚拟服务

    最新版本见Github,点击查看历史版本基于XEAjax扩展的Mock虚拟服务插件;对于前后端分离的开发模式,ajax+mock使前端不再依赖后端接口开发效率更高。CDN使用script方式安装,XEAjaxMock会定义为全局变量生产环境请使用xe-ajax-mock.min.js,更小的压缩版本,可以带来更快的速度体验。

  2. vue 使用 xe-ajax

    安装完成后自动挂载在vue实例this.$ajaxCDN安装使用script方式安装,VXEAjax会定义为全局变量生产环境请使用vxe-ajax.min.js,更小的压缩版本,可以带来更快的速度体验。cdnjs获取最新版本点击浏览已发布的所有npm包源码unpkg获取最新版本点击浏览已发布的所有npm包源码AMD安装require.js安装示例ES6Module安装通过Vue.use()来全局安装示例./Home.vue

  3. AJAX POST数据中文乱码解决

    前端使用encodeURI进行编码后台java.net.URLDecoder进行解码编解码工具

  4. Koa2框架利用CORS完成跨域ajax请求

    实现跨域ajax请求的方式有很多,其中一个是利用CORS,而这个方法关键是在服务器端进行配置。本文仅对能够完成正常跨域ajax响应的,最基本的配置进行说明。这样OPTIONS请求就能够通过了。至此为止,相当于仅仅完成了预检,还没发送真正的请求呢。

  5. form提交时,ajax上传文件并更新到&lt;input&gt;中的value字段

  6. ajax的cache作用

    filePath="+escape;},error:{alert;}});解决方案:1.加cache:false2.url加随机数正常代码:网上高人解读:cache的作用就是第一次请求完毕之后,如果再次去请求,可以直接从缓存里面读取而不是再到服务器端读取。

  7. 浅谈ajax上传文件属性contentType = false

    默认值为contentType="application/x-www-form-urlencoded".在默认情况下,内容编码类型满足大多数情况。在这里,我们主要谈谈contentType=false.在使用ajax上传文件时:在其中先封装了一个formData对象,然后使用post方法将文件传给服务器。说到这,我们发现在JQueryajax()方法中我们使contentType=false,这不是冲突了吗?这就是因为当我们在form标签中设置了enctype=“multipart/form-data”,

  8. 909422229_ajaxFileUpload上传文件

    ajaxFileUpload.js很多同名的,因为做出来一个很容易。我上github搜AjaxFileUpload出来很多类似js。ajaxFileUpload是一个异步上传文件的jQuery插件传一个不知道什么版本的上来,以后不用到处找了。语法:$.ajaxFileUploadoptions参数说明:1、url上传处理程序地址。2,fileElementId需要上传的文件域的ID,即的ID。3,secureuri是否启用安全提交,默认为false。4,dataType服务器返回的数据类型。6,error

  9. AJAX-Cache:一款好用的Ajax缓存插件

    原文链接AJAX-Cache是什么Ajax是前端开发必不可少的数据获取手段,在频繁的异步请求业务中,我们往往需要利用“缓存”提升界面响应速度,减少网络资源占用。AJAX-Cache是一款jQuery缓存插件,可以为$.ajax()方法扩展缓存功能。

  10. jsf – Ajax update/render在已渲染属性的组件上不起作用

    我试图ajax更新一个有条件渲染的组件。我可以确保#{user}实际上是可用的。这是怎么引起的,我该如何解决呢?必须始终在ajax可以重新呈现之前呈现组件。Ajax正在使用JavaScriptdocument.getElementById()来查找需要更新的组件。但是如果JSF没有将组件放在第一位,那么JavaScript找不到要更新的内容。解决方案是简单地引用总是渲染的父组件。

返回
顶部