api接口 数据获取 缓存_在本地缓存获取的AJAX请求:包装获取API

api接口 数据获取 缓存

本文由特邀作者Peter Bengtsson撰写。 SitePoint来宾帖子旨在为您带来JavaScript社区知名作家和演讲者的引人入胜的内容

本文演示了如何实现获取的请求的本地缓存,以便如果反复执行,它将从会话存储中读取。 这样做的好处是您不需要为要缓存的每个资源使用自定义代码。

如果您想在下一个JavaScript晚宴上看起来很酷,请继续学习,在这里您可以炫耀各种兑现承诺,最新的API和本地存储的技能。

提取API

希望您现在对fetch熟悉。 它是浏览器中的新本机API,可以代替旧的XMLHttpRequest API。

我可以使用提取吗? 来自caniuse.com的跨主要浏览器支持提取功能的数据。

在尚未在所有浏览器中完美实现的地方,您可以使用GitHub的fetch polyfill (如果整日无事可做,请参见Fetch Standard规范 )。

天真的选择

假设您确切地知道需要下载哪个资源,而只想下载一次。 您可以使用全局变量作为缓存,如下所示:

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)

在CodePen上

那只是依靠一个全局变量来保存缓存的数据。 直接的问题是,如果您重新加载页面或导航到某些新页面,则缓存的数据将消失。

让我们在剖析其缺点之前先升级我们的第一个天真解决方案。

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)

在CodePen上

第一个直接的问题是, fetch是基于承诺的,这意味着我们无法确定何时完成获取,因此可以肯定的是,在承诺实现之前,我们不应该依赖其执行。

第二个问题是该解决方案非常特定于特定的URL和特定的缓存数据(在此示例中为键information )。 我们想要的是基于URL的通用解决方案。

首次实施–保持简单

让我们在fetch放一个包装,它还会返回一个承诺。 调用它的代码可能不在乎结果是来自网络还是来自本地缓存。

因此,假设您曾经这样做:

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

在CodePen上

现在,您需要对其进行包装,以便重复的网络调用可以从本地缓存中受益。 让我们简单地将其cachedFetch ,因此代码如下所示:

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

第一次运行时,它需要通过网络解析请求并将结果存储在缓存中。 第二次应直接从本地存储中提取。

免费学习PHP!

全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。

原价$ 11.95 您的完全免费

让我们从简单包装fetch函数的代码开始:

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

在CodePen上

这有效,但是毫无用处。 让我们开始实现所获取数据的存储

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
  })
}

在CodePen上

这里有很多事情。

实际上,由fetch返回的第一个承诺继续进行并发出GET请求。 如果有与CORS(跨来源资源共享)问题.text() .json().blob()方法将无法正常工作。

最有趣的功能是我们必须克隆第一个Promise返回的Response对象。 如果我们不这样做,我们将向自己注入过多,并且当promise的最终用户尝试调用.json() (例如),他们将收到此错误:

TypeError: Body has already been consumed.

要注意的另一件事是对响应类型是什么的谨慎:我们仅在状态码为200 内容类型为application/jsontext/*存储响应。 这是因为sessionStorage只能存储文本。

这是使用此示例:

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')
  })

到目前为止,该解决方案的精巧之处在于,它对于JSON HTML请求都可以正常工作而不会产生干扰。 当它是映像时,它不会尝试将其存储在sessionStorage

第二实施–实际返回缓存命中

因此,我们的第一个实现只是负责存储请求的响应。 但是,如果您第二次调用cachedFetch它仍然不会尝试从sessionStorage 检索任何内容。 我们需要做的是首先返回一个诺言,并且该诺言需要解析一个Response对象

让我们从一个非常基本的实现开始:

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
  })
}

在CodePen上

而且就可以了!

要查看其运行情况,请打开此代码的CodePen,然后在开发人员工具中打开浏览器的“网络”标签。 几次按“运行”按钮(在CodePen的右上角),您应该看到通过网络重复请求仅图像。

关于此解决方案的一件好事是缺少“回调意大利面条”。 由于sessionStorage.getItem调用是同步的(也称为阻塞),因此我们不必处理“它是否在本地存储中?” 内的诺言或回调。 而且只有当那里有东西时,我们才返回缓存的结果。 如果不是,则if语句仅继续执行常规代码。

第三次实施–到期时间如何?

到目前为止,我们一直在使用sessionStorage ,它与localStorage一样,只不过当您启动新选项卡时, sessionStorage会被清除。 这意味着我们正在采用一种“自然的方式”,即不会将内容缓存太久。 如果我们改用localStorage并缓存某些内容,即使远程内容已更改,它也只会“永远”卡在那里。 那很不好。

更好的解决方案是赋予用户控制权。 (在这种情况下,用户是使用我们的cachedFetch函数的Web开发人员)。 与服务器端的Memcached或Redis之类的存储一样,您可以设置生存期,指定应将其缓存多长时间。

例如,在Python(带有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')
>>>

现在, sessionStoragelocalStorage都没有内置此功能,因此我们必须手动实现它。 我们将通过始终在存储时记录时间戳并将其与可能的缓存命中进行比较来做到这一点。

但是在我们这样做之前,情况会如何? 这样的事情怎么样:

// 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)
  })

我们要添加的关键的新内容是,每次保存响应数据时,我们也会 存储它们时进行记录。 但是请注意,现在我们还可以切换到localStorage的强大存储,而不是sessionStorage 。 我们的自定义到期代码将确保我们不会在原本持久的localStorage得到可怕的过时缓存命中。

因此,这是我们最终的工作解决方案:

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, 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 => {
          localStorage.setItem(cacheKey, content)
          localStorage.setItem(cacheKey+':ts', Date.now())
        })
      }
    }
    return response
  })
}

在CodePen上

未来的实现–更好,更高级,更酷

我们不仅要避免过度使用这些Web API,最好的部分是localStorage速度比依赖网络快了数十亿倍。 请参阅此博客文章,以比较localStorage和XHR: localForage与XHR 。 它可以衡量其他因素,但基本上可以得出以下结论: localStorage确实非常快,磁盘缓存预热很少见。

那么,我们如何进一步改善解决方案呢?

处理二进制响应

我们在这里的实现不会麻烦缓存非文本的东西,例如图像,但是没有理由不这样做。 我们将需要更多代码。 特别是,我们可能想存储有关Blob的更多信息。 每个响应基本上都是Blob。 对于文本和JSON,它只是一个字符串数组。 typesize并不重要,因为您可以从字符串本身中找出它。 对于二进制内容,必须将blob转换为ArrayBuffer

出于好奇,要查看支持图像的实现的扩展,请查看此CodePen

使用哈希缓存键

另一个潜在的改进是通过将每个URL(这就是我们以前用作键的内容)的哈希值散列到较小的值,从而以空间换取速度。 在上面的示例中,我们仅使用了一些非常小的且整洁的URL(例如https://httpbin.org/get ),但是如果您的URL非常大且包含很多查询字符串,并且您有很多,它真的可以加起来。

一种解决方案是使用这种安全且快速的整洁算法

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;
}

如果您愿意,请签出此CodePen 。 如果您在Web控制台中检查存储,则会看到诸如557027443类的557027443

结论

现在,您有了一个可以使用的解决方案,可以坚持使用自己的Web应用程序,也许您正在使用Web API,并且您知道响应可以很好地缓存给用户。

可能是该原型的自然扩展的最后一件事是使它超出文章,进入具有测试和README的真实,具体项目,并在npm上发布-但这又是另外一次!

翻译自: https://www.sitepoint.com/cache-fetched-ajax-requests/

api接口 数据获取 缓存

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值