api接口 数据获取 缓存
本文由特邀作者Peter Bengtsson撰写。 SitePoint来宾帖子旨在为您带来JavaScript社区知名作家和演讲者的引人入胜的内容
本文演示了如何实现获取的请求的本地缓存,以便如果反复执行,它将从会话存储中读取。 这样做的好处是您不需要为要缓存的每个资源使用自定义代码。
如果您想在下一个JavaScript晚宴上看起来很酷,请继续学习,在这里您可以炫耀各种兑现承诺,最新的API和本地存储的技能。
提取API
希望您现在对fetch熟悉。 它是浏览器中的新本机API,可以代替旧的XMLHttpRequest
API。
在尚未在所有浏览器中完美实现的地方,您可以使用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)
那只是依靠一个全局变量来保存缓存的数据。 直接的问题是,如果您重新加载页面或导航到某些新页面,则缓存的数据将消失。
让我们在剖析其缺点之前先升级我们的第一个天真解决方案。
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)
第一个直接的问题是, fetch
是基于承诺的,这意味着我们无法确定何时完成获取,因此可以肯定的是,在承诺实现之前,我们不应该依赖其执行。
第二个问题是该解决方案非常特定于特定的URL和特定的缓存数据(在此示例中为键information
)。 我们想要的是基于URL的通用解决方案。
首次实施–保持简单
让我们在fetch
放一个包装,它还会返回一个承诺。 调用它的代码可能不在乎结果是来自网络还是来自本地缓存。
因此,假设您曾经这样做:
fetch('https://httpbin.org/get')
.then(r => r.json())
.then(issues => {
console.log('Your origin is ' + info.origin)
})
现在,您需要对其进行包装,以便重复的网络调用可以从本地缓存中受益。 让我们简单地将其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)
}
这有效,但是毫无用处。 让我们开始实现所获取数据的存储 。
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
})
}
这里有很多事情。
实际上,由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')
})
到目前为止,该解决方案的精巧之处在于,它对于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的右上角),您应该看到通过网络重复请求仅图像。
关于此解决方案的一件好事是缺少“回调意大利面条”。 由于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
})
}
未来的实现–更好,更高级,更酷
我们不仅要避免过度使用这些Web API,最好的部分是localStorage
速度比依赖网络快了数十亿倍。 请参阅此博客文章,以比较localStorage
和XHR: localForage与XHR 。 它可以衡量其他因素,但基本上可以得出以下结论: localStorage
确实非常快,磁盘缓存预热很少见。
那么,我们如何进一步改善解决方案呢?
处理二进制响应
我们在这里的实现不会麻烦缓存非文本的东西,例如图像,但是没有理由不这样做。 我们将需要更多代码。 特别是,我们可能想存储有关Blob的更多信息。 每个响应基本上都是Blob。 对于文本和JSON,它只是一个字符串数组。 type
和size
并不重要,因为您可以从字符串本身中找出它。 对于二进制内容,必须将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上发布-但这又是另外一次!
api接口 数据获取 缓存