Caching Files With Service Worker

504 阅读3分钟

浏览器的缓存是把双刃剑,使用得当的话,可以加快页面的加载速度,减少向服务器的请求次数,进而减少带宽和服务器压力。但是如果使用不得当的话,会造成新功能或者问题修复未能正常生效等问题,用户体验有所下降。常见的缓存方式包括强制缓存和协商缓存,分别用在不同情境下,通过相关的Headers控制(感兴趣的可以看之前HTTP Headers文章中关于缓存的介绍),也是大多数网站的首要技术选择。那么还有什么其他缓存相关的技术可以使用吗?让我们来关注一下Service Worker

快速入门

Service Worker旨在通过代码精确控制缓存文件和HTTP请求,是已经被废弃掉的AppCache技术的替代方案。Service Worker有相关的生命周期概念,如下所示:

实际案例

我们来看一下语雀是如何使用Service Worker来缓存相关内容的。

注册Service Worker

ServiceWorkerContainernavigator下,所以先判断是否支持Service Worker,不支持的话另做处理

if ('serviceWorker' in navigator) {
  // Register a service worker hosted at the root of the
  // site using the default scope.
  navigator.serviceWorker.register('/serviceworker.js').then(function(registration) {
    console.log('Service worker registration succeeded:', registration);
  }, /*catch*/ function(error) {
    console.log('Service worker registration failed:', error);
  });
} else {
  console.log('Service workers are not supported.');
}

ServiceWorkerGlobalScope逻辑处理

在Worker内不存在全局变量window取而代之的是self,这里注册了两个Scope变量assetsresourceBase,并用importScripts引入新的逻辑处理

self.assets = [
  'https://gw.alipayobjects.com/os/chair-script/skylark/common.9795d8b0.chunk.css',
  'https://gw.alipayobjects.com/os/lib/??react/16.13.1/umd/react.production.min.js,react-dom/16.13.1/umd/react-dom.production.min.js,react-dom/16.13.1/umd/react-dom-server.browser.production.min.js,moment/2.24.0/min/moment.min.js',
];
self.resourceBase = 'https://gw.alipayobjects.com/os/chair-script/skylark/';
importScripts(
  'https://gw.alipayobjects.com/os/chair-script/skylark/serviceworker.d050b459.js'
);

值得注意的是语雀把react,react-dom,moment等不常变更的依赖放在了Service Worker内,实现了另一种形式的app vendor chunk

处理缓存

主要逻辑如下所示:

 self.addEventListener("install", (e => {
        Array.isArray(self.assets) && e.waitUntil(caches.open("v1").then((e => {
            e.addAll(self.assets)
        })))
    })), self.addEventListener("activate", (e => {
        Array.isArray(self.assets) && caches.open("v1").then((e => {
            e.keys().then((t => {
                t.forEach((t => {
                    self.assets.includes(t.url) || e.delete(t)
                }))
            }))
        }))
    }));
    const r = [self.resourceBase, "https://at.alicdn.com/t/", "https://gw.alipayobjects.com/os/"];
    self.addEventListener("fetch", (e => {
        r.some((t => e.request.url.startsWith(t))) && e.respondWith(caches.match(e.request).then((t =>
            t && 200 === t.status ? t : fetch(e.request).then((t => {
                if (200 !== t.status) return t;
                const r = t.clone();
                return caches.open("v1").then((t => {
                    t.put(e.request, r)
                })), t
            })).catch((() => {})))))
    }))

这里实现了如下逻辑:

  1. 在install阶段调用cache.addAll()self.assets内的文件缓存
  2. 在active阶段将新旧的self.assets进行对比,并将失效的缓存删除掉: self.assets.includes(t.url) || e.delete(t)
  3. 注册fetch事件监听,如果请求url在[self.resourceBase, "https://at.alicdn.com/t/", "https://gw.alipayobjects.com/os/"]内则从缓存内拿相应的文件

可以看出语雀使用Service Worker进行缓存,整体的逻辑简单明了,并没有什么复杂高深的内容在里面。

其它应用

我们从语雀处理缓存逻辑时的fetch事件监听可以看出来,fetch可以做的不止从缓存内拿相应的文件这么简单,我们可以完全控制整个fetch过程。那么可以做的事情就比较多了: 1. 缓存一个离线的html,当检测到无网络时,展示相应的html内容,提升用户体验 2. 对请求不到资源的情况做错误处理,展示相应的内容 3. 将所有图片换成我的支付宝付款码🙈

总结

Service Worker做缓存还是挺有用的,相比较Cache-Control之类的Headers来做缓存控制而言,拥有更细粒度的控制过程,并且可以做相应的错误和降级处理。但是依然需要注意缓存带来的时效性问题,否则得不偿失。

参考链接