性能优化小册 - 可编程式缓存:Service Workers

Service Workers 不仅可以应用于 PWA ,PC 端也可以利用其强大的功能实现一些有趣的优化,网络中有很多关于 Service Workers 介绍的比较好的文章,基于小册宗旨,本文并不是一篇 Service Workers 的详细教程。

一些注意的点

Service worker 是一个注册在指定源和路径下的事件驱动 worker

它运行在 worker 上下文中,不能访问 DOM,相对于驱动应用的主 JavaScript 线程,它运行在其他线程中,所以不会造成阻塞。

它采用 JavaScript 控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。你可以完全控制应用在特定情形(最常见的情形是网络不可用)下的表现。

它设计为完全异步,同步 API(如 XHR 和 localStorage)不能在 Service worker 中使用。

在已经支持 serivce worker 的浏览器的版本中,很多特性没有默认开启,

了解浏览器对 serivce worker 的支持性。

如果你发现示例代码在当前版本的浏览器中怎么样都无法正常运行,你可能需要开启一下浏览器的相关配置

另外,需要注意的是,出于安全原因 Service Workers 要求必须在 HTTPS 下才能运行,为了便于本地开发,localhost 也被浏览器认为是安全源。

注册 Service Worker

使用 ServiceWorkerContainer.register() 函数来注册站点的 service workerservice sorker 只是一个 JavaScript 脚本)。

注意,这个文件的 url 是相对于 origin, 而不是相对于引用它的那个 JS 文件。
<!-- index.html -->
<script>
if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("/serviceworker.js")
    .then(function(reg) {
      if(reg.installing) {
        console.log('Service worker installing');
      } else if(reg.waiting) {
        console.log('Service worker installed');
      } else if(reg.active) {
        console.log('Service worker active');
      }
    })
    .catch(console.error);
}
</script>

配置缓存清单

Service Worker 注册之后,浏览器会尝试为你的页面或站点安装并激活它。

install 事件会在注册完成之后触发,install 事件一般是被用来填充你的浏览器的缓存能力。

为了达成这个目的,我们使用了 Service Worker 的新的标志性的存储 API — Cache,一个 service worker 上的全局对象,它使我们可以存储网络响应发来的资源,并且根据它们的请求来生成 key

Cache 接口像 workers 一样,是暴露在 window 作用域下的,尽管它被定义在 service worker 的标准中, 但是它不必一定要配合 service worker 使用。
// serviceworker.js
const cacheName = 'my-cache';
// self 代表 worker 线程自身,即子线程的全局对象
self.addEventListener('install', event => {
  // 用来存放缓存的静态资源和路由
  const filesToCache = [
    '/',
    '/static/css/reset.css',
    '/static/css/css-loader.css',
    '/static/css/create-version.css',
    '/static/css/bootstrap-grid.css',
    '/static/css/bootstrap.css',
    '/static/js/css-animations.js',
    '/static/js/vue.js',
  ];
  event.waitUntil(
    // caches 是 CacheStorage 的实例子:caches instanceof CacheStorage -> true
    // 使用 CacheStorage.open(cacheName) 打开一个 Cache 对象
    caches.open(cacheName)
    // 将字符串 URL 数组添加到缓存中
    .then(cache => cache.addAll(filesToCache))
    .catch(e => console.error(e))
  );
});

这里我们新增了一个 install 事件监听器,接着在事件上接了一个 ExtendableEvent.waitUntil() 方法。

waitUntil() 用来确保 service worker 不会在 waitUntil() 里边的代码执行完毕之前安装完成。

如果 caches.open(cacheName)rejected,安装就会失败,这个 worker 不会做任何事情(例如:URL 拼写错误)。

注意:首次注册/激活 Service Worker 线程的页面需要再次加载才会受其控制(二次生效)。在成功安装完成并处于激活状态之前,Service Worker 线程不会收到 fetch (下文会提到)和 push 事件。

使用 fetch 拦截请求

现在已经将站点资源缓存了,还需要告诉 Service Worker 让它用这些缓存内容来做点什么,我们可以借助 fetch API 进行一层拦截。

//fetch 事件处理程序,拦截请求并应用于所有缓存中的静态资产
self.addEventListener('fetch', e => {
    e.respondWith(
        caches.match(e.request)
        .then(response => response ? response : fetch(e.request))
    )
});

caches.match(event.request) 允许我们对网络请求的资源和 cache 里可获取的资源进行匹配,查看是否缓存中有相应的资源。这个匹配通过 urlvary header 进行,就像正常的 http 请求一样。

查看 Fetch API documentation 了解更多有关 Request 和 Response 对象的更多信息。

当匹配到 catch 资源时,caches.match(event.request) 就会 resolve,在 then 回调中就可以直接返回 response

caches.match(e.request)
  .then(response => response)

如果没有匹配到资源,caches.match(event.request) 就会 reject,可以告诉浏览器直接使用 fetch 进行默认的网络请求。(意味着在网络可用的时候可以直接像服务器请求资源)

caches.match(e.request)
  .then(response => response ? response : fetch(e.request))

更新缓存

更新缓存清单,可以借助 activate 事件进行处理,本文案例代码并没有对其进行实现。
只是使用 clients.claim() 对页面进行控制权获取,这样之后打开页面都会使用版本更新的缓存。

self.addEventListener('activate', e => self.clients.claim());

优化后的效果

Chrome 有一个 chrome://inspect/#service-workers 可以展示当前设备上激活和存储的 service worker。还有个 chrome://serviceworker-internals 可以展示更多细节来允许你开始/暂停/调试 worker 的进程。

通过 Chrome devToolsApplication Tab 我们可以查看当前服务工作线程的运行情况。

serviceworker.js 完整代码:


const cacheName = 'my-cache';
// self.clients.claim() 取得页面控制权,这样之后打开页面都会使用版本更新的缓存
self.addEventListener('activate', e => self.clients.claim());

self.addEventListener('install', event => {
   // 用来存放缓存的静态资源和路由
  const filesToCache = [
    '/',
    '/static/css/reset.css',
    '/static/css/css-loader.css',
    '/static/css/create-version.css',
    '/static/css/bootstrap-grid.css',
    '/static/css/bootstrap.css',
    '/static/js/css-animations.js',
    '/static/js/vue.js',
  ];
  
  event.waitUntil(
    // caches 是 CacheStorage 的实例子:caches instanceof CacheStorage -> true
    // 使用 CacheStorage.open(cacheName) 打开一个 Cache 对象
    caches.open(cacheName)
    // 将 filesToCache (字符串 URL 数组)添加到缓存中
    .then(cache => cache.addAll(filesToCache))
    .catch(e => console.error(e))
  );
});
// fetch 进行拦截
self.addEventListener('fetch', e => {
    e.respondWith(
        caches.match(e.request)
        .then(response => {
         return  response ? response : fetch(e.request)
        })
    )
});

延伸阅读推荐:这篇文章

同系列文章:

相关文献

阅读 569

推荐阅读
前端 1943
用户专栏

前端技术文章

2299 人关注
18 篇文章
专栏主页
目录