pfan123/Articles

Service Worker理解使用

pfan123 opened this issue · 0 comments

什么是 Service Worker

W3C 组织早在 2014 年 5 月就提出过 Service Worker 这样的一个 HTML5 API ,主要用来做持久的离线缓存。

浏览器中的 javaScript 都是运行在一个单一主线程上的,在同一时间内只能做一件事情。随着 Web 业务不断复杂,我们逐渐在 js 中加了很多耗资源、耗时间的复杂运算过程,这些过程导致的性能问题在 WebApp 的复杂化过程中更加凸显出来。

Service Worker 是在新开 Web Worker进程脱离在主线程之外, 将缓存拦截处理完成后通过 postMessage 方法告诉主线程,而主线程通过 onMessage 方法得到 Web Worker 的结果反馈。Service Worker 在 Web Worker 的基础上加上了持久离线缓存能力。

Service Worker 之前也有在 HTML5 上做离线缓存的 API 叫 AppCache, 但存在很多缺点。W3C 决定 AppCache 仍然保留在 HTML 5.0 Recommendation 中,在 HTML 后续版本中移除。
Issue: https://github.com/w3c/html/issues/40open_in_new
Mailing list: https://lists.w3.org/Archives/Public/public-html/2016May/0005.htmlopen_in_new

Service Worker 功能和特性

Service Worker 的伟大使命,就是让缓存做到优雅和极致,让 Web App 相对于 Native App 的缺点更加弱化,也为开发者提供了对性能和体验的无限遐想,其含有很多特性和功能点

  • 一个独立的 worker 线程,独立于当前网页进程,有自己独立的 worker context。
  • 一旦被 install,就永远存在,除非被 uninstall
  • 需要的时候可以直接唤醒,不需要的时候自动睡眠(有效利用资源,此处有坑)
  • 可编程拦截代理请求和返回,缓存文件,缓存的文件可以被网页进程取到(包括网络离线状态)
  • 离线内容开发者可控
  • 能向客户端推送消息
  • 不能直接操作 DOM
  • 出于安全的考虑,必须在 HTTPS 环境下才能工作(允许在开发调试的 localhost 使用)
  • 异步实现,内部大都是通过 Promise 实现

Service Worker 浏览器支持情况

Service Worker 浏览器支持情况怎么样呢?参考 Can I use 可得下图:
Service Worker 浏览器支持情况

前支持的浏览器不多,而且支持的浏览器也是在试验阶段,Chrome、Firefox、Opera 支持性较好,同时 x5 andriod 支持 Service Worker

Service Worker 使用限制

if('serviceWorker' in navigator) {
  navigator.serviceWorker.register(scriptURL, options)
  .then(registration => {
    console.error('registration', registration)
  })
  .catch(err => {
    console.error('catch', err)
  })
}

Service Worker 除了 work 线程的限制外,由于可拦截页面请求,为了保证页面安全,浏览器端对 Service Worker 的使用限制也不少。

1)service worker 脚本的 URL, 不支持跨域,不允许缓存 service worker 脚本如 service-worker.js。

2)service worker 脚本的 URL, 不支持 Blob/String URLCreate service worker from Blob/String URL

3)无法直接操作 DOM 对象,也无法访问 window、document、parent 对象。可以访问 location、navigator;

4)可代理的页面作用域限制 (scope) 。默认是 service-worker.js 所在文件目录及子目录的请求可代理,可在注册时手动设置作用域范围;

5)必须在 https 中使用,允许在开发调试的 localhost 使用。

Service Worker 生命周期

Service Worker 的工作原理是基于注册、安装、激活等步骤在浏览器 js 主线程中独立分担缓存任务的,那么我们如何在这些 API 自身一系列的操作中进行一些我们自己想让 worker 干的事情呢?

这里我们需要了解一下 Service Worker 的生命周期的概念,这有利于我们学会在各个生命周期的阶段进行有目的性的回调,让我们自定义的工作在 Service Worker 中正确有效的开展下去。MDN 给出了详细的 Service Worker 生命周期图:

Service Worker 生命周期

我们可以看到生命周期分为这么几个状态 安装中, 安装后, 激活中, 激活后, 废弃

  • 安装( installing ):这个状态发生在 Service Worker 注册之后,表示开始安装,触发 install 事件回调指定一些静态资源进行离线缓存。
    install 事件回调中有两个方法:

  • event.waitUntil():传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止。

  • self.skipWaiting():self 是当前 context 的 global 变量,执行该方法表示强制当前处在 waiting 状态的 Service Worker 进入 activate 状态。

  • 安装后( installed ):Service Worker 已经完成了安装,并且等待其他的 Service Worker 线程被关闭。

  • 激活( activating ):在这个状态下没有被其他的 Service Worker 控制的客户端,允许当前的 worker 完成安装,并且清除了其他的 worker 以及关联缓存的旧缓存资源,等待新的 Service Worker 线程被激活。

Service Worker 文件

对于浏览器来说,Service Worker 是一个独立于 js 主线程的一种 Web Worker 线程,一个独立于主线程的 Context,但是面向开发者来说 Service Worker 的形态其实就是一个需要开发者自己维护的文件,我们假设这个文件叫做 service-worker.js,此文件的内容就是定制 Service Worker 生命周期中每个阶段所处理的定制化的细节逻辑,比如缓存 Cache 的读写,更新的策略,推送的策略等等,通常 service-worker.js 文件是处于项目的根目录,并且需要保证能直接通过 https: //yourhost/service-worker.js 这种形式直接被访问到才行。

service-worker.js基本结构代码,如下:

// 安装
self.addEventListener('install', function (e) {
    // 缓存 App Shell 等关键静态资源和 html (保证能缓存的内容能在离线状态跑起来)
});

// 激活
self.addEventListener('activate', function (e) {
    // 激活的状态,这里就做一做老的缓存的清理工作
});

// 缓存请求和返回(这是个简单的缓存优先的例子)
self.addEventListener('fetch', function (e) {
    e.respondWith(caches.match(e.request)
        .then(function (response) {
            if (response) {
                return response;
            }
            // fetchAndCache 方法并不存在,需要自己定义,这里只是示意代码
            return fetchAndCache(e.request);
        })
    );
});

快速注册 Service Worker

注册 Service Worker 还是蛮简单的,只要小段代码。只要在工程中的 html 文档的 <script> 标签里或者随便在页面的哪个 javaScript 模块中添加如下代码即可

navigator.serviceWorker && navigator.serviceWorker.register('/service-worker.js', {scope: '/'})
	.then( registration => {
		// 注册成功
		console.log('ServiceWorker registration successful with scope: ', registration.scope);
	})
	.catch( err => {
		// 注册失败:(
		console.log('ServiceWorker registration failed: ', err);
	})

sw-register-webpack-plugin 与 sw-precache-webpack-plugin

无论是 Service Worker 作用域问题,还是 Service Worker 的更新问题,都与 Service Worker 的注册息息相关,一个看似简单的 Service Worker 的注册还是有很多地方需要注意,但是如果这些都需要在每个项目中都要自己完全实现一遍,还是非常繁琐的。而sw-precache-webpack-pluginsw-register-webpack-plugin 作为一个 Webpack Plugin 很好的帮助我们解决了优雅的注册 Service Worker 的问题

参考资料:

workbox

Service Worker 简介

如何优雅的为 PWA 注册 Service Worker

Service Worker最佳实践

Progressive Web App 的离线存储

Service Workers 与离线缓存

PWA与service worker工作原理探析

service worker 实现离线缓存

使用service worker对静态资源进行全缓存

PWA 应用实战