Service Worker 从入门到进阶

image

特别简的介

去年开始火遍南北的 PWA 技术落地情况有负重望,主要源于 safrai 对于这一技术支持不甚理想,不支持 mainfest 文件也不支持 service Worker

service worker 是一个特殊的 web Worker,因此他与页面通信和 worker 是一样的,同样不能访问 DOM。特殊在于他是由事件驱动的具有生命周期的 worker,并且可以拦截处理页面的网络请求(fetch),可以访问 cacheIndexDB

换言之 service Worker 可以让开发者自己控制管理缓存的内容以及版本,为离线弱网环境下的 web 的运行提供了可能,让 web 在体验上更加贴近 native。

兼容情况

safrai 已经于 2017年8月 开始了 service Worker 的开发。

image

目前浏览器PC支持情况如图
国内主要浏览器支持情况

android 设备在 4.4 版本使用 Chromium 作为内核,Chromium 在 40 对于 service worker 支持。国内浏览器包括微信浏览器在内基本已经支持 service Worker 这为提升体验提供了可能。service workerHTTP2 更加配哦,在将来基于它可以实现消息推送,静默更新以及地理围栏等服务。

了解前的了解

webWorker
fetch
cache
promise

生命周期

image

Service Workermain.js 进行注册,首次注册前会进行分析,判断加载的文件是否在域名下,协议是否为 HTTPS 的,通过这两点则成功注册。
service Worker 开始进入下一个生命周期状态 installinstall 完成后会触发 service Workerinstall 事件。 如果 install 成功则接下来是 activate状态, 然后这个 service worker 才能接管页面。当事件 active 事件执行完成之后,此时 service Worker 有两种状态,一种是 active,一种是 terminatedactive 是为了工作,terminated则为了节省内存。当新的 service Worker 处于 install/waitting 阶段,当前 service Worker 处于 terminated,就会发生交接替换。或者可以通过调用 self.skipWaiting() 方法跳过等待。
被替换掉的原有的 service WorkerRedundant 阶段,在 install 或者 activating 中断的也会进入 Redundant 阶段。所以一个 Service Worker 脚本的生命周期有这样一些阶段(从左往右):

[图片上传失败...(image-af3cfa-1511157771617)]

Install

install 存在中间态 installing 这个状态在 main.jsregistration注册对象中可以访问到。

/* In main.js */
// 重写 service worker 作用域到 ./
navigator.serviceWorker.register('./sw.js', {scope: './'}).then(function(registration) {  
    if (registration.installing) {
        // Service Worker is Installing
    }
})

安装时 service Workerinstall 事件被触发,这一般用于处理静态资源的缓存

service worker 缓存的静态资源
chrome PWA 演示实例
/* In sw.js */
self.addEventListener('install', function(event) {  
  event.waitUntil(
  // currentCacheName 对应调试工具中高亮位置,缓存的名称
  // 调用 `cache.open` 方法后才可以缓存文件
    caches.open(currentCacheName).then(function(cache) {
    // arrayOfFilesToCache 为存放缓存文件的数组
      return cache.addAll(arrayOfFilesToCache);
    })
  );
});

event.waitUntil() 方法接收一个 promise 对象, 如果这个 promise 对象 rejectedservice Worker 安装失败,状态变更为 Redundant。关于 cache 相关说明看下文。

Installed / Waiting

安装完成待正在运行的 service Worker 交接的状态。
Service Worker registration 对象, 我们可以获得这个状态

/* In main.js */
navigator.serviceWorker.register('./sw.js').then(function(registration) {  
    if (registration.waiting) {
        // Service Worker is Waiting
    }
})

这是一个提示用户更新的好时机,或者可以静默更新。

Activating

  • 当页面没有正在运行的 service Worker时;
  • service Worker脚本中调用了 self.skipWaiting 方法;
  • 用户切换页面使原有的 service Worker 释放;
  • 特定失效已过,释放因此原有的 service Worker 被释放

则状态变为 activating,触发 service workeractive 事件。

/* In sw.js */
self.addEventListener('activate', function(event) {  
  event.waitUntil(
    // Get all the cache names
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        // Get all the items that are stored under a different cache name than the current one
        cacheNames.filter(function(cacheName) {
          return cacheName != currentCacheName;
        }).map(function(cacheName) {
          // Delete the items
          return caches.delete(cacheName);
        })
      ); // end Promise.all()
    }) // end caches.keys()
  ); // end event.waitUntil()
});

install 事件中的 event.waitUntil 方法。当所接收的 promisereject 那么 serviceWorker 进入 Redundant状态。

Actived

activting成功后,这时 service Worker 接管了整个页面状态变为 acticed
这个状态我们可以拦截请求和消息。

/* In sw.js */

self.addEventListener('fetch', function(event) {  
  // Do stuff with fetch events
});

self.addEventListener('message', function(event) {  
  // Do stuff with postMessages received from document
});

Redundant

service Workerinstall active过程中处错误或者,被新的 service Worker 替换状态会变为 Redundant

如果是后一种情况,则该 worker 仍然控制这个页面。

值得注意的是已经 installservice worker 页面关闭后再打开不会触发 install 事件,但是会重新注册。更多参考文章 探索 Service Worker 「生命周期」

请求处理

处于 actived 阶段的 service Worker 可以拦截页面发出的 fetch,也可以发出fetch请求,可以将请求和响应缓存在 cache里,也可以将 responsecache 中取出。

缓存使用策略

因此可以根据使用的场景,使用缓存的 response 给到页面减少请求及时响应,亦或者将请求返回的结果更新到缓存,在应用离线时返回给页面。这就是以下的多种策略。

  1. 网络优先: 从网络获取, 失败或者超时再尝试从缓存读取
  2. 缓存优先: 从缓存获取, 缓存插叙不到再尝试从网络抓取,在上文中的代码块就是该种策略的实现。
  3. 最快: 同时查询缓存和网络, 返回最先拿到的
  4. 仅限网络: 仅从网络获取
  5. 仅限缓存: 仅从缓存获取

示例

fetch 基于stream 的 ,因此 response & request 一旦被消费则无法还原,所以这里在缓存的时候需要使用 clone 方法在消费前复制一份。

self.addEventListener('fetch', function(event) {
// 只对 get 类型的请求进行拦截处理
  if (event.request.method !== 'GET') {
    console.log('WORKER: fetch event ignored.', event.request.method, event.request.url);
    return;
  }
  event.respondWith(
  // 缓存中匹配请求
    caches.match(event.request)
      .then(function(response) {
        if (response) {
          return response;
        }

        // 因为 event.request 流已经在 caches.match 中使用过一次,
        // 那么该流是不能再次使用的。我们只能得到它的副本,拿去使用。
        var fetchRequest = event.request.clone();

        // fetch 的通过信方式,得到 Request 对象,然后发送请求
        return fetch(fetchRequest).then(
          function(response) {
            // 检查是否成功
            if(!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }

            // 如果成功,该 response 一是要拿给浏览器渲染,而是要进行缓存。
            // 不过需要记住,由于 caches.put 使用的是文件的响应流,一旦使用,
            // 那么返回的 response 就无法访问造成失败,所以,这里需要复制一份。
            var responseToCache = response.clone();

            caches.open(CACHE_NAME)
              .then(function(cache) {
                cache.put(event.request, responseToCache);
              });

            return response;
          }
        );
      })
    );
});

最佳实践

Register 时机

service Worker 将加剧对 CPU 时间和内存的争用,从而影响浏览器渲染以及网页的交互。Chrome 团队的开发者 Jeff Posnick 实践表明在显示动画期间注册 service Worker 会导致低端移动设备出现卡顿,因此在这种场景下延后注册或等更好的用户体验。

//Bad
window.addEventListener('DOMContentLoaded', function() {
    navigator.serviceWorker.register('/sw.js').then(function(registration) {
      
    }).catch(function(err) {
      
    }); 
  });


// Good
if ('serviceWorker' in navigator) {
// 判断浏览器支持情况
  window.addEventListener('load', function() {
  // 页面所有资源加载完成后注册
    navigator.serviceWorker.register('/service-worker.js');
  });
}

但是当你使用 clients.claim()service Worker 控制所有

install 事件中静态资源缓存

service Workerinstall 事件中缓存文件过程中,当其中一个文件加载失败,则 install 失败。因此可以对要缓存的文件进行分级,一定要加载的,和允许加载失败的,对于允许加载失败的文件。

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('mygame-core-v1').then(function(cache) {
    // 不稳定文件或大文件加载
      cache.addAll(
        //...
      );
      // 稳定文件或小文件加载
      return cache.addAll(
        // core assets & levels 1-10
      );
    })
  );
});

处理请求中离线情况

service Worker 发送请求时,捕获异常,并返回页面一个 response 通知页面可能离线。

function unableToResolve () {
  /* 
    当代码执行到这里,说明请求无论是从缓存还是走网络,都无法得到答复,这个时机,我们可以返回一个相对友好的页面,告诉用户,你可能离线了。
  */
  console.log('WORKER: fetch request failed in both cache and network.');
  return new Response('<h1>Service Unavailable</h1>', {
    status: 503,
    statusText: 'Service Unavailable',
    headers: new Headers({
      'Content-Type': 'text/html'
    })
  });
}
fetch(event.request).then(fetchedFromNetwork, unableToResolve).catch(unableToResolve);

引入开关机制

开关是在饿了么实践经验里提出降级方案,通过向后端请求一个是否降级的接口,如果降级则注销掉已经注册的service Worker。这里要注意不要缓存这个开关请求。为了便于问题排查,可以设置一个 debug 模式(在 url 添加某些字符)。

错误监控

self.addEventListener('error', event => {
  // 上报错误信息
  // 常用的属性:
  // event.message
  // event.filename
  // event.lineno
  // event.colno
  // event.error.stack
})
// 捕获 promise 错误
self.addEventListener('unhandledrejection', event => {
  // 上报错误信息
  // 常用的属性:
  // event.reason
})

这两个事件都只能在 worker 线程的 initial 生命周期里注册。(否则会失败,控制台可看到警告)

Google 开发工具助力 service worker 开发

Google 提供了 sw-toolboxsw-precache 两个工具方便快速生成 service-worker.js 文件:

  • sw-precache用于生成页面所需静态资源列表,目前有 webpack 插件 sw-precache-webpack-plugin 可以配合
  • sw-toolbox 提供了动态缓存使用的通用策略, 这些动态的资源不合适用 sw-precache 预先缓存。同时它提供了一套类似 Express.js 路由的语法, 用于编写策略。它还提供了 LRU 替换策略与 TTL 失效机制,可以保证我们的应用不会超过浏览器的缓存配额。

更多[参考文章]([PWA 入门: 理解和创建 Service Worker 脚本])

注意事项

  • 作用域:出于安全原因, Service Worker 脚本的作用范围不能超出脚本文件所在的路径。比如地址是 "/sw-test/sw.js" 的脚本只能控制 "/sw-test/" 下的页面。
  • 本地开发环境可以使用 http 协议, 上线必须使用https 协议。
  • Service Worker 中的 Javascript 代码必须是非阻塞的,所以你不应该在 Service Worker 代码中是用 localStorage 以及 XMLHttpRequest
  • 在页面关闭后,浏览器可以继续保持service worker运行,也可以关闭service worker,这取决与浏览器自己的行为,所以不要在 serviceWorker.js 中定义全局变量,如果想要保存一些持久化的信息,你可以在service worker里使用IndexedDB API。

参考

MDN
Service Worker lifecycle
service worker note
update service worker
chrom service worker sample
PWA 入门: 理解和创建 Service Worker 脚本
PWA 在饿了么的实践经验

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

推荐阅读更多精彩内容