2016-08-27

[译] Fetch 请求的本地缓存

原文作者: Peter Bengtsson
原文地址: https://www.sitepoint.com/cache-fetched-ajax-requests/
译文地址: http://www.wemlion.com/post/cache-fetched-ajax-requests
本文由 文蔺 翻译,转载请保留此声明。
著作权属于原作者,本译文仅用于学习、研究和交流目的,请勿用于商业目的。

本文展示了如何使用实现 fetch 请求的本地缓存,遇到重复请求时,将会从 sessionStorage 中读取数据。这样做的好处是,无需为每个需要缓存的资源编写自定义代码。

如果你想在 JavaScript 盛会中露露脸,秀秀如何玩转 Promise、最前沿的 API 和 localStorage,那就接着往下看吧。

Fetch API

此时此刻,你对 fetch 可能已经很熟悉了。它是浏览器提供的用以替代旧版的的原生 API。

并非所有浏览器都完美支持 fetch,但你可以使用 GitHub 上的 fetch polyfill(如果没事做,可以看看 Fetch 标准)。

原始替代版本

做个假设,我们准确了解需要下载的那个资源,并且只想下载一次。可以使用全局变量作为缓存,像下面这样:

On CodePen

上面使用了全局变量来保存缓存的数据。马上可以发现问题,一旦刷新页面或者跳转到其他页面,缓存的数据就消失了。

在剖析这个办法的短板之前,先将解决方案升级下。

On CodePen

第一个问题是, 是基于 Promise 的,意味着我们无法准确知晓 fetch 何时完成,因此在 fetch 完成之前,我们不能依赖它的执行。

第二个问题是,该解决方案详细指定了 URL 和缓存的内容(本例中的 )。我们需要一个基于 URL 的通用解决方案。

第一次的简单实现

外面再包装一层,同样也返回 Promise。调用该方法时,我们并不关心结果是来源于网络还是本地缓存。

之前你可能是这样做的:

On CodePen

现在加上一层包装,重复的网络请求可以通过本地缓存进行优化。我们将这个包装过的方法简单称作 ,代码如下:

该方法首次运行的时候,需要发出网络请求,并将结果缓存下来。第二次请求时,则会直接从本地存储中取出数据。

首先试试简单地将 包装下:

On CodePen

这当然能工作,不过没什么用。接下来,来实现获取数据的存储

On CodePen

上面发生了不少事。

所返回的首个 Promise 实际上还是径直发出了 GET 请求。注意如果有 CORS(Cross-Origin Resource Sharing,跨域资源共享)的问题, 这些方法不会工作。

最有意思的点在于,我们需要克隆首个 Promise 返回的 Response 对象。如果不这样做,我们就介入过多,当该 Promise 的最终使用者调用如 这些方法时,会得到如下错误:

另外需要注意的一点是,需要注意响应类型:我们只存储状态码为 内容类型为 的响应。因为 只能存储文本数据。

下面是使用示例:

让人喜欢的是,这个解决方案到目前为止可以正常工作,也不会干扰 JSON HTML 请求。当数据为图片的时候,它也不会试图将其存在 中。

真实返回命中缓存的第二次实现

我们的第一次实现,仅仅只关心响应结果的存储。当你第二次调用 时,并未试着从 检索任何内容。我们要做的,首先是返回一个 Promise,它需要返回一个 Response 对象

先看下最基本的实现:

On CodePen

这已经可以工作了!

打开 CodePen 查看上面代码的实际效果,记得开启浏览器开发者工具中的 Network tab。多点几次 “Run” 按钮(CodePen 的右上角),可以发现,只有图片被反复请求。

本解决方案的好处是避免了“意面式回调”(callback spaghetti)。 的调用是同步的(也就是阻塞的),所以在 Promise 或者回调中无需应对“它在本地存储中是否存在?”这种问题。只要有内容,就返回缓存结果。否则就按正常逻辑执行。

考虑失效时间的第三次实现

到目前为止我们一直在使用 ,它有点像 ,除了在打开新页面时会被清除这一点。这意味着我们在使用一种“自然形式”,内容不会缓存很久。如果要使用 来缓存内容,那就算远程内容改变了,浏览器还是会“永远”卡在本地内容。这太糟糕了。

更好的解决办法是提供用户控制。(这里的用户指的是使用 函数的 Web 开发者。)就像 Memcached 或 Redis 这些服务端存储一样,我们可以指定缓存的使用期。

例如在 Python (with Flask) 中:

对此,目前 都没有内建的功能实现,所以需要自己手动来实现。通过对比存储与缓存命中时的时间戳,可以达成目的。

在此之前,先看看大概应该长什么样子:

最重要的来了,每次保存响应数据的时候,需要记录何时存储的。现在我们也可以切换到 上了。代码会保证我们不会命中过期的缓存,在 中内容原本是持久化的。

下面是最终的解决方案:

On CodePen

未来更好、更理想、更酷的实现

我们在避免过度变动 Web API,最棒的是 可比依赖网络快得多了。看看这篇文章对 和 XHR 的比较: localForage vs. XHR。它还衡量了其他内容,但得出基本结论, 确实很快,磁盘缓存热身(disk-cache warm-ups,?不知如何翻译,请读者赐教)也很少出现。

接下来,我们还能怎样改进方案呢?

处理二进制响应

我们的实现没有考虑缓存非文本的内容,如图片等等,但这并非不可能。需要一些更多的代码。特别的,我们可能想存储更多关于 Blob 的信息。从根本上说,所有响应都是 Blob。对文本和 JSON 来说,它只是字符串数组, 并不真正那么重要,因为从字符串本身就能识别出来。对二进制内容而言,需要将它们转换为 ArrayBuffer

关注更多内容,请看 CodePen 上支持图片的实现。

使用哈希键值缓存

另外一点潜在的优化点是对用作 key 的每个 URL 进行哈希处理,使其变得更小,以空间换取速度(trade space for speed)。在上面的例子中,我们使用了很多非常短小整洁的 URL(如 ),但如果你使用了大量的带有很多查询字符串的长 URL,这样做就很有意义了。

办法之一是使用这个不错的算法,以其安全快速而知名:

如果觉得这个不错,看下 CodePen。在控制台上可以看到类似 这样的 key 值。

结语

现在我们拥有了一个可以使用在 web app 中的工作方案了,我们使用 Web API,并且知晓响应结果会很好地为用户缓存下来。

最后一件事大概是这个扩展置于本文之外,将其作为一个真实、具体的项目,加上测试和 ,并发布到 npm 上 —— 换个时间再做吧!