Service Worker

3次阅读

共计 3337 个字符,预计需要花费 9 分钟才能阅读完成。

当下 PWA 比较火,而 Service Worker 是实现 PWA 的一项关键技术,今天我们一起了解下关于 Service Worker 的一些基础知识和适用场景。
什么是 Server Worker
我们先来看一下官方文档中对于 Server Worker 的核心定义:
Service workers 本质上充当 Web 应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。
这是一条很准确的定义,但对于不了解 Service Worker 的同学来说可能并不形象,下面我们更形象的理解一下这个概念。我们以去银行取钱为例子,其过程大概如下图:

从上面的图中我们可以看到,银行的钱存在金库中,客户取钱时并不是直接去金库里拿,而是需要通过银行的工作人员,再告知银行工作人员需要多少钱,并出示相应凭证后,由银行工作人员从金库中拿出钱给客户,并从账户中减去相应金额。这么做的原因很容易理解,因为金库是公用的,所有客户的钱都放在里面,我们无法保证每个客户都能只拿走属于自己的钱,并按照实际情况更新金库记录。
我们的应用在请求服务器资源时,其过程也是类似的:

从上面的图可以看到,请求资源的过程中,HTTP 协议充当了取钱过程中的银行工作人员,客户端应用需要的资源在服务器上,但应用却无法直接去服务器获取资源,而是通过 HTTP 协议进行,请求中指定的各种 Header 信息,就是取钱时的凭证。
而 Service Worker 可以理解成,在客户端创建了一个属于自己的金库,先看图:

当我们需要取钱或者获取资源时,可以先从本地的金库中拿,本地金库没有,再通过原来的流程获取。这时我们再回头看文章开始的定义,应该就能够理解了。
Service Worker 与 Cache 的关系
正常情况下,客户端获取一个资源的过程有如下三步:

而关于请求资源的优化,一般也集中在这三步完成:

不发出请求就能够获得资源;
提高服务器查找资源的速度;
减小返回内容的体积;

看完上面的部分我们可以发现,当使用 Service Worker 中已有的资源时,客户端应用获取资源并没有进入 HTTP 请求的流程,也就是说,通过 Service Worker,客户端应用可以在不发出请求的情况下获得资源,这很容易就让我们想到缓存,那么 Service Worker 和我们平时经常提到的强缓存和协商缓存等是什么关系呢?整理了一个图,可以先看下:

从整体上来说,应用获取一个资源的缓存类型分为上图中的四种,分别是 Service Worker、Memory Cache、Disk Cache 和 No Cache。资源查找顺序为从左向右,找到资源则返回,未找到则继续寻找,直至最终获取资源。上面的图中可以清楚的看出 Service Worker 在缓存类型中的位置,也能看到跟平时经常提到的强缓存和协商缓存的关系。
Service Worker 使用逻辑
在了解了 Service Worker 的概念后,我们看下 Servise Worker 的基本使用逻辑,使用它的基础过程是首先注册一个 Woker,这时浏览器会在后台启动一个新的线程,在这个线程启动后,会按照 Service Worker 中的代码逻辑将一些资源缓存下来,缓存完毕后,启动对页面请求的监听,当监听到页面请求资源时可以做出相对应的响应,比如如果资源在 Service Worker 中缓存过了,就可以直接返回资源。
注册
Service Worker 对象保存在 window.navigator 内,首先调用 register 方法进行注册,导入一个 js 文件,文件中是我们的 Service Worker 逻辑,代码如下:
navigator.serviceWorker.register(‘/sw.js’)
.then(function(reg){
console.log(“success”, reg);
}).catch(function(err) {
console.log(“error”, err);
});
需要注意的是 Service Worker 是有作用域的,它的作用域为文件的当前路径,Service Worker 文件只能管理自己作用下的资源,比如 abcde.com/home/sw.js 的作用域为 abcde.com/home/。
激活
注册后的 Service Worker 就能在调试工具中看到了,下面是一个 chrome 调试面板的截图:

画红框的内容是一些比较关键的信息,比如其中表明了 Service Worker 的文件名和路径,以及当前 Service Worker 的状态,Service Worker 的状态分为几种,STOPPED(已停止)、STARTING(正在启动)、RUNNING(正在运行)和 STOPPING(正在停止),比如上面的截图就处于 RUNNING 状态。
只有处于 running 状态的 Service Worker 才能生效,这就需要对注册后的 Service Worker 进行加载和激活,注册完毕后,Service Worker 会自动开始下载,下载后会触发 install 事件,我们可以监听这个事件并进行下载资源的操作,代码如下:
const CACHE_NAME = “demo-a”;
this.addEventListener(“install”, function(event) {
console.log(“install service worker success”);
caches.open(CACHE_NAME);
let cacheResources = [“https://abcde.com/demo.js”];
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
cache.addAll(cacheResources);
})
);
});
经过上面的代码,demo.js 文件就被我们缓存下来了,下载完后 Service Worker 就会执行激活:
this.addEventListener(“active”, function(event) {
console.log(“service worker active success”);
});
此时我们通过开发者工具就能看到一个激活的 Service Worker 了,整体梳理一下大概是下面的过程:

需要注意的是,图中灰色的部分是一个独立的特殊线程,并不是浏览器渲染页面执行 js 的线程,因此使用 Service Worker 的过程中无需担心会影响页面的渲染。
更新
我们注册了 Service Worker 后,还面临着更新的问题,当我们的业务迭代时必然要更新 Service Worker,在我们理解了它的整个注册过程后,理解更新就很简单了,直接上图:

当应用加载时,会下载 Service Worker 文件,这是在浏览器中就会有两个文件,一个是当前正在使用的 Service Worker,一个是新下载的 Service Worker,当新下载的文件下载完毕后,浏览器会对两个文件进行 Diff 操作,如果发现文件没有更新,则会丢弃掉新下载的 Service Worker 文件,如果发现有变化,则会加载新的 Service Worker,但新加载的 Service Worker 会处于 wating 状态,并不会实际发挥作用,只有当整个浏览器中对正在运行的 Service Worker 没有依赖时,才会将运行中的 Service Worker 抛弃,将新的 Servier Worker 置为激活状态。
常见使用场景

用于浏览器缓存,提高加载速度;
实现离线应用,最近 PWA 如此火爆;
实现消息的主动推送,为 web 应用增加一种给力的交互方式;

兼容性
我们了解一些 Service Worker 的基础知识,以及一些比较常见的使用场景,那么目前 Service Worker 的兼容性如何呢,看下图

目前主流的现代浏览器支持度还是不错的,但是到目前为止全系列的 IE 浏览器均不支持 Service Worker,而且有一点感受很明显,在查资料的过程中,看了网上不少的博客,不同的博客上也有当时写博客时的 Service Worker 支持度,可以明显感觉到 Service Worker 的支持程度在快速提升,随着支持度的提升,相信会有越来越多的开发者在项目中使用这项技术。
借助 Service Worker,真正让 PWA 应用变得流行,也许就在不久的将来。

正文完
 0

Service Worker

3次阅读

共计 10245 个字符,预计需要花费 26 分钟才能阅读完成。

Service Worker
随着前端快速发展,应用的性能已经变得至关重要,关于这一点大佬做了很多统计。你可以去看看。
如何降低一个页面的网络请求成本从而缩短页面加载资源的时间并降低用户可感知的延时是非常重要的一部分。对于提升应用的加载速度常用的手段有 Http Cache、异步加载、304 缓存、文件压缩、CDN、CSS Sprite、开启 GZIP 等等。这些手段无非是在做一件事情,就是让资源更快速的下载到浏览器端。但是除了这些方法,其实还有更加强大的 Service Worker 线程。
Service Worker 与 PWA 的现状
说起 service worker 就不得不提起 PWA 了,service worker 做为 PWA 的核心技术之一,多年来一直被 Google 大力推广,这里简单介绍一下。
通俗来说,PWA 就是渐进式 web 应用 (Progressive Web App)。早在 16 年初,Google 便提出 PWA,希望提供更强大的 web 体验,引导开发者回归开放互联网。它弥补了 web 对比 Native App 急缺的几个能力,比如离线使用、后台加载、添加到主屏和消息推送等,同时它还具备了小程序标榜的“无需安装、用完即走”的特性。
虽然 PWA 技术已经被 W3C 列为标准,但是其落地情况一直以来是很让人失望的,始终受到苹果的阻碍,最重要的原因在于 PWA 绕过 Apple Store 审核,直接推给用户。如果普及,这将威胁到苹果的平台权威,也就意味着苹果与开发者的三七分成生意将会落空。
所以一直以来 safrai 不支持 mainfest 以及 service worker 这两项关键技术,即使在 18 年开始支持了,但是对 PWA 的支持力度也远远低于安卓,具体体现在 service worker 缓存无法永久保存,以及 service worker 的 API 支持不够完善,一个最明显的不同在于安卓版本的 PWA 会保留你的登录状态,并且会系统级推送消息。而在苹果上,这两点都做不到。也就是说,iPhone 上的微博 PWA,每次打开都要重新登录,而且不会收到任何推送信息。
另外由于某些不可描述的原因,在国内无法使用 Service Worker 的推送功能,虽然国内已经有两家公司做了 service worker 的浏览器推送,但是成熟度还有待调研。由于目前各版本手机浏览器对 service worker 的支持度都不太相同,同一个接口也存在差异化还有待统一,之于我们来说,也只能用 Service Worker 做一做 PC 浏览器的缓存了。
Service Worker 的由来
Service Worker(以下简称 sw)是基于 WEB Worker 而来的。
众所周知,javaScript 是单线程的,随着 web 业务的复杂化,开发者逐渐在 js 中做了许多耗费资源的运算过程,这使得单线程的弊端更加凹显。web worker 正是基于此被创造出来,它是脱离在主线程之外的,我们可以将复杂耗费时间的事情交给 web worker 来做。但是 web worker 作为一个独立的线程,他的功能应当不仅于此。sw 便是在 web worker 的基础上增加了离线缓存的能力。当然在 Service Worker 之前也有在 HTML5 上做离线缓存的 API 叫 AppCache, 但是 AppCache 存在很多缺点,你可以亲自看看。
sw 是由事件驱动的, 具有生命周期,可以拦截处理页面的所有网络请求 (fetch),可以访问 cache 和 indexDB,支持推送,并且可以让开发者自己控制管理缓存的内容以及版本,为离线弱网环境下的 web 的运行提供了可能,让 web 在体验上更加贴近 native。换句话说他可以把你应用里的所有静态动态资源根据不同策略缓存起来,在你下次打开时不再需要去服务器请求,这样一来就减少了网络耗时,使得 web 应用可以秒开,并且在离线环境下也变得可用。做到这一切你只需要增加一个 sw 文件,不会对原有的代码产生任何侵入,是不是很 perfect?
Service Worker 基本特征

无法操作 DOM
只能使用 HTTPS 以及 localhost
可以拦截全站请求从而控制你的应用
与主线程独立不会被阻塞(不要再应用加载时注册 sw)
完全异步,无法使用 XHR 和 localStorage
一旦被 install,就永远存在,除非被 uninstall 或者 dev 模式手动删除
独立上下文
响应推送
后台同步

。。。
service worker 是事件驱动的 worker,生命周期与页面无关。关联页面未关闭时,它也可以退出,没有关联页面时,它也可以启动。
Dedicated Worker 以及 Shared Worker 与 Service Worker 三者非常重要的区别在于不同的生命周期。对于 Service Worker 来说文档无关的生命周期,是它能提供可靠 Web 服务的一个重要基础。
Service Worker 生命周期

register 这个是由 client 端发起,注册一个 serviceWorker,这需要一个专门处理 sw 逻辑的文件
Parsed 注册完成,解析成功,尚未安装
installing 注册中,此时 sw 中会触发 install 事件,需知 sw 中都是事件触发的方式进行的逻辑调用,如果事件里有 event.waitUntil() 则会等待传入的 Promise 完成才会成功
installed(waiting) 注册完成,但是页面被旧的 Service Worker 脚本控制, 所以当前的脚本尚未激活处于等待中,可以通过 self.skipWaiting() 跳过等待。
activating 安装后要等待激活,也就是 activated 事件,只要 register 成功后就会触发 install,但不会立即触发 activated,如果事件里有 event.waitUntil() 则会等待这个 Promise 完成才会成功,这时可以调用 Clients.claim() 接管所有页面。
activated 在 activated 之后就可以开始对 client 的请求进行拦截处理,sw 发起请求用的是 fetch api,XHR 无法使用
fetch 激活以后开始对网页中发起的请求进行拦截处理

terminate 这一步是浏览器自身的判断处理,当 sw 长时间不用之后,处于闲置状态,浏览器会把该 sw 暂停,直到再次使用

update 浏览器会自动检测 sw 文件的更新,当有更新时会下载并 install,但页面中还是老的 sw 在控制,只有当用户新开窗口后新的 sw 才能激活控制页面
redundant 安装失败, 或者激活失败, 或者被新的 Service Worker 替代掉

Service Worker 脚本最常用的功能是截获请求和缓存资源文件, 这些行为可以绑定在下面这些事件上:

install 事件中, 抓取资源进行缓存
activate 事件中, 遍历缓存, 清除过期的资源
fetch 事件中, 拦截请求, 查询缓存或者网络, 返回请求的资源

Service Worker 实践
在这之前你可以先看看 Google 的 demo
我们先从 sw 的注册开始,官方给的 demo 里的注册是这样的:
if (‘serviceWorker’ in navigator) {
navigator.serviceWorker.register(‘service-worker.js’);
}
但是这样做会有一些问题,页面在首次打开的时候就进行缓存 sw 的资源,因为 sw 内预缓存资源是需要下载的,sw 线程一旦在首次打开时下载资源,将会占用主线程的带宽,以及加剧对 cpu 和内存的使用,而且 Service worker 启动之前,它必须先向浏览器 UI 线程申请分派一个线程,再回到 IO 线程继续执行 service worker 线程的启动流程,并且在随后多次在 ui 线程和 io 线程之间切换,所以在启动过程中会存在一定的性能开销,在手机端尤其严重。
况且首次打开各种资源都非常宝贵,完全没有必要争第一次打开页面就要缓存资源。正确的做法是,页面加载完以后 sw 的事。
正确的姿势:
if (‘serviceWorker’ in navigator) {
window.addEventListener(‘load’, function() {
navigator.serviceWorker.register(‘/sw.js’);
});
}
但是仅仅是这样就够了吗?只有注册,那么发生问题的时候怎么注销 sw 呢?注销以后缓存如何处理?这些是要提前考虑好的
另外使用 sw 进行注册时,还有一个很重要的特性,即,sw 的作用域不同,监听的 fetch 请求也是不一样的。假设你的 sw 文件放在根目录下位于 /sw/sw.js 路径的话,那么你的 sw 就只能监听 /sw/* 下面的请求,如果想要监听所有请求有两个办法,一个是将 sw.js 放在根目录下,或者是在注册是时候设置 scope。
一个考虑了出错降级的简易注册 demo:
window.addEventListener(‘load’, function() {
const sw = window.navigator.serviceWorker
const killSW = window.killSW || false
if (!sw) {
return
}

if (!!killSW) {
sw.getRegistration(‘/serviceWorker’).then(registration => {
// 手动注销
registration.unregister();
// 清除缓存
window.caches && caches.keys && caches.keys().then(function(keys) {
keys.forEach(function(key) {
caches.delete(key);
});
});
})
} else {
// 表示该 sw 监听的是根域名下的请求
sw.register(‘/serviceWorker.js’,{scope: ‘/’}).then(registration => {
// 注册成功后会进入回调
console.log(‘Registered events at scope: ‘, registration.scope);
}).catch(err => {
console.error(err)
})
}
});
下面部分是 sw.js 文件中要做的事情,在上面注册的步骤成功以后我们首先要在 sw.js 文件中监听注册成功以后抛出的 install 事件。
self.addEventListener(‘install’, function(e) {
// …
})
通常来说,当我们监听到这个事件的时候要做的事情就是缓存所有静态文件
self.addEventListener(‘install’, function(event) {
event.waitUntil(
caches.open(‘cache-v1’).then(function(cache) {
return cache.addAll([
‘/’,
“index.html”,
“main.css”,
]);
})
);
})
这里首先执行了一个 event.waitUntil 函数,该函数是 service worker 标准提供的函数,接收一个 promise 参数,并且监听函数内所有的 promise, 只要有一个 promise 的结果是 reject,那么这次安装就会失败。比如说 cache.addAll 时,有一个资源下载不回来,即视为整个安装失败,那么后面的操作都不会执行,只能等待 sw 下一次重新注册。另外 waitUntil 还有一个重要的特性,那就是延长事件生命周期的时间,由于浏览器会随时睡眠 sw,所以为了防止执行中断就需要使用 event.waitUntil 进行捕获,当所有加载都成功时,那么 sw 就可以下一步。
另外这里的缓存文件的列表通常来说我们应当使用 webpack 的插件或者其他工具在构建的时候自动生成。缓存的版本号也应当独立出来修改,这里我们将每一次的构建视作一个新的版本。
安装成功后就会等待进入 activate 阶段,这里要注意的是,并不是 install 一旦成功就会立即抛出 activate 事件,如果当前页面已经存在 service worker 进程,那么就需要等待页面下一次被打开时新的 sw 才会被激活,或者使用 self.skipWaiting() 跳过等待。
const cacheStorageKey = ‘testCache1’;
self.addEventListener(‘activate’, event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return cacheNames.filter(cacheName => cacheStorageKey !== cacheName);
}).then(cachesToDelete => {
return Promise.all(cachesToDelete.map(cacheToDelete => {
return caches.delete(cacheToDelete);
}));
}).then(() => {
// 立即接管所有页面
self.clients.claim()
})
);
});
在 activate 中通常我们要检查并删除旧缓存,如果事件里有 event.waitUntil() 则会等待这个 Promise 完成才会成功。这时可以调用 Clients.claim() 接管所有页面,注意这会导致新版的 sw 接管旧版本页面。
当激活完毕后就可以在 fetch 事件中对站点作用范围下的所有请求进行拦截处理了,你可以在这个阶段灵活的使用 indexDB 以及 caches 等 api 制定你的缓存规则。
// 发起请求时去根据 uri 去匹配缓存,无法命中缓存则发起请求,并且缓存请求
self.addEventListener(‘fetch’, function(event) {
event.respondWith(
caches.match(event.request).then(function(resp) {
return resp || fetch(event.request).then(function(response) {
return caches.open(‘v1’).then(function(cache) {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});
event.respondWith:接收的是一个 promise 参数,把其结果返回到受控制的 client 中,内容可以是任何自定义的响应生成代码。
另外这里有一些问题:

默认发起的 fetch 好像不会携带 cookie,需要设置 {credential: ‘include’}
对于跨域的资源,需要设置 {mode: ‘cors’},否则 response 中拿不到对应的数据
对于缓存请求时,Request & Response 中的 body 只能被读取一次,因为请求和响应流只能被读取一次,其中包含 bodyUsed 属性,当使用过后,这个属性值就会变为 true,不能再次读取,解决方法是,把 Request & Response clone 下来:request.clone() || response.clone()

当然这只是一个 demo,实际情况是不可能像这样缓存所有请求的。如果你使用工具来实现 sw 的话,比如 sw-toolbox,通常有如下几种缓存策略:

networkFirst:首先尝试通过网络来处理请求,如果成功就将响应存储在缓存中,否则返回缓存中的资源来回应请求。它适用于以下类型的 API 请求,即你总是希望返回的数据是最新的,但是如果无法获取最新数据,则返回一个可用的旧数据。
cacheFirst:如果缓存中存在与网络请求相匹配的资源,则返回相应资源,否则尝试从网络获取资源。同时,如果网络请求成功则更新缓存。此选项适用于那些不常发生变化的资源,或者有其它更新机制的资源。
fastest:从缓存和网络并行请求资源,并以首先返回的数据作为响应,通常这意味着缓存版本则优先响应。一方面,这个策略总会产生网络请求,即使资源已经被缓存了。另一方面,当网络请求完成时,现有缓存将被更新,从而使得下次读取的缓存将是最新的。
cacheOnly:从缓存中解析请求,如果没有对应缓存则请求失败。此选项适用于需要保证不会发出网络请求的情况,例如在移动设备上节省电量。
networkOnly:尝试从网络获取网址来处理请求。如果获取资源失败,则请求失败,这基本上与不使用 service worker 的效果相同。

或者根据不同的请求类型或者文件类型给予不同的策略亦或者更加复杂的策略:
self.addEventListener(‘fetch’, function (event) {
var request = event.request;

// 非 GET 请求
if (request.method !== ‘GET’) {
event.respondWith(

);
return;
}

// HTML 页面请求
if (request.headers.get(‘Accept’).indexOf(‘text/html’) !== -1) {
event.respondWith(

);
return;
}

// get 接口请求
if (request.headers.get(‘Accept’).indexOf(‘application/json’) !== -1) {
event.respondWith(

);
return;
}

// GET 请求 且 非页面请求时 且 非 get 接口请求(一般请求静态资源)
event.respondWith(

);
}
Service Worker 的更新
用户首次访问 sw 控制的网站或页面时,sw 会立刻被下载。
之后至少每 24 小时它会被下载一次。它可能被更频繁地下载,不过每 24 小时一定会被下载一次,以避免不良脚本长时间生效,这个是浏览器自己的行为。
浏览器会将每一次下载回来的 sw 与现有的 sw 进行逐字节的对比,一旦发现不同就会进行安装。但是此时已经处于激活状态的旧的 sw 还在运行,新的 sw 完成安装后会进入 waiting 状态。直到所有已打开的页面都关闭,旧的 sw 自动停止,新的 sw 才会在接下来重新打开的页面里生效。
在 SW 中的更新可以分为两种,基本静态资源的更新和 SW.js 文件自身的更新。但是不管是哪种更新,你都必须要对 sw 文件进行改动,也就是说要重新安装一个新的 sw。
首先假设一种情况,站点现有的 sw 缓存使用 v1 来进行命名,即在 install 的时候,我们使用 caches.open(‘v1’) 来进行预缓存,这时候旧的资源会全部存在 caches 里的 v1 下。
self.addEventListener(‘install’, function(e) {
e.waitUntil(
caches.open(‘v1’).then(function(cache) {
return cache.addAll([
“index.html”
])
})
)
})
现在站点更新了,我们可以简单的把 chache 里的 v1 改名为 v2,这个时候由于我们修改了 sw 文件,浏览器会自发的更新 sw.js 文件并触发 install 事件去下载最新的文件(更新缓存可以发生在任何地方),这时新的站点会存在于 v2 缓存下,待到新的 sw 被激活之后,就会启用 v2 缓存。
这是一种很简单并且安全的方式,相当于旧版本的自然淘汰,但毕竟关闭所有页面是用户的选择而不是程序员能控制的。另外我们还需注意一点:由于浏览器的内部实现原理,当页面切换或者自身刷新时,浏览器是等到新的页面完成渲染之后再销毁旧的页面。这表示新旧两个页面中间有共同存在的交叉时间,因此简单的切换页面或者刷新是不能使得 sw 进行更新的,老的 sw 依然接管页面,新的 sw 依然在等待。也就是说,即使用户知道你的站点更新了,用户自行在浏览器端做 f5 操作,这时,由于旧的 sw 还未死亡,所以用户看到的还是旧版本的页面。那么我们如何能让新的 sw 尽快接管页面呢?
那就是在 sw 内部使用 self.skipWaiting() 方法。
self.addEventListener(‘install’, function(e) {
e.waitUntil(
caches.open(cacheStorageKey).then(function(cache) {
return cache.addAll(cacheList)
}).then(function() {
// 注册成功跳过等待,酌情处理
return self.skipWaiting()
})
)
})
但是很明显,同一个页面,前半部分的请求是由旧的 sw 控制,而后半部分是由新的 sw 控制。这两者的不一致性很容易导致问题,除非你能保证同一个页面在两个版本的 sw 相继处理的情况下依然能够正常工作,才能够这样做。
也就是说,我们最好能够保证页面从头到尾都是由一个 sw 来处理的,其实也很简单:
navigator.serviceWorker.addEventListener(‘controllerchange’, () => {
window.location.reload();
})
我们可以在注册 sw 的地方监听 controllerchange 事件来得知控制当前页面的 sw 是否发生了改变,然后刷新站点,让自己从头到尾都被新的 sw 控制,就能避免 sw 新旧交替的问题了。但是 sw 的变更就发生在加载页面后的几秒内,用户刚打开站点就遇上了莫名的刷新,如果你不想被用户拍砖的话我们再来考虑考虑更好的方式。
毫无征兆的刷新页面的确不可接受,让我们来看看百度的 lavas 框架是怎么做的
当检测到有新的 sw 被安装之后弹出一个提示栏来告诉用户站点已更新,并且让用户点击更新按钮,不过 lavas 这个通知栏非常简单(丑),实际应用的话我们可以在上面丰富内容,比如增加更新日志之类的东西,另外这个按钮也不够突出,我曾多次以为我按 f5 起到的作用和他是相同的,直到我理解了它的原理才发现只能通过点击这个按钮来完成新旧 sw 的更换。

新的 sw 安装完成时会触发 onupdatefound 的方法,通过监听这个方法来弹出一个提示栏让用户去点击按钮。
navigator.serviceWorker.register(‘/service-worker.js’).then(function (reg) {
// Registration.waiting 会返回已安装的 sw 的状态,初始值为 null
// 这里是为了解决当用户没有点击按钮时却主动刷新了页面,但是 onupdatefound 事件却不会再次发生
// 具体可以参考 https://github.com/lavas-project/lavas/issues/212
if (reg.waiting) {
// 通知提示栏显示
return;
}
// 每当 Registration.Installing 属性获取新的 sw 时都会调用该方法
reg.onupdatefound = function () {
const installingWorker = reg.installing;
//
installingWorker.onstatechange = function () {
switch (installingWorker.state) {
case ‘installed’:
// 应为在 sw 第一次安装的时候也会调用 onupdatefound,所以要检查是否已经被 sw 控制
if (navigator.serviceWorker.controller) {
// 通知提示栏显示
}
break;
}
};
};
}).catch(function(e) {
console.error(‘Error during service worker registration:’, e);
});
然后就是处理通知栏点击事件之后的事情,这里只写和 sw 交互的部分,向等待中的 sw 发送消息。
try {
navigator.serviceWorker.getRegistration().then(reg => {
reg.waiting.postMessage(‘skipWaiting’);
});
} catch (e) {
window.location.reload();
}
当 sw 接收到消息以后,执行跳过等待操作。
// service-worker.js
// SW 不再在 install 阶段执行 skipWaiting 了
self.addEventListener(‘message’, event => {
if (event.data === ‘skipWaiting’) {
self.skipWaiting();
}
})
接下来就是通过 navigator.serviceWorker 监听 controllerchange 事件来执行刷新操作。好了,这样一来问题就解决了,但是这种方式只能通过去点击更新按钮而无法通过用户刷新浏览器来更新。
完整 demo
Service Worker 库
谷歌在早期有两个 pwa 的轮子:

sw-precache
sw-toolbox

都有对应的 webpack 插件,但是请注意,这两个从 2016 年开始已经不在维护了,因为有了更好的 GoogleChrome/workbox,google 官方也推荐大家使用 workbox,百度的 lavas 现在也是在使用该轮子。
另外还有同时支持 AppCache 的 NekR/offline-plugin。
本文作者:闫冬文章来源:Worktile 技术博客
文章转载请注明出处。

正文完
 0