[译] Fetch 请求的本地缓存
原文作者:Peter Bengtsson
原文地址:https://www.sitepoint.com/cache-fetched-ajax-requests/
中文翻译:文蔺
译文地址:http://www.wemlion.com/2016/cache-fetched-ajax-requests/
本文由 @文蔺 翻译,转载请保留此声明。
著作权属于原作者,本译文仅用于学习、研究和交流目的,请勿用于商业目的。
本文展示了如何使用实现 fetch 请求的本地缓存,遇到重复请求时,将会从 sessionStorage 中读取数据。这样做的好处是,无需为每个需要缓存的资源编写自定义代码。
如果你想在 JavaScript 盛会中露露脸,秀秀如何玩转 Promise、最前沿的 API 和 localStorage,那就接着往下看吧。
Fetch API
此时此刻,你对 fetch 可能已经很熟悉了。它是浏览器提供的用以替代旧版的XMLHttpRequest
的原生 API。
并非所有浏览器都完美支持 fetch,但你可以使用 GitHub 上的 fetch polyfill(如果没事做,可以看看 Fetch 标准)。
原始替代版本
做个假设,我们准确了解需要下载的那个资源,并且只想下载一次。可以使用全局变量作为缓存,像下面这样:
let origin = null |
上面使用了全局变量来保存缓存的数据。马上可以发现问题,一旦刷新页面或者跳转到其他页面,缓存的数据就消失了。
在剖析这个办法的短板之前,先将解决方案升级下。
fetch('https://httpbin.org/get') |
第一个问题是,fetch
是基于 Promise 的,意味着我们无法准确知晓 fetch 何时完成,因此在 fetch 完成之前,我们不能依赖它的执行。
第二个问题是,该解决方案详细指定了 URL 和缓存的内容(本例中的 information
)。我们需要一个基于 URL 的通用解决方案。
第一次的简单实现
在 fetch
外面再包装一层,同样也返回 Promise。调用该方法时,我们并不关心结果是来源于网络还是本地缓存。
之前你可能是这样做的:
fetch('https://httpbin.org/get') |
现在加上一层包装,重复的网络请求可以通过本地缓存进行优化。我们将这个包装过的方法简单称作 cachedFetch
,代码如下:
cachedFetch('https://httpbin.org/get') |
该方法首次运行的时候,需要发出网络请求,并将结果缓存下来。第二次请求时,则会直接从本地存储中取出数据。
首先试试简单地将 fetch
包装下:
const cachedFetch = (url, options) => { |
这当然能工作,不过没什么用。接下来,来实现获取数据的存储。
const cachedFetch = (url, options) => { |
上面发生了不少事。
fetch
所返回的首个 Promise 实际上还是径直发出了 GET 请求。注意如果有 CORS(Cross-Origin Resource Sharing,跨域资源共享)的问题,.text()
、.json()
、.blob()
这些方法不会工作。
最有意思的点在于,我们需要克隆首个 Promise 返回的 Response 对象。如果不这样做,我们就介入过多,当该 Promise 的最终使用者调用如 .json()
这些方法时,会得到如下错误:
TypeError: Body has already been consumed. |
另外需要注意的一点是,需要注意响应类型:我们只存储状态码为 200
且内容类型为 application/json
或 text/*
的响应。因为 sessionStorage
只能存储文本数据。
下面是使用示例:
cachedFetch('https://httpbin.org/get') |
让人喜欢的是,这个解决方案到目前为止可以正常工作,也不会干扰 JSON 与 HTML 请求。当数据为图片的时候,它也不会试图将其存在 sessionStorage
中。
真实返回命中缓存的第二次实现
我们的第一次实现,仅仅只关心响应结果的存储。当你第二次调用 cachedFetch
时,并未试着从 sessionStorage
中检索任何内容。我们要做的,首先是返回一个 Promise,它需要返回一个 Response 对象。
先看下最基本的实现:
const cachedFetch = (url, options) => { |
这已经可以工作了!
打开 CodePen 查看上面代码的实际效果,记得开启浏览器开发者工具中的 Network tab。多点几次 “Run” 按钮(CodePen 的右上角),可以发现,只有图片被反复请求。
本解决方案的好处是避免了“意面式回调”(callback spaghetti)。sessionStorage.getItem
的调用是同步的(也就是阻塞的),所以在 Promise 或者回调中无需应对“它在本地存储中是否存在?”这种问题。只要有内容,就返回缓存结果。否则就按正常逻辑执行。
考虑失效时间的第三次实现
到目前为止我们一直在使用 sessionStorage
,它有点像 localStorage
,除了在打开新页面时会被清除这一点。这意味着我们在使用一种“自然形式”,内容不会缓存很久。如果要使用 localStorage
来缓存内容,那就算远程内容改变了,浏览器还是会“永远”卡在本地内容。这太糟糕了。
更好的解决办法是提供用户控制。(这里的用户指的是使用 cachedFetch
函数的 Web 开发者。)就像 Memcached 或 Redis 这些服务端存储一样,我们可以指定缓存的使用期。
例如在 Python (with Flask) 中:
>>> from werkzeug.contrib.cache import MemcachedCache |
对此,目前 sessionStorage
和 localStorage
都没有内建的功能实现,所以需要自己手动来实现。通过对比存储与缓存命中时的时间戳,可以达成目的。
在此之前,先看看大概应该长什么样子:
// 使用默认过期时间,如 5 min |
最重要的来了,每次保存响应数据的时候,也需要记录何时存储的。现在我们也可以切换到 localStorage
上了。代码会保证我们不会命中过期的缓存,在 localStorage
中内容原本是持久化的。
下面是最终的解决方案:
const cachedFetch = (url, options) => { |
未来更好、更理想、更酷的实现
我们在避免过度变动 Web API,最棒的是 localStorage
可比依赖网络快得多了。看看这篇文章对 localStorage
和 XHR 的比较: localForage vs. XHR。它还衡量了其他内容,但得出基本结论,localStorage
确实很快,磁盘缓存热身(disk-cache warm-ups,?不知如何翻译,请读者赐教)也很少出现。
接下来,我们还能怎样改进方案呢?
处理二进制响应
我们的实现没有考虑缓存非文本的内容,如图片等等,但这并非不可能。需要一些更多的代码。特别的,我们可能想存储更多关于 Blob 的信息。从根本上说,所有响应都是 Blob。对文本和 JSON 来说,它只是字符串数组,type
和 size
并不真正那么重要,因为从字符串本身就能识别出来。对二进制内容而言,需要将它们转换为 ArrayBuffer。
关注更多内容,请看 CodePen 上支持图片的实现。
使用哈希键值缓存
另外一点潜在的优化点是对用作 key 的每个 URL 进行哈希处理,使其变得更小,以空间换取速度(trade space for speed)。在上面的例子中,我们使用了很多非常短小整洁的 URL(如 https://httpbin.org/get
),但如果你使用了大量的带有很多查询字符串的长 URL,这样做就很有意义了。
办法之一是使用这个不错的算法,以其安全快速而知名:
const hashstr = s => { |
如果觉得这个不错,看下 CodePen。在控制台上可以看到类似 557027443
这样的 key 值。
结语
现在我们拥有了一个可以使用在 web app 中的工作方案了,我们使用 Web API,并且知晓响应结果会很好地为用户缓存下来。
最后一件事大概是这个扩展置于本文之外,将其作为一个真实、具体的项目,加上测试和 README
,并发布到 npm 上 —— 换个时间再做吧!
原文作者:Peter Bengtsson
原文地址:https://www.sitepoint.com/cache-fetched-ajax-requests/
本译文仅用于学习、研究和交流目的,转载请保留原文出处