我们知道 web 是一种极度依赖网络的应用形式,这也是 web 应用相对于原生(Native)应用的一个很大的缺点。在没有网络的情况下,再好的 webapp 也会陷入“巧妇难为无米之炊”的境地,于是 service worker 应运而生(Appcache 标准已被弃用),它也是实现 PWA 的最重要的基础。
先看看 service worker (下面简称 sw) 的有哪些能力吧
- JavaScript 控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源(最重要的功能)
- 后台数据同步
- 响应来自其它源的资源请求
- 集中接收计算成本高的数据更新,比如地理位置和陀螺仪信息,这样多个页面就可以利用同一组数据
- 在客户端进行 CoffeeScript,LESS,CJS/AMD 等模块编译和依赖管理(用于开发目的)
- 后台服务钩子
- 自定义模板用于特定 URL 模式
- 性能增强,比如预取用户可能需要的资源,比如相册中的后面数张图片
这里我们重点研究第一点 - 缓存控制能力
使用 service worker
首先,和普通 worker 一样,我们需要在 js 主线程进行注册。因为 sw 初始化后可能会预加载额外资源导致影响主线程的资源加载速度,所以一般建议在 onload 之后再进行注册。
1 | // main.js |
scope
表示定义 sw 注册范围的 URL,sw 可以控制的 URL 范围。如果传入了./h5
,那么 sw 只会对包含/h5
的路径生效。
Service worker 在一个浏览器中只有一个实例, 多个浏览上下文(例如页面,工作者等)可以与相同的服务工作者相关联,每个都通过唯一的 ServiceWorker 对象。
生命周期
sw 有 3 个声明周期
- 下载
- 安装
- 激活
下载安装
当用户访问 sw 控制的页面时,sw 就会被下载下来并进行安装,并触发 install
事件。但是下载安装后并不会立刻运行,这取决于当前是否有正在运行的 sw。如果有,则立刻安装,如果没有,则会进入 worker in waiting 状态。
1 | // service-worker.js |
如下图,当我们直接刷新页面,此时旧的 sw 还在运行,新的 sw 进入了 waiting 状态。
激活
一般来说当所有已加载的页面不再使用旧的 sw 才会激活新的 sw。只要页面不再依赖旧的 sw,新的 sw 会被激活(成为 active worker),同时会触发 activate
事件。
那么,有没有办法强制激活新的 sw 呢?
答案是肯定的。
1 | // service-worker.js |
在安装之后,调用 self.skipWaiting()
会跳过等待阶段,直接启动新的 sw。在激活后调用 self.clients.claim()
是为了保证所有正在运行的窗口能接收到 controller 更改的通知。
1 | // main.js |
在 chrome 上测试发现,即时不使用claim()
方法,某个窗口更新后,其他打开的窗口仍然能触发controllerchange
,因此这里的 claim 个人理解是当做样板代码,有些浏览器器可能需要。
The ServiceWorkerGlobalScope.skipWaiting() method of the ServiceWorkerGlobalScope forces the waiting service worker to become the active service worker.Use this method with Clients.claim() to ensure that updates to the underlying service worker take effect immediately for both the current client and all other active clients. - MDN
CacheStorage
在开始缓存资源之前,我们需要了解下 sw 的缓存机制。在 sw 中,我们对请求的缓存是存放在 CacheStorage
中的,可以通过访问 self.caches
(worker 中) 或者 window.caches
(主线程)中访问。 相关 API
在 sw 中,我们从 caches.open(name)
来获取一个独立的命名空间的 cache
对象,并在这个对象中进行操作。
离线处理
首先,我们在 sw 安装成功后,通过 cache.addAll
缓存所有需要缓存的资源。
1 | const CACHE_NAME = 'V2'; |
sw 中绝大部分方法都是异步的 promise,sw 的回调提供了watiUntil
方法,仅当传入的 promise resolve 后才会继续走后续的流程。这样一来我们可以把想要的异步操作放到 waitUntil
中执行。
随后在 activate
触发后,清理旧版本的缓存,防止超出缓存大小阈值。
1 | self.addEventListener('activate', (event) => { |
下一步,也是 sw 最具魔法的一步,我们可以通过fetch
事件拦截网络请求,还可以通过event.respondWith
来定义返回的数据。如果自定义了返回值,网络请求不会真正的发送,这样一来即使网络处于离线状态,发送的请求仍然可以返回缓存的数据。
如果我们把前端所有静态静态资源(包括页面)进行了缓存,浏览器甚至可以不发送请求就能得到运行所需的代码,这样就和 native app 一样的了!是不是很酷。
言归正传回到代码的实现,我们采用缓存优先(cache first)策略处理请求
- 当有缓存时使用缓存。
- 当没有缓存时发送请求,并将结果缓存供下一次使用。这里需要注意 response 需要 clone 一个新的对象,因为被消费过的 response 无法再次使用。
1 | self.addEventListener('fetch', (event) => { |
效果
初次运行时,我们可以看到正常的请求完成后,下载安装了 sw,并且 sw 发送了额外的请求去下载配置的资源。。
首次运行
这里看到资源貌似是重新下载了是因为本地代理没有配置 http 缓存,实际场景下我们对静态资源都会配置 http 缓存。而由于我们是在页面 onload 后才去注册 sw,所以运行时这些资源都将会从浏览器缓存中读取。
后续进入
结语
上面只是一个最简单的 sw 用法的展示。实际应用中我们可以使用一些开源的工具,例如 webpack 可以使用 workbox-webpack-plugin和 sw-precache-webpack-plugin来实现。