Service Worker初探随笔

558 阅读5分钟

关于Service Worker印象

第一次意识到前端还可以做离线存储是在17年参加百度的面试;

第二次听说是在GMTC前端大会上听于秋老师讲美团金融支付的离线存储与增量更新方案;

第三次接触是调研Flomo笔记对PWA运用;

第四次运用是在做一个类CodeSandbox项目,在Service Worker中去处理ES Module Import发出的请求以及Vue文件的编译。

最近在学习Github1s也发现不少运用到了Service Worker,相识这么多年了,决定系统性的学习一下。搜了一些中文的教程,有一些质量挺不错的(见文末参考文章),但主要是偏向于最佳实践,对小白友好的入门指南反而少,于是就随意记录下自己作为新接触Service Worker小白在学习过程中印象深刻的一些点。

一些基础概览

developer.mozilla.org/en-US/docs/…

  • 运行在独立的线程,不会造成JavaScript主线程阻塞,自然也不能访问DOM
  • 完全基于异步的设计,因此如XHRWeb Storage的同步API是不能用的(不是很好理解)
  • 只可以运行在https环境(localhost例外)

注册

之前提到Service Worker是运行在独立的线程,自然引用方式也不同于常规JS脚本。你并不需要通过script标签去引用,而是直接通过JS BOM API去注册:

if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('sw.js', { scope: './' });
}

这里讲一下scope这个配置项,一般称呼其为“作用域”,默认值为Service Worker注册文件(即sw.js)所在的目录。第一印象以为是“允许被拦截资源的作用域”,于是尝试加载了一个远程CDN脚本并拦截缓存,发现一切OK。仔细翻了翻才理解这其实是“service worker逻辑生效的页面作用域”,并且scope只允许往小了设置,其默认值即为最大值。(这里不得不说,方法与实践方面许多中文教程还是不错的,基础知识真的是能读英文就读英文)

例如我们在https://example.com/index.html中注册Service Worker,scope设置为./story

navigator.serviceWorker.register('sw.js', { scope: './story' });
  • https://example.com/index.html
    不生效。(你没看错,即使是在这个文件里注册的Service Worker,但是不属于scope的范围之内,一样不生效)
  • https://example.com/other.html
    不生效
  • https://example.com/story/index.html
    生效

完整的Usage可以戳这里
developer.mozilla.org/en-US/docs/…

生命周期

Service Worker生命周期是一件挺复杂的事情,这里我只简单讲一下自己在学习过程中探索的一些点。完整的 解析可以参考一下Google开发者文档,写得比较全。 developers.google.com/web/fundame…

安装

用户首次访问应用时会下载、安装、激活Service Worker并触发installactivate事件。但除此之外,并不会执行其它的代码(比如对fetch事件的监听逻辑),直到你下次进入该页面或访问作用域内的其它页面。

更新

sw.js文件有改动时,用户再次访问会重新安装,即触发install事件,但并不会立即激活。此时打开Chrome开发者工具,可以看到如下图:image.png红框部分表示老版本的Service Worker#205正在运行,新版本的Service Worker#206等待激活。那新的Service Worker什么时候才会生效能?其实就是把所有使用老版本Service Worker的浏览器tab页都关闭了,再重新访问应用,自然就会激活并触发activate事件了。官方原文:

It is only activated when there are no longer any pages loaded that are still using the old service worker. As soon as there are no more pages to be loaded, the new service worker activates (becoming the active worker).

需要格外注意的是,由于install事件和activate事件触发和实际Service Worker生效并不一定在同一时间,所以在这些事件中写业务逻辑时需要考虑周全。
当然,以上说的这些都是默认行为,实际浏览器也提供了API去改变这个行为,具体可以参考:

基础能力

这里我会以代码加注释的形式简单讲讲Service Worker缓存资源与拦截请求这两个重要基础能力

缓存资源

缓存相关的API:developer.mozilla.org/en-US/docs/…

const urls = [
  'styles.css',
  'demo.js',
]
// self是可以理解为Service Worker里的global
self.addEventListener('install', event => {
  // event.waitUntil用来保持线程处于`installing`阶段,如果promise最终未能成功resolve,浏览器将会丢弃这个Service Worker
  event.waitUntil(
    // 起一个缓存对象
    caches.open('static-v1')
      .then(cache => {
        cache.addAll(urls) // 将这些文件添加至缓存中
      })
  );
});

此时去浏览器刷新页面,会发现似乎缓存的资源并没有被使用上。一般来说,一个系统的功能性与易用性是成反比的。Service Worker也是如此,它为我们提供了精确控制缓存的能力,自然,任何一个简单的缓存需求都需要我们手动去实现。上述代码我们只是实现了缓存资源,如何使用这些缓存的资源,也需要我们去实现,这里就牵扯出了Service worker的另一个重要能力——拦截请求。

拦截请求

要实现缓存资源的使用,我们需要拦截网页发出的请求,将请求的资源与缓存中的资源对比,如果是同一个则使用缓存的资源。

// fetch事件会拦截所有的请求
self.addEventListener('fetch', event => {
  // respondWith允许自定义返回请求的内容
  event.respondWith(async () => {
    // 将缓存的资源与请求的资源相匹配
    const cachedResponse = await caches.match(event.request);
    if (cachedResponse) return cachedResponse;
    // 如果没有缓存,则走正常请求
    return fetch(event.request);
  });
});

结语

基础能力之所以叫基础,就是因为在实际应用中可以在应用层玩出非常多的花来,比如我们非常熟悉Chrome小恐龙游戏,也可以通过Service Worker来实现。Google为我们提供了许多Service Worker的例子,参考 github.com/GoogleChrom… 除此之外,Service Worker也提供了例如消息推送Notifaction这样的应用层能力,这比较直接就不讨论了。

参考文章: