ServiceWorker上篇:应用与实践

背景

根据w3c draft对Service Worker(以下简称SW)的定位。SW是属于PWA全家桶的其中一项技术。传统Web加载document以及子资源是浏览器内部策略,开发者无法直接控制网络资源的加载,而Web Application标准是建立在网络资源可控的前提下,所以SW被提出用于平衡Web和Web Application之间的gap,SW负责提供在线或离线的资源给document,替换掉过时的Application Cache。
随着SW生命周期以及独立Script Context的完善,使得SW有能力承担Web后台Service的角色,例如Push通知、后台数据同步、网络资源管理、计算密集型任务等。某大厂小程序也使用过SW作为Service端载体。
IOS系统在2018年部分支持SW,但是在卸载策略、Push/Background Sync API标准与W3C标准不一样,跨平台开发需谨慎。另外因为SW有能力拦截网络请求,所以为了安全性,网站或Web Application需要支持HTTPS协议。

应用

Service Worker基本用法:

先上个简单Demo,代码https://github.com/mdn/sw-test/。Http Server可以用github内的python工具,也可以直接在Chrome中安装Web Server扩展,链接如下。
https://chrome.google.com/webstore/detail/web-server-for-chrome/ofhbbkphhbklhfoeikjpcbhemlocgigb?hl=en

service worker基本步骤:

  1. 页面index.html通过 serviceWorkerContainer.register(scriptUrl, scope) 注册service worker 。scriptUrl是SW js的url,scope是SW脚本执行的作用域,例如对于域名https://serviceworke.rs/ 创建两个iframe分别是controlled.html以及non-controlled.html,register(sw.js, {scope: './controlled'})使sw.js只能控制controlled.html的网络资源,而无法控制non-controlled.html。
  2. 如果注册成功,sw.js 就在 ServiceWorkerGlobalScope 环境中运行; 这是一个特殊类型的 woker 上下文运行环境,与主运行线程(register执行环境,也就是index.html)相独立,同时也没有访问 DOM 的能力。
  3. 注册成功后sw.js会触发service worker的install事件,此时可以使用IndexDB和Cache API预先缓存资源。
  4. oninstall事件的处理程序执行完毕后,可以认为 service worker 安装完成了。
  5. 安装完成后,会接收到一个激活事件(activate event)。 onactivate主要用途是清理先前版本的service worker 脚本中使用的过期资源,防止本地存储爆炸。
  6. Service Worker 现在可以控制页面了,但仅是在 register() 成功后打开的页面。也就是说,如果页面在service worker activate之前加载网络资源,则这些网络资源不会被service worker控制。所以,页面需要重新加载让 service worker 获得完全的控制。
  7. Service worker脚本通过监听fetch, push, sync API事件实现对页面的控制。

sw.js脚本更新方法:
Service Worker规范提供了skipWaiting以及update两种方式可以让开发者更新SW。
使用skipWaiting立即更新。注意:旧的sw.js业务逻辑会被立即杀掉,需要处理交接时的问题。具体见https://zhuanlan.zhihu.com/p/51118741

self.addEventListener('install', event => {
  self.skipWaiting();

  event.waitUntil(
    // caching etc
  );
});

使用update通知内核更新,具体时机由内核决定,例如navigations and functional events之后触发更新。

navigator.serviceWorker.register('/sw.js').then(reg => {
  // sometime later…
  reg.update();
});

Service worker生命周期的各个阶段以及可监听的事件见参考资料。
参考资料:https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API/Using_Service_Workers

网络资源控制:

Demo代码以及说明:
google offline cookbook

基本步骤:

  1. sw.js监听网络请求fetch事件。
  2. 在fetch回调中使用caches.open("sw_cache")打开一个命名的cache。
  3. 使用fetch API(区别于fetch事件)向网络请求资源,或者cache.get从缓存中取资源,用return将获取到的资源返回给内核。

控制模式:
通过fetch、cache以及Promiss搭配,可以对网络资源实现以下几种控制模式。

  1. 网络或缓存加载。
self.addEventListener('fetch', function(event) {
  // If a match isn't found in the cache, the response
  // will look like a connection error
  event.respondWith(caches.match(event.request));
});
self.addEventListener('fetch', function(event) {
  event.respondWith(fetch(event.request));
  // or simply don't call event.respondWith, which
  // will result in default browser behaviour
});
  1. 先网络后取缓存,vice versa。
self.addEventListener('fetch', function(event) {
  event.respondWith(
    fetch(event.request).catch(function() {
      return caches.match(event.request);
    })
  );
});
  1. 同时网络以及取缓存,快者优先。
// Promise.race is no good to us because it rejects if
// a promise rejects before fulfilling. Let's make a proper
// race function:
function promiseAny(promises) {
  return new Promise((resolve, reject) => {
    // make sure promises are all promises
    promises = promises.map(p => Promise.resolve(p));
    // resolve this promise as soon as one resolves
    promises.forEach(p => p.then(resolve));
    // reject if all promises reject
    promises.reduce((a, b) => a.catch(() => b))
      .catch(() => reject(Error("All failed")));
  });
};

self.addEventListener('fetch', function(event) {
  event.respondWith(
    promiseAny([
      caches.match(event.request),
      fetch(event.request)
    ])
  );
});
  1. 先取缓存再网络更新。
self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.open('mysite-dynamic').then(function(cache) {
      return cache.match(event.request).then(function(response) {
        var fetchPromise = fetch(event.request).then(function(networkResponse) {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        })
        return response || fetchPromise;
      })
    })
  );
});
  1. 在Android 7.0及以上可以通过终端Java ServiceWorkerController
    设置所有资源的网络控制方式,包括LOAD_DEFAULT、LOAD_CACHE_ELSE_NETWORK、LOAD_CACHE_ELSE_NETWORK、LOAD_NO_CACHE以及LOAD_CACHE_ONLY。

参考资料:
google offline cookbook 以上例子出处
MDN Service Worker cookbook 不仅包括网络资源的demo,还包括Push通知、offline应用等。
MDN Service Worker Guide SW基本使用

跨Scope Context通信:

如同前后端通信。当用户在网页或Web App操作DOM时,想触发SW执行网络资源拉取或计算任务,就要通过postMessage接口将消息跨Context传递。
Demo代码:https://github.com/googlechrome/samples/tree/gh-pages/service-worker/post-message

通信方式:

单向通信。

index.html -> sw.js

  1. sw.js监听message事件
self.addEventListener("message", function(event) { 
  console.log(event.data.command);
});
  1. 在MainScope中获取SW对象。
#方式一: SW install成功后取navigator.serviceWorker.controller
# 实际对应ServiceWorkerRegistration.active
var serviceWorker =  navigator.serviceWorker.controller
#方式二: register成功后在返回的ServiceWorkerRegistration中取SW对象
#注:ServiceWorkerRegistration中不同状态对应不同的SW对象
navigator.serviceWorker.register('sw.js').then(function(registration) {
  var serviceWorker = registration.installing;
  if (!serviceWorker)
    serviceWorker = registration.waiting;
  else if (!serviceWorker)
    serviceWorker = registration.active;
});
  1. 调用postMessage接口,即可在index.html中发送消息到sw.js。
serviceWorker.postMessage({command:'hello'})

API资料:
https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/controller

sw.js -> index.html

  1. 通过SW控制的页面的代理对象,例如index.html,调用postMessage发送消息。
    clients.matchAll().then(clients => {
        clients.forEach(client => {
            client.postMessage({command:'Ack'});
        })
    })

API资料:
https://developer.mozilla.org/en-US/docs/Web/API/Client

双向通信。

建立MessageChannel,同样通过postMessage传递Channel连接。
index.html

if (navigator.serviceWorker.controller) { 
  const messageChannel = new MessageChannel();  
  messageChannel.port1.onmessage = function(event) { 
    console.log(`Response from the SW : ${event.data.command}`); 
  } 
  navigator.serviceWorker.controller.postMessage({ command: "connect"}, [messageChannel.port2]); 
}

API资料:https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel

sw.js

self.addEventListener("message", function(event) { 
  const data = event.data; 
  if (data.command === "connect") { 
    event.ports[0].postMessage({ command: "accept" }); 
  }
});
广播通信

建立同一命名的BroadcastChannel,sw.js与client在BroadcastChannel添加message监听以及postMessage。

// From sw.js:
const channel = new BroadcastChannel('sw-messages');
channel.postMessage({title: 'Hello from SW'});

// From index.html:
const channel = new BroadcastChannel('sw-messages');
channel.addEventListener('message', event => {
  console.log('Received', event.data);
});

API资料:https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel

存在的问题

问题:StopWorker 会触发MessageChannel 关闭,而且 service worker 再次重启之后也无法重建原来的 Messagechannel。这就意味着,在 service worker stop之后,整个双向通信的通道就不能使用了。按照 service worker 规范的说明,浏览器可以在任意需要的时候关闭和重启 service worker,这也等同于 service worker 与其控制页面建立的 MessageChannel 随时会断掉,而且无法重建。
解决方法:

  1. 每次发送消息都新建 MessageChannel。Google 官方的 DEMO 就是使用了这种方式。它将postMessage包装成 sendMessage 方法,该方法每次调用都会创建新的 MessageChannel。缺点是每次消息通信都需要新建 MessageChannel 实例,这样它与单向通信相比,优势就不明显了。
  2. 页面监听SW生命周期,在事件回调中维护MessageChannel。每当Service​Worker​Registration​.installing安装了新的SW之后,会触发Service​Worker​Registration.onupdatefound回调。在回调中建立与新的SW的连接,并且监听新的ServiceWorker.onstatechange事件,当SW状态转为redundant时标记连接不可用。优点是Channel管理逻辑与postMessage逻辑隔离,缺点是需要额外实现Channel的管理逻辑。

API资料:https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/state
http://craig-russell.co.uk/2016/01/29/service-worker-messaging.html#.XPSPJfkzaUl
https://github.com/GoogleChrome/samples/tree/gh-pages/service-worker/post-message

Worker可能随时罢工

Service worker 规范中提到:“Service workers may be started by user agents without an attached document and may be killed by the user agent at nearly any time”,即 Service Worker 线程可能在任意时间被浏览器停止,即使关联的文档还未关闭 service worker 线程也有可能已被停止。这种设计主要是为了降低 Service Worker 对资源(比如浏览器内存、手机电量等)的消耗。所以,Service Worker 线程一般在什么情况下会被停止?

  1. Service worker JS 有任何异常,都会导致 service worker 线程退出。
    包括但不限于 JS 文件存在语法错误、service worker 安装失败或激活失败、service worker JS 执行时出现未被捕获的异常。
  2. Service worker 功能事件处理完成,处于空闲状态,service worker 线程会自动退出。
  3. Service worker JS 执行时间过长,service worker 线程会自动退出。比如 service worker JS 执行时间超过 30 秒,或 Fetch 请求在 5 分钟内还未完成。
  4. 浏览器会周期性检查各个 service worker 线程是否可以退出, 一般在启动 service worker 线程时会检查一次。
  5. 为了方便开发者调试, Chromium 进行了特殊处理, 在连上 devtools 之后,service worker 线程不会退出。Keep a serviceworker alive when devtools is attached - chromium - Monorail

参考文档:https://www.wengbi.com/thread_50556_1.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260