飞码网-免费源码博客分享网站

点击这里给我发消息

本地缓存获取的AJAX请求:包裹Fetch API|-JavaScript教程

飞码网-免费源码博客分享网站 爱上飞码网—https://www.codefrees.com— 飞码网-matlab-python-C++ 爱上飞码网—https://www.codefrees.com— 飞码网-免费源码博客分享网站

 

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

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

提取API

希望您现在对提取有所了解。它是浏览器中的新本机API,可以代替旧的XMLHttpRequestAPI。

我可以使用提取吗?来自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)
  })

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

让我们从简单包装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/json或时,我们才存储响应text/*这是因为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')
  })

到目前为止,此解决方案的巧妙之处在于,它对于JSONHTML请求都可以正常工作,而不会产生干扰当它是图像时,它不会尝试将其存储在中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')
>>>

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

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

 

 

// 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比依赖网络的速度快了数十亿倍。有关localStorageXHR与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。如果您在网络控制台中检查存储空间,则会看到类似的键557027443

结论

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

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

飞码网-免费源码博客分享网站 爱上飞码网—https://www.codefrees.com— 飞码网-matlab-python-C++ 爱上飞码网—https://www.codefrees.com— 飞码网-免费源码博客分享网站
赞 ()

相关推荐

内容页底部广告位3
留言与评论(共有 0 条评论)
   
验证码: