image
特别简的介
去年开始火遍南北的 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 的开发。
image
目前浏览器PC支持情况如图
国内主要浏览器支持情况
android 设备在 4.4 版本使用 Chromium 作为内核,Chromium 在 40 对于 service worker 支持。国内浏览器包括微信浏览器在内基本已经支持 service Worker 这为提升体验提供了可能。service worker 与 HTTP2 更加配哦,在将来基于它可以实现消息推送,静默更新以及地理围栏等服务。
了解前的了解
生命周期
image
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 事件被触发,这一般用于处理静态资源的缓存
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 对象 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 在饿了么的实践经验












网友评论