Service Worker 从入门到进阶

22 篇文章 0 订阅
6 篇文章 0 订阅

特别简的介

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

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

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

兼容情况

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

在这里插入图片描述

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

了解前的了解

webWorker
fetch
cache
promise

生命周期

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 脚本的生命周期有这样一些阶段(从左往右):

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;
        }
    <span class="hljs-comment">// 因为 event.request 流已经在 caches.match 中使用过一次,</span>
    <span class="hljs-comment">// 那么该流是不能再次使用的。我们只能得到它的副本,拿去使用。</span>
    <span class="hljs-keyword">var</span> fetchRequest = event.request.<span class="hljs-keyword">clone</span>();

    <span class="hljs-comment">// fetch 的通过信方式,得到 Request 对象,然后发送请求</span>
    <span class="hljs-keyword">return</span> fetch(fetchRequest).then(
      <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(response)</span> </span>{
        <span class="hljs-comment">// 检查是否成功</span>
        <span class="hljs-keyword">if</span>(!response || response.status !== <span class="hljs-number">200</span> || response.type !== <span class="hljs-string">'basic'</span>) {
          <span class="hljs-keyword">return</span> response;
        }

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

        caches.open(CACHE_NAME)
          .then(<span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(cache)</span> </span>{
            cache.put(event.request, responseToCache);
          });

        <span class="hljs-keyword">return</span> response;
      }
    );
  })
);

});

最佳实践

Register 时机

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

//Bad
window.addEventListener('DOMContentLoaded', function() {
    navigator.serviceWorker.register('/sw.js').then(function(registration) {
}).catch(<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">err</span>) </span>{
  
}); 

});

// 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 在饿了么的实践经验

      </div>
    </div>
</div>
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值