Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

浏览器缓存、CacheStorage、Web Worker 与 Service Worker #113

Open
youngwind opened this issue Feb 6, 2018 · 14 comments
Open

浏览器缓存、CacheStorage、Web Worker 与 Service Worker #113

youngwind opened this issue Feb 6, 2018 · 14 comments

Comments

@youngwind
Copy link
Owner

youngwind commented Feb 6, 2018

前言

最近在翻红宝书,看到 Web Worker 那章,猛然意识到,通过它竟然可以把几个缓存相关的概念串起来,甚是有趣,撰文记之。最后我也写了一个完整的离线应用 Demo,以供运行调试。

浏览器缓存

传统意义上的浏览器缓存,分为强缓存协商缓存,其共同点都是通过设置 HTTP Header 实现。关于两者的异同已经被讨论得很多,我就不赘述了,附两个参考资料。

  1. 浏览器的缓存机制, By Aitter
  2. http协商缓存VS强缓存, By wonyun

这种浏览器缓存(我称之为 Header 缓存)有两个共同的缺点:

  1. 当没有网络的时候,应用无法访问,因为 HTML 页面总得去服务器获取。
  2. 缓存不可编程,无法通过 JS 来精细地对缓存进行增删改查。

应用缓存

为了在无网络下也能访问应用,HTML5 规范中设计了应用缓存(Application Cache)这么一个新的概念。通过它,我们可以做离线应用。然而,由于这个 API 的设计有太多的缺陷,被很多人吐槽,最终被废弃。废弃的原因可以看看这些讨论:

  1. 为什么app cache没有得到大规模应用?它有哪些硬伤吗?
  2. Application Cache is a Douchebag, By Jake Archibald

PS:我当年毕设也用到过这种技术,没想到短短几年就被废弃了,技术迭代何其之快也!

CacheStorage

为了能够精细地、可编程地控制缓存,CacheStorage 被设计出来。有了它,就可以用 JS 对缓存进行增删改查,你也可以在 Chrome 的 DevTools 里面直观地查看。对于传统的 Header 缓存,你是没法知道有哪些缓存,更加没法对缓存进行操作的。你只能被动地修改 URL 让浏览器抛弃旧的缓存,使用新的资源。

image

PS:CacheStorage 并非只有在 Service Worker 中才能用,它是一个全局性的 API,你在控制台中也可以访问到 caches 全局变量。

Web Worker

一直以来,一个网页只会有两个线程:GUI 渲染线程和 JS 引擎线程。即便你的 JS 写得再天花乱坠,也只能在一个进程里面执行。然而,JS 引擎线程和 GUI 渲染线程是互斥的,因此在 JS 执行的时候,UI 页面会被阻塞住。为了在进行高耗时 JS 运算时,UI 页面仍可用,那么就得另外开辟一个独立的 JS 线程来运行这些高耗时的 JS 代码,这就是 Web Worker

Web Worker 有两个特点:

  1. 只能服务于新建它的页面,不同页面之间不能共享同一个 Web Worker。
  2. 当页面关闭时,该页面新建的 Web Worker 也会随之关闭,不会常驻在浏览器中。

PS:还有一个相关的概念:Shared Worker,不过这个东西比较复杂,我并未深入研究,感兴趣的读者可以了解,也可以看看 Shared Worker 跟 Service Worker 的区别

Service Worker

终于说到本文的主角了。Service Worker 与 Web Worker 相比,相同点是:它们都是在常规的 JS 引擎线程以外开辟了新的 JS 线程。不同点主要包括以下几点:

  1. Service Worker 不是服务于某个特定页面的,而是服务于多个页面的。(按照同源策略)
  2. Service Worker 会常驻在浏览器中,即便注册它的页面已经关闭,Service Worker 也不会停止。本质上它是一个后台线程,只有你主动终结,或者浏览器回收,这个线程才会结束。
  3. 生命周期、可调用的 API 等等也有很大的不同。

总而言之,Service Worker 是 Web Worker 进一步发展的产物。关于如何使用 Service Worker,可以参考下面的资料。

  1. 借助Service Worker和cacheStorage缓存及离线开发, By 张鑫旭
  2. 使用Service Worker做一个PWA离线网页应用, By 会编程的银猪
  3. 【译】理解Service Worker, 作者 By Adnan Chowdhury, 译者 By 安秦

我也写了一个 Service Worker 用作离线应用的 Demo,大家可以调试观察。下面我们讨论几个 Service Worker 容易被忽略的地方,以我的 Demo 为例。

Service Worker 只是 Service Worker

一开始我以为 Service Worker 就是用来做离线应用的,后来渐渐研究才发现不是这样的。→ Service Worker 只是一个常驻在浏览器中的 JS 线程,它本身做不了什么。它能做什么,全看跟哪些 API 搭配使用

  1. 跟 Fetch 搭配,可以从浏览器层面拦截请求,做数据 mock;
  2. 跟 Fetch 和 CacheStorage 搭配,可以做离线应用;
  3. 跟 Push 和 Notification 搭配,可以做像 Native APP 那样的消息推送,这方面可以参考 villainhr 的文章:Web 推送技术
  4. ……

假如把这些技术融合在一起,再加上 Manifest 等,就差不多成了 PWA 了。
总之,Service Worker 是一种非常关键的技术,有了它,我们能更接近浏览器底层,能做更多的事情。

The idea is that we, as browser developers, acknowledge that we are not better at web development than web developers. And as such, we shouldn't provide narrow high-level APIs that solve a particular problem using patterns we like, and instead give you access to the guts of the browser and let you do it how you want, in a way that works best for your users.

出处:https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#handling_updates

初次访问不会触发 fetch 事件

按照官方给的 Demo,Service Worker 注册的代码是放在 HTML 的最后。但是,当我尝试把 Service Worker 的注册代码提到最开头,并且 console 出时间戳,我发现一个现象:即便 Service Worker 注册成功之后再请求资源,这些资源也不会触发 fetch 请求,只有再次访问页面才会触发 fetch 事件。这是为什么呢?后来我在官方文档中找到了答案:如果你的页面加载时没有 Service Worker,那么它所依赖的其他资源请求也不会触发 fetch 事件

The first time you load the demo, even though dog.svg is requested long after the service worker activates, it doesn't handle the request, and you still see the image of the dog. The default is consistency, if your page loads without a service worker, neither will its subresources. If you load the demo a second time (in other words, refresh the page), it'll be controlled. Both the page and the image will go through fetch events, and you'll see a cat instead.

出处:https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#activate

cache.add VS cache.put

在 install 事件中用 cache.addAll,在 fetch 事件中用 cache.put,add 和 put 有什么区别吗?→ cache.add = fetch + cache.put

The add() method of the Cache interface takes a URL, retrieves it, and adds the resulting response object to the given cache. The add() method is functionally equivalent to the following:

fetch(url).then(function(response) {
  if (!response.ok) {
    throw new TypeError('bad response status');
  }
  return cache.put(url, response);
})

出处:https://developer.mozilla.org/en-US/docs/Web/API/Cache/add

event.waitUntil 和 event.respondWith

先说 event.waitUntil

  1. 只能在 Service Worker 的 install 或者 activate 事件中使用;
  2. 看起来像是一个 callback,但是,即便你不使用它,程序也可能正常运行。如果你传递了一个 Promise 给它,那么只有当该 Promise resolved 时,Service Worker 才会完成 install;如果 Promise rejected 掉,那么整个 Service Worker 便会被废弃掉。因此,cache.addAll 里面,只要有一个资源获取失败,整个 Service Worker 便会失效。

再说 event.respondWith

  1. 只能在 Service Worker 的 fetch 事件中使用;
  2. 作用相当于一个 callback,当传入的 Promise resolved 之后,才会将对应的 response 返回给浏览器。

总之,虽然 event.waitUntil 和 event.respondWith 中的 event 都是继承于 Event 类,但是它们与常见的 event 对象差异很大,这些方法也只有在 Service Worker 的那些对应的事件中才存在

资源的更新

以前我们用强缓存的时候,如果资源需要更新,那么我们只需要改变资源的 URL,换上新的 MD5 戳就好了。如果使用 Service Worker + CacheStorage + Fetch 做离线应用,又该如何处理资源的更新呢?

  1. 当有任何的资源(HTML、JS、Image、甚至是 sw.js 本身)需要更新时,都需要改变 sw.js。因为有了 sw.js,整个应用的入口变成了 sw.js,而非原先的 HTML。每当用户访问页面时,不管你当前是不是命中了缓存,浏览器都会请求 sw.js,然后将新旧 sw.js 进行字节对比,如果不一样,说明需要更新。因此,你能看到在 Demo 中,我们有一个 VERSION 字段,它不仅代表 sw.js 本身的版本,更代表整个应用的版本

  2. 不要试图通过改变 sw.js 的名字(如改成 sw_v2.js)来触发浏览器的更新,因为 HTML 本身会被 sw.js 缓存,而缓存的 HTML 中永远都指向 sw.js,导致浏览器无法得知 sw_v2.js 的更新。虽然,你可以像上面提到的文章:使用Service Worker做一个PWA离线网页应用 那样,再结合其他的手段来判断 HTML 的更新状态,但是会更加复杂,官方并不推荐。

    you may consider giving each version of your service worker a unique URL. Don't do this! This is usually bad practice for service workers, just update the script at its current location.

    出处:https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#avoid_changing_the_url_of_your_service_worker_script

  3. 每次 sw.js 的更新,都会根据 VERSION 字段新建一个缓存空间,然后把新的资源缓存在里面。等到旧的 sw.js 所控制的网页全部关闭之后,新的 sw.js 会被激活,然后 在 activate 事件中删除旧缓存空间。这样既能保证在同时打开多个网页时更新 sw.js 不出差错,也能及时删除冗余的缓存

双重缓存

上面我们谈到,当新的 sw.js install 的时候,会重新 fetch addAll 里面的所有资源,不管里面的资源是否需要更新,这显然违背了 Web 增量下载的原则,怎么办呢? → 结合使用强缓存和 Service Worker,做一个双重缓存。强缓存在前, Service Worker 在后。举个例子,假如有两个强缓存 a_v1.js 和 b_v1.js,现在 a 不变,b 要改成 b_v2.js,修改 sw.js 的 addAll 和 VERSION。当新的 sw.js install 的时候,addAll 要 fetch a_v1.js ,但是浏览器发现 a_v1.js 是强缓存,所以根本不会发起网络请求,只有 b_v2.js 才会发起网络请求。具体的可以调试我的 Demo 查看现象。

关于这种方法,有两点要说明一下。

  1. 需要在 cache.addAll 中指定资源的版本号,就如同在 html 中指定那般。因为在使用 Service Worker 之后,HTML 只是加载资源的入口,判断资源是否改变的功能,已经转移到 sw.js 中了。
    return cache.addAll([
        './',
        'getList',
        'img/avatar_v1.jpg',
        'js/index_v2.js',
        'js/jquery_v1.js'
    ]);
  2. 上面提到的文章:使用Service Worker做一个PWA离线网页应用 中也有提到这种多重缓存的做法,但是作者认为浏览器会先读取 Service Worker,没有的话才会读取强缓存,这与我的 Demo 实践结果不相符。

总结

写到这儿,也差不多结束了,对于 Service Worker,我还有很多不懂的地方。围绕着 Service Worker 的这一系列新兴 API,代表着更好的 Web 体验,也代表着 Web 的未来,以后仍需多加关注学习。

@riskers
Copy link

riskers commented Feb 6, 2018

这几个技术结合起来就是 PWA 了

@youngwind
Copy link
Owner Author

的确如此。 @riskers

@riskers
Copy link

riskers commented Feb 6, 2018

@youngwind 最近一年一直在关注 PWA

@AnnaLEchevarria
Copy link

这种浏览器缓存(我称之为 Header 缓存)有两个共同的缺点:

“当没有网络的时候,应用无法访问,因为 HTML 页面总得去服务器获取。” 这句怎么理解,通过header设置,页面不是可以缓存下来吗

@youngwind
Copy link
Owner Author

youngwind commented Mar 2, 2018

是,通过设置 HTML 页面的 header,的确可以将 HTML 页面也设置为强缓存。然而,要考虑下面 2 个问题。

  1. 假如 HTML 要更新呢?你怎么办?你总不能像 js 文件那样换 MD5 戳吧,因为页面的 URL 只有一个。
  2. 即便你把 HTML 做成强缓存,当你断开网络时,你会发现你访问到的是一个“无网络”的页面。为什么呢?因为当网络断开时,浏览器默认是会显示“无网络”页面的,除非你这个页面进行了“可离线访问”的特殊操作,比如应用缓存需要在 html 标签中生命 manifest 属性,比如 service-worker 中需要监听 fetch 事件。

@zhaozhiwen2014

@AnnaLEchevarria
Copy link

@youngwind 谢谢 你文中 说的“最近在翻红宝书” 红宝书 是哪本书 求推荐

@jsm1003
Copy link

jsm1003 commented Mar 7, 2018

@zhaozhiwen2014 javascript 高级程序设计

@vevlins
Copy link

vevlins commented May 28, 2018

结合CacheStorage+Service Worker+diff算法应该可以实现更好的前端静态资源增量更新。

@youngwind
Copy link
Owner Author

你说的我倒是第一次听说,我在网上找了些资料:用增量更新算法为 web 应用节省流量,相关 PPT,这是你说的意思吗?当然,文中用的是 localStorage,你说的是 CacheStorage+Service Worker,不过关键的 diff 算法应该是一致的。
这种方案,有点类似于 Webpack 的热更新,不过 Webpack 的热更新只在开发环境用,不需要考虑实际环境的 Hash 等问题。
@vevlins

@vevlins
Copy link

vevlins commented May 28, 2018

对的。我是在思考静态资源到底是合并还是拆分想到了增量更新这个想法,我也看到了腾讯的这个开源项目,另外据说也有一些cdn做了这样的事情,但是这样需要对cdn整体做改动,不掌握在项目方自己手里。

另外这种直接弃用http缓存的方案我认为可能会造成一些隐性的问题,我对CacheStorage的了解不多,应该是相当于提供了js操作http缓存的能力吧。而且结合Service Worker还可以做一些资源有效性的批量查询等。

相对于localstorage的方案,这样似乎改动更小,对原有的缓存机制也没有做太大的颠覆,想要接入或者降级应该也会更稳妥一点。

@kokokele
Copy link

shaofeng 👍 这关注量

@lvming6816077
Copy link

有个问题请教一下,浏览器本身是有http的cache control缓存的,如果这个缓存没有过期,当页面发起一个请求时,是先经过fetch拦截还是浏览器本身的http缓存?

@Clarkkkk
Copy link

浏览器的缓存机制这篇文章的链接似乎已经过时,我找到了有效的链接:https://git-lt.github.io/2016/11/21/web-cache/

@Mica-Ma
Copy link

Mica-Ma commented Oct 23, 2020

Good

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants