Service Worker
简介:
1、Service Worker 是 PWA 技术基础之一,脱离浏览器主线程的特性,使得 Web App 离线缓存成为可能,
更为后台同步、通知推送等功能提供了思路
2、通常所讲的 Service Worker 指的是 Service Worker 线程
3、浏览器中执行的 JavaScript 文件是运行在一个单一线程上,称为主线程,而 Service Worker 是一种独立于
浏览器主线程的工作线程,与当前的浏览器主线程是完全隔离的,并有自己独立的执行上下文
4、通过 navigator.serviceWorker.register(文件路径)方法就能够注册一个 Service Worker,
在当前的浏览器主线程的基础上新起一个 Service Worker 线程
5、Service Worker 不仅是一个独立于主线程的的一个工作线程,并且还是一个可以在离线环境下运行的工作线程
背景:
1、浏览器中的 JavaScript 都是运行在一个单一主线程上的,在同一时间内只能做一件事情。
随着 Web 业务不断复杂,在 JavaScript 中的代码逻辑中往往会出现很多耗资源、耗时间的复杂运算过程。
这些过程导致的性能问题在 Web App 日益增长的复杂化过程中更加凸显出来
2、为了解决 Web 网络连接不稳定的问题,W3C 在很早的时候提出了 ApplicationCache 机制来解决离线缓存的问题,
做法是在 HTML 页面中可以指定一个清单文件 manifest.appcache,清单中指定需要离线缓存的静态资源,
ApplicationCache 能够解决离线可访问的问题,但该方法也带来了不小的问题
3、ApplicationCache 机制带来的问题:
(1)在 manifest.appcache 文件中定义的资源全部被成功加载后,这些资源文件连同引用 manifest.appcahe 文件的
HTML 文档一并被移动到永久离线缓存中。所以如果想只缓存 JS、CSS、图片等文件,而不希望缓存 HTML 文档
以保持获得最新内容的情况来说,是个非常大的问题
(2)根据 ApplicationCache 的加载机制,如果仅仅修改被缓存资源文件的内容(没有修改资源文件的路径或名称),
浏览器将直接从本地离线缓存中获取资源文件。所以每次修改资源文件时,也要修改 manifest.appcache 文件,
以触发资源文件的重新加载和缓存,维护成本太高
(3)靠一个 manifest.appcache 配置文件来维护一个复杂的站点的缓存策略实在是一件非常艰难的工作,
毕竟单纯靠配置是非常不灵活的
(4)对动态请求无法处理
4、基于 Woker 工作线程的离线能力和离线缓存机制的双重迫切需求,通过不断的实践和发展,W3C 最终提出的
Service Worker API 可以以独立工作线程的方式运行,结合持久缓存调度策略,能够很好的解决离线缓存问题
特点:
1、出于安全的考虑 Service Worker 必须运行在 HTTPS 协议下
2、有自己完全独立的执行上下文。一旦被安装成功就永远存在,除非线程被程序主动解除,
且访问时自动激活,关闭时自动睡眠,以减少资源损耗
3、完全异步实现的,内部的接口的异步化都是通过 Promise 实现,并且在 Service Worker 中不能直接操作 DOM,
出于安全和体验的考虑,UI 的渲染工作必须只能在主线程完成
4、可拦截并代理请求、处理请求的返回内容、持久化缓存静态资源达到离线访问的效果,和 ApplicationCache 不同,
Service Worker 的所有的离线内容开发者完全可控,甚至是可以控制动态请求,第三方静态资源等
5、可离线并在后台工作
注:可通过 'serviceWorker' in navigator 判断浏览器是否支持 Service Worker
作用域:
1、Service Worker 作用域是一个 URL path 地址,指的是它能够控制的页面的范围
2、“控制页面”是指 Service Worker 可以处理这些页面里面的资源请求和网络请求,
然后通过它自身的调度机制构建离线缓存策略
3、如果页面不在 Service Worker 的作用域范围内,Service Worker 就无法处理页面的任何资源或请求
如:sw.js 在 /index/a/b 下,它只能控制 /index/a/b/*
注:类似于 Ajax 的跨域请求可以通过对请求的 Access-Control-Allow-Origin 设置,我们也可以通过服务器
对 sw.js 这个文件的请求头进行设置,就能够突破作用域的限制,只需要在服务端对 sw.js 请求设置
Service-Worker-Allowed 请求头为更大控制范围或者其他控制范围的 scope 即可。如:Service-Worker-Allowed: /a/
4、作用域污染:多个 Service Worker 控制同一个页面
注:通过进行手动 “unregister” 来清除掉污染的 Service Worker或借助 navigator.serviceWorker.getRegistrations()
方法将污染的 Service Worker 先注销掉,然后在注册自己的所在作用域的 Service Worker
生命周期:
1、在主线程成功注册 Service Worker 之后,开始下载并解析执行 Service Worker 文件,
执行过程中开始安装 Service Worker,在此过程中会触发 worker 线程的 install 事件
2、如果 install 事件回调成功执行(在 install 回调中通常会做一些缓存读写的工作,可能会存在失败的情况),
则开始激活 Service Worker,在此过程中会触发 worker 线程的 activate 事件,如果 install 事件回调执行失败,
则生命周期进入 Error 终结状态,终止生命周期
3、完成激活之后,Service Worker 就能够控制作用域下的页面的资源请求,可以监听 fetch 事件
4、如果在激活后 Service Worker 被 unregister 或者有新的 Service Worker 版本更新,
则当前 Service Worker 生命周期完结,进入 Terminated 终结状态
5、工作流程:
(1)Service Worker 文件只在首次注册的时候执行了一次
(2)安装、激活流程也只是在首次执行 Service Worker 文件的时候进行了一次
(3)首次注册成功的 Service Worker 没能拦截当前页面的请求
(4)非首次注册的 Service Worker 可以控制当前的页面并能拦截请求
注:首次注册没能拦截请求是因为,Service Worker 的注册是一个异步的过程,在激活完成后当前页面的请求
都已经发送完成,因为时机太晚,此时是拦截不到任何请求的,只能等待下次访问再进行
6、waitUntil 机制:
(1)由于 Service Worker 生命周期异步触发的特性,当 install 回调中的逻辑报错了,并不会影响 Service Worker
的生命周期继续向后推进,即后面如果没错的话 active 事件仍会被触发
(2)Service Worker 事件回调的参数是一个 ExtendableEvent 对象,在 Service Worker 中需要使用
ExtendableEvent.waitUntil() 方法来保证生命周期的执行顺序,该方法接收一个 Promise 参数
// sw.js console.log('service worker 注册成功') self.addEventListener('install', event => { // 引入 event.waitUntil 方法 event.waitUntil(new Promise((resolve, reject) => { // 模拟 promise 返回错误结果的情况 reject('安装出错') // resolve('安装成功') })) }) self.addEventListener('activate', () => { // 激活回调的逻辑处理 console.log('service worker 激活成功') }) self.addEventListener('fetch', event => { console.log('service worker 抓取请求成功: ' + event.request.url) })
(3)在 install 事件回调被调用时,waitUntil 把即将被激活的 worker 线程状态延迟为 installing 状态,
直到传递的 Promise 被成功地 resolve,确保:Service Worker 工作线程在所有依赖的
核心 cache 被缓存之前都不会被安装
(4)当 ExtendableEvent.waitUntil()运行时,如果 Promise 是 resolved,任何事情都不会发生;
如果 Promise 是 rejected,installing 或者 activating 的状态会被设置为 redundant
注:如果在 ExtendableEvent 处理程序之外调用 waitUntil(),浏览器会抛出一个InvalidStateError 错误。
如果多个调用将会堆叠,所产生的所有 promise 将被添加到延长生命周期的 promise 等待执行完成
7、终端:
(1)在手机端或者 PC 端浏览器,每新打开一个已经激活了 Service Worker 的页面,那 Service Worker 所控制的终端就
新增一个,每关闭一个包含已经激活了 Service Worker 页面的时候(不包含手机端浏览器进入后台运行的情况),
则 Service Worker 所控制的终端就减少一个
(2)当所有的终端共用一个 worker 工作线程时,在 worker 线程中执行 console.log()等操作会作用到所有的终端
(3)self.clients.claim()使激活 Service Worker 之后马上控制所有终端
更新原理:
1、当浏览器监测到新的 Service Worker 更新之后,会重新进行注册、安装
2、当检测到当前的页面被激活态的 Service Worker 控制着的话,会进入 waiting 状态
3、waiting 后有两种选择:
(1)通过 skipWaiting 跳过 waiting 状态
(2)在所有终端保持 waiting 状态,直到 Service Worker 对所有终端失去控制(关闭所有终端的时候)
4、Service Worker 在全局提供了一个 skipWaiting() 方法,skipWaiting() 在 waiting 期间调用
还是在之前调用并没有什么不同
5、手动更新:
(1)当刷新页面重新执行 register 方法时,浏览器检测到 Service Worker 文件更新则触发该文件更新
(2)如果站点在浏览器后台长时间没有被刷新,则浏览器将自动检查更新,通常是每隔 24 小时检查一次
(3)手动触发更新代码
// 1 小时重新加载一次 navigator.serviceWorker.register('/sw.js') .then(reg => { setInterval(() => { reg.update() }, 60 * 60 * 1000) })
6、通过 update on reload 功能,开发者可以做到以下几点:
(1)重新提取 Service Worker
(2)即使字节完全相同,也将其作为新版本安装,这表示运行 install 事件并更新缓存
(3)跳过 waiting 阶段,直接激活新 Service Worker
(4)浏览页面,每次浏览时(包括刷新)都将进行更新,无需重新加载两次或关闭标签
调试:
1、关注点:
(1)Service Worker 文件 JavaScript 代码是否有报错
(2)Service Worker 能否顺利安装、激活或者更新
(3)在不同机型上的兼容性是不是有问题
(4)不同类型资源和请求的缓存策略的验证
2、debug 环境下的开发跳过等待状态:使用 skipWaiting
3、借助 Chrome Devtool 进行调试:
(1)Offline:复选框可将 DevTools 切换至离线模式,等同于 Network 窗格中的离线模式
(2)Update on reload:复选框可强制 Service Worker 线程在每次页面加载时更新
(3)Bypass for network:复选框可绕过 Service Worker 线程并强制浏览器转至网络寻找请求的资源
(4)Update:按钮可对指定的 Service Worker 线程执行一次性更新
(5)Push:按钮可在没有负载的情况下模拟推送通知
(6)Sync:按钮可模拟后台同步事件
(7)Unregister:按钮可注销指定的 Service Worker 线程
(8)Source:告诉当前正在运行的 Service Worker 线程的安装时间,链接是 Service Worker 线程源文件的名称。
点击链接会将定向并跳转至 Service Worker 线程来源
(9)Status:告诉 Service Worker 线程的状态
(10)Clients:告诉 Service Worker 线程作用域的原点
4、查看缓存:Cache Storage 选项卡提供了一个已使用(Service Worker 线程)Cache API 缓存的只读资源列表
5、网络跟踪:
(1)经过 Service Worker 的 fetch 请求 Chrome 都会在 Chrome DevTools Network 标签页里标注出来
(2)来自 Service Worker 的内容会在 Size 字段中标注为 from ServiceWorker
(3)Service Worker 发出的请求会在 Name 字段中添加 ⚙ 图标
6、真机调试:
(1)Android
①准备:
·PC 上已安装 Chrome 32 或更高版本
·PC 上已安装 USB 驱动程序(如果使用 Windows),确保设备管理器报告正确的 USB 驱动程序
·一根可以将 Android 设备连接至开发计算机的 USB 线
·一台 Android 4.0 或更高版本的 Android 设备
②步骤:
·将 Android 设备通过 USB 线与 PC 连接
·在 Android 设备上进行一些设置,选择 “设置 > 开发者选项 > 开启 USB 调试”
·在 PC 上打开 Chrome,使用一个 Google 帐户登录到 Chrome
(远程调试在隐身模式或访客模式下无法运行)
·在 PC 的 Chrome 浏览器地址栏输入 chrome://inspect
·在 Remote Target 下找到对应的 Android 设备
·点击远程设备链接进入 Chrome Devtools
(2)iOS
①准备:
·一台 Mac 电脑
·一个 icloud 账号
·一个 Apple 的移动设备(iPhone)
·用 iCloud 账号登陆 Mac 和 iPhone
·对 iPhone 进行设置:设置 > Apple ID 用户中心入口 > iCloud > 打开 Safari
·对 iPhone 进行设置:设置 > Safari浏览器 > 高级 > 打开 Web Inspector
·对 Mac 进行设置:系统偏好设置 > iCloud > 勾上 Safari
·对 Mac 进行设置:打开 Safari > Safari 菜单 > 偏好设置 > 高级 > 勾选“在菜单栏中显示开发菜单”
(这时候 Safari 的系统菜单栏多了一个 开发 标签)
②步骤:
·用 USB 线连接 iPhone 和 Mac
·在 iPhone 上打开 PWA 站点
·打开 Mac 上 Safari 菜单栏的 开发 标签,就可以点击进 我的 iPhone
·接下来会发现 我的 iPhone 子菜单里有在 iphone 上打开的 PWA 站点,
这时候就可以用 Safari 的 Devtools 进行调试