特别简的介
去年开始火遍南北的 PWA
技术落地情况有负重望,主要源于 safrai
对于这一技术支持不甚理想,不支持 mainfest
文件也不支持 service Worker
。
service worker
是一个特殊的 web Worker
,因此他与页面通信和 worker
是一样的,同样不能访问 DOM
。特殊在于他是由事件驱动的具有生命周期的 worker
,并且可以拦截处理页面的网络请求(fetch),可以访问 cache
和 IndexDB
。
换言之 service Worker
可以让开发者自己控制管理缓存的内容以及版本,为离线弱网环境下的 web 的运行提供了可能,让 web 在体验上更加贴近 native。
兼容情况
safrai
已经于 2017年8月 开始了 service Worker
的开发。
android
设备在 4.4
版本使用 Chromium
作为内核,Chromium
在 40 对于 service worker
支持。国内浏览器包括微信浏览器在内基本已经支持 service Worker
这为提升体验提供了可能。service worker
与 HTTP2
更加配哦,在将来基于它可以实现消息推送,静默更新以及地理围栏等服务。
了解前的了解
生命周期
Service Worker
在 main.js
进行注册,首次注册前会进行分析,判断加载的文件是否在域名下,协议是否为 HTTPS
的,通过这两点则成功注册。
service Worker
开始进入下一个生命周期状态 install
, install
完成后会触发 service Worker
的 install
事件。 如果 install
成功则接下来是 activate
状态, 然后这个 service worker
才能接管页面。当事件 active
事件执行完成之后,此时 service Worker
有两种状态,一种是 active
,一种是 terminated
。active
是为了工作,terminated
则为了节省内存。当新的 service Worker
处于 install/waitting
阶段,当前 service Worker
处于 terminated
,就会发生交接替换。或者可以通过调用 self.skipWaiting()
方法跳过等待。
被替换掉的原有的 service Worker
到 Redundant
阶段,在 install
或者 activating
中断的也会进入 Redundant
阶段。所以一个 Service Worker
脚本的生命周期有这样一些阶段(从左往右):
[图片上传失败...(image-af3cfa-1511157771617)]
Install
install
存在中间态 installing
这个状态在 main.js
的 registration
注册对象中可以访问到。
/* In main.js */
// 重写 service worker 作用域到 ./
navigator.serviceWorker.register('./sw.js', {scope: './'}).then(function(registration) {
if (registration.installing) {
// Service Worker is Installing
}
})
安装时 service Worker
的 install
事件被触发,这一般用于处理静态资源的缓存
/* 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
对象 rejected
则 service 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 worker
的 active
事件。
/* 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
方法。当所接收的 promise
被 reject
那么 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 Worker
在 install
active
过程中处错误或者,被新的 service Worker
替换状态会变为 Redundant
。
如果是后一种情况,则该 worker
仍然控制这个页面。
值得注意的是已经
install
的service worker
页面关闭后再打开不会触发install
事件,但是会重新注册。更多参考文章 探索 Service Worker 「生命周期」
请求处理
处于 actived
阶段的 service Worker
可以拦截页面发出的 fetch
,也可以发出fetch
请求,可以将请求和响应缓存在 cache
里,也可以将 response
从 cache
中取出。
缓存使用策略
因此可以根据使用的场景,使用缓存的 response
给到页面减少请求及时响应,亦或者将请求返回的结果更新到缓存,在应用离线时返回给页面。这就是以下的多种策略。
- 网络优先: 从网络获取, 失败或者超时再尝试从缓存读取
- 缓存优先: 从缓存获取, 缓存插叙不到再尝试从网络抓取,在上文中的代码块就是该种策略的实现。
- 最快: 同时查询缓存和网络, 返回最先拿到的
- 仅限网络: 仅从网络获取
- 仅限缓存: 仅从缓存获取
示例
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 Worker
在 install
事件中缓存文件过程中,当其中一个文件加载失败,则 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-toolbox
和 sw-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 在饿了么的实践经验