Notes icon indicating copy to clipboard operation
Notes copied to clipboard

初探 Service Worker

Open yleo77 opened this issue 8 years ago • 0 comments

这是一篇简单的介绍 Service Worker 以及如何使用的文章,这个东西也不复杂。主要分背景为什么它会出现如何使用 以及总结 四部分。

背景

当大家谈论 web 应用的时候,潜在里都清楚它有一个受限点:当没有网的时候,所有网站或应用打开都会出现「网页无法加载」或类似的文案提示,web 的这一特点,自它的诞生,一直持续到现在。

而现在,Service worker 的出现,正是为了解决这一天然缺陷,或者更准确得说叫做突破这一限制,使得 web 应用也能够如同 Native APP 一样,即使离线,页面也不会出现无法加载的局面。

为什么它会出现

源于现在绝大多数的 web HTML5 应用都是严重依赖于网络,没有网络 HTML5 便会完全瘫痪。而借助于 service worker,即使再没有网络的情况下,也可以方便打开 HTML5 应用。

那它和之前的 AppCache 又有什么区别?

  • AppCache 在设计上存在一些缺陷,诸如更新过程中一个文件更新失败,则本次更新失败;诸如是基于配置式的,不够灵活,这也就是为什么在这个年代很多人喜欢用 localStorage 来做缓存的原因,可编程宜控制。
  • AppCache 存在着诸多的规则,在不清楚这些规则的情况下,很容易出现预期之外的效果;而Service worker 借助于 CacheStorage 在熟悉 API 的情况下一切都可以尽在掌握之中。

总的来说,AppCache 还是不够宜上手,即便在浏览器都广泛支持的情况。

怎么用?

先附一张 Service worker 的流程图以及示例代码 ,可以结合下文对照着来看。

worker lifecycle

via: MDN

首先,在需要使用 service-worker 的页面中注册它。

// 主页面  
// sw.js 为 service-worker 的主要代码。
navigator.serviceWorker.register('sw.js', {
  // scope 的作用域,最上级目录不能超过当前目录
  scope: './'
});

注册好之后,页面便会尝试安装 service-worker, 此时会触发 service-worker 的 install 事件,一般会在该阶段做一些 初始化,例如缓存文件的操作;

// sw.js
this.addEventListener('install', function(event) {
  event.waitUntil(caches.open('sw-storage').then(function(cache) {
    return cache.addAll([
      './',
      'script.js',
      'style.css',
    ]);
  }));
});

我们看到了非常熟悉的 then 方法调用,没错 Service Worker 依赖于 Promise 的实现。那 caches 是什么?event.waitUntil 又是什么?未曾熟知的 API,一一来看。

caches 放在下面单独来看。event.waitUntil 如文档描述:

the ExtendableEvent.waitUntil() method extends the lifetime of the event

对比着来看,如果不借助于event.waitUntil,那么 install 回调函数中同步代码执行完毕后就会立刻改变 service worker 的状态;反之,会等待 install 回调函数中的 promise 被 resolve 或 reject 后才会改变。

备注:在 sw.js 的上下文中,service worker 的安装状态可以通过this.registration.installing拿到。

在 install 事件触发之后,便会到达 service-worker 的第二个事件响应: 激活 activate。在 activate 阶段,可以做一些删除旧缓存的操作;

this.addEventListener('activate', function(event) {
  // console.log(‘ service worker:  activate event is fired’ );
});

此外,注册 Service Worker 的整个流程中,还有一个状态:redundant 当注册失败时会触发。

Service Worker 的初始化就算是马上就要完成了,还差最后一步:监听 fetch 事件。为什么 service-worker 能够不通过网络请求加载资源以及即便是离线状态下也可以使用的奥秘就在这里:

this.addEventListener('fetch', function(event) {
  event.respondWith(
  // 拦截fetch请求,从caches 中匹配并返回
    caches.match(event.request).then(function(response) {
      return response || fetch(event.request)
  // 如果没有匹配到,则通过 fetch API 从网络获取
  }).catch(function() {
    return fetch(event.request);
      })
  );
});

那么不监听fetch事件的后果是什么呢?所有的请求都走正常的网络请求了。

到此,以上的示例就展示了从注册到激活再到 监听,拦截 fetch 请求返回缓存的资源文件整个主体流程,但还差了一个重要环节:当网站静态资源更新之后,如何更新客户端中已经被缓存的静态资源?

先回顾下上面已经提到的两步: install -> activate

现在来看这个问题,每次页面刷新,都会去服务器请求 sw.js ,当发现服务器返回的该文件和本地不一致时,便会触发 service 的 install 事件监听,注意此时也只是安装,并不会激活。激活需要一个条件:当前不存在任何已经加载的页面再使用旧版本的 service-worker,当激活成功后,新版本的 service worker 便开始正常工作。

因为激活的条件,所以在实际的调试过程中,会发现,当新版本的注册好之后,我们需要关闭当前的已打开的标签页,必须通过重新开新标签页的形式才能完成激活。

基于我们上面 sw.js 代码的最简实现,每一次刷新页面,我们通过观察 Chrome Devtools 中 Network 面板的瀑布流时,会发现这样的现象:

  • 每一次,都会请求 sw.js 文件 ( 必须);
  • 当 sw.js 有更新时,此时会重新下载需要缓存的静态资源 (因为install 中有调用 cache.addAll 这样的 API 缓存资源);

sw-network

如上图所示,当使用了 Service worker 之后,fetch 会被 service worker 劫持,所以网络面板的请求,Size 一列都变成了from ServiceWorker

关于 CacheStorage 及 Cache

在看官方例子的时候,可能大家已经注意到了,上面的代码示例中出现了cache.addAll 以及 caches.open 这样的方法调用,却也没有看到它的声明,这又是什么?从 caches 来入手。

caches 存在于在ServiceWorkerGlobalScope 作用域中(也就是上文提到的 sw.js 这个文件的执行环境),它是 CacheStorage 的一个快捷方式,关联着当前的 service worker,只读属性。CacheStorage 是什么?

The CacheStorage interface represents the storage for Cache objects. It provides a master directory of all the named caches that a ServiceWorker, other type of worker or window scope can access (you don't have to use it with service workers, even though that is the spec that defines it) and maintains a mapping of string names to corresponding Cache objects.

直白得说,它就和 localStorage 一样,只是它专门用来缓存 HTTP 的 Response 对象。它的规范也是定义在 Service Worker 中,这意味着它的使用脱离不了 Service Worker 的上下文。

cache 则是 CacheStorage 通过调用 open 方法生成的一个 Cache 实例。在该实例上便可通过调用 add 或者 put 方法进行存储 Response。

// 在 ServiceWorkerGlobalScope 中
caches.open('sw-storage').then(function(cache) {
  return cache.add('script.js');
})

如何集成进现有项目

那么,如何在现有的项目中集成进 Service Worker呢?也是 So easy,将代码示例中的部分片段做少许调整就可以。当然了有更方便的方法:

  • 如果使用了类似 Webpack 构建工具的工程,只需要在 webpack 的配置文件中增加 sw-precache-webpack-plugin ,根据官方配置做简单配置即可;
  • 如果工程中并没有使用到构建工具,可以将 GoogleChrome 团队出的sw-precache 安装在本地,之后在项目目录下执行一条命令(需要一些配置),便会生成 service-worker.js 文件,之后引入在 html 中就大功告成。

BYW,第一种方案也是基于sw-precache 实现的。

总结

遗憾的是 Service Worker 各个浏览器支持的并不太好。截至到 iOS 11,也尚未支持,Android 倒好一些。几乎可以肯定的是,一旦这项技术被广大浏览器厂商支持,那么它对 Web 应用将会带来质的飞跃,激进得看,甚至会因此而将 Web 从此划分为两个时代。

is ServiceWorker Ready?

TODO

  • [ ] 结合 PWA 中的其他技术点看看 service work 对未来 web 的影响

参考资料

yleo77 avatar Jun 14 '17 11:06 yleo77