Service Worker - 图1

简介

Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。
你可以把Service Worker简单理解为一个独立于前端页面,在后台运行的进程。
因此,它不会阻塞浏览器脚本的运行,同时也无法直接访问浏览器相关的API(例如:DOM、localStorage等)。
此外,即使在离开你的Web App,甚至是关闭浏览器后,它仍然可以运行。

主要能力

  • 能够创建有效的离线体验,拦截网络请求并根据网络状态判断是否使用缓存数据或者更新缓存数据。
  • 允许访问推送通知和后台同步API。

具体文档可以查阅这个链接:[ 传送门 ]

使用前提

  • 浏览器支持 ServiceWorker
  • 使用 HTTPS 传输协议(也可以运行在 localhost/127.0.0.1 域下)

    实现思路

    首先,我们想一下,当访问一个web网站时,我们实际上做了什么呢?总体上来说,我们通过与与服务器建立连接,获取资源,然后获取到的部分资源还会去请求新的资源(例如html中使用的css、js等)。所以,粗粒度来说,我们访问一个网站,就是在获取/访问这些资源。
    当处于离线或弱网环境时,我们无法有效访问这些资源,这就是制约我们的关键因素。因此,一个最直观的思路就是:如果我们把这些资源缓存起来,在某些情况下,将网络请求变为本地访问,这样是否能解决这一问题?是的。但这就需要我们有一个本地的cache,可以灵活地将各类资源进行本地存取。
    Service Worker - 图2

有了本地的cache还不够,我们还需要能够有效地使用缓存、更新缓存与清除缓存,进一步应用各种个性化的缓存策略。而这就需要我们有个能够控制缓存的“worker”——这也就是Service Worker的部分工作之一。

Service Worker - 图3

生命周期

Service Worker - 图4

Parsed 已解析

当我们第一次尝试注册一个 Service Worker 的时候,用户代理(浏览器)解析脚本并获取入口点。
如果解析成功(并且其他比如 HTTPS 的条件满足),我们就可以访问 Service Worker 的 registration 对象。
它包含这个 Service Worker 的状态信息以及它的作用域。

  1. /* In main.js */
  2. if ('serviceWorker' in navigator) {
  3. navigator.serviceWorker.register('./sw.js')
  4. .then(function(registration) {
  5. console.log("Service Worker Registered", registration);
  6. })
  7. .catch(function(err) {
  8. console.log("Service Worker Failed to Register", err);
  9. })
  10. }

Service Worker 的成功注册并不代表它已经被安装或者是被激活,只是脚本被成功解析而已,它与当前文档同源,并且是 HTTPS 协议。
一旦解析完成,Service Worker 就开始进入下一个状态。

Installing 正在安装

一旦 Service Worker 脚本已经被解析,用户代理(浏览器)就准备安装它,它开始进入正在安装的状态。
在 Service Worker 的 registration 对象中,我们能够通过检查 installing 属性来知道它是否处于正在安装的状态。

  1. /* In main.js */
  2. navigator.serviceWorker.register('./sw.js').then(function(registration) {
  3. if (registration.installing) {
  4. // Service Worker is Installing
  5. }
  6. })

在正在安装的状态中,Service Worker 脚本可以处理 install 事件,在一个典型的 install 事件中,我们通常会为文档缓存一些静态文件。

  1. /* In sw.js */
  2. self.addEventListener('install', function(event) {
  3. event.waitUntil(
  4. caches.open(currentCacheName).then(function(cache) {
  5. return cache.addAll(arrayOfFilesToCache);
  6. })
  7. );
  8. });

如果在事件对象中有一个 event.waitUntil() 方法,接受一个 Promise 参数,在这个 Promise 对象没有被 resolved 之前,install 事件都不会成功。如果 Promise 被 rejected 了,那么 install 事件失败,Service Worker 变成冗余态(redundant)。

  1. /* In sw.js */
  2. self.addEventListener('install', function(event) {
  3. event.waitUntil(
  4. return Promise.reject(); // Failure
  5. );
  6. });

Installed / Waiting 已安装

如果安装成功,Service Worker 的状态变为已安装(installed),也被称为等待中(waiting)。
处于这个状态代表它是一个合法的,但并未被激活的 worker。
它还没有控制文档,或者应该说是它正准备从当前 worker 那里接管文档。
在 Service Worker 的 registration 对象中,我们可以通过 waiting 属性来判断它是否处于该状态。

  1. /* In main.js */
  2. navigator.serviceWorker.register('./sw.js').then(function(registration) {
  3. if (registration.waiting) {
  4. // Service Worker is Waiting
  5. }
  6. })

这是一个很好的时机去通知应用的用户更新一个新的版本,或者为他们自动更新。

Activating 正在激活

在如下几个场景下,一个处于 waiting 状态的 Service Worker 会被触发,成为 activating 状态:

  • 当前没有激活的 worker
  • 如果在 Service Worker 的脚本中 self.skipWaiting() 被调用
  • 如果用户访问其他页面并释放了之前激活的 worker
  • 在一个特定的时间过去后,之前一个激活的 worker 被释放

处于正在激活(activating)状态下,我们可以在 Service Worker 脚本中处理 activate 事件。
在一个典型的 activate 事件中,我们可以清除缓存中的文件。

  1. /* In sw.js */
  2. self.addEventListener('activate', function(event) {
  3. event.waitUntil(
  4. // Get all the cache names
  5. caches.keys().then(function(cacheNames) {
  6. return Promise.all(
  7. // Get all the items that are stored under a different cache name than the current one
  8. cacheNames.filter(function(cacheName) {
  9. return cacheName != currentCacheName;
  10. }).map(function(cacheName) {
  11. // Delete the items
  12. return caches.delete(cacheName);
  13. })
  14. ); // end Promise.all()
  15. }) // end caches.keys()
  16. ); // end event.waitUntil()
  17. });

和 install 事件类似,有一个 event.waitUntil() 方法,在传入的 Promise 没有 resolved 之前,激活都不会成功。
如果 Promise 被 rejected 了,那么 activate 事件将失败,Service Worker 变成冗余状态。

Activated 已激活

如果激活成功了,Service Worker 变为激活(active)状态。
如果处于此状态,那么它就是一个完成控制着文档的激活了的 worker。
我们可以通过 active 属性来检查它是否处于此状态。

  1. /* In main.js */
  2. navigator.serviceWorker.register('./sw.js').then(function(registration) {
  3. if (registration.active) {
  4. // Service Worker is Active
  5. }
  6. })

当一个 Service Worker 被激活了,它就可以处理一些有用的事件了:fetch 以及 message

  1. /* In sw.js */
  2. self.addEventListener('fetch', function(event) {
  3. // Do stuff with fetch events
  4. });
  5. self.addEventListener('message', function(event) {
  6. // Do stuff with postMessages received from document
  7. });

Redundant 冗余

一个 Service Worker 变成冗余状态可以有如下几个原因:

  • 处于 installing 状态时安装失败
  • 处于 activating 状态时激活失败
  • 一个新的 Service Worker 代替了它成为了激活的 Service Worker

如果 Service Worker 是由于前两个原因变成冗余状态的,我们可以在开发者工具中看到它们:
Service Worker - 图5
如果之前有一个激活的 Service Worker,那么它将继续控制着文档。

实现

Service Worker - 图6

检测兼容性

首先我们要做的是检测当前浏览器是否支持ServiceWorker,目前Firefox和Chrome浏览器支持。

  1. if ('serviceWorker' in navigator) {
  2. navigator.serviceWorker.register(sw.js) // 注册sw.js 文件中变成的服务对象,返回注册成功的对象
  3. .then(function(swReg){
  4. swRegistration = swReg;
  5. }).catch(function(error) {
  6. console.error('Service Worker Error', error);
  7. });
  8. }

编写服务

通常在浏览器端进行本地存储,可以运用 CookiesSessionLocalStoargeCacheStorage 实现。
要实现页面缓存,这里就要运用到 CacheStorage
它提供了一个 ServiceWorker 类型的工作者或 window 范围可以访问的所有命名缓存的主目录, 并维护字符串的映射名称到相应的 Cache 对象。

  1. // sw.js
  2. 'use strict'
  3. let cacheName = 'pwa-demo-assets'; // 缓存名字
  4. let imgCacheName = 'pwa-img';
  5. let filesToCache;
  6. filesToCache = [ // 所需缓存的文件
  7. '/',
  8. '/index.html',
  9. '/scripts/app.js',
  10. '/assets/imgs/48.png',
  11. '/assets/imgs/96.png',
  12. '/assets/imgs/192.png',
  13. '/dist/js/app.js',
  14. '/manifest.json'
  15. ];
  16. self.addEventListener('install', function(e) {
  17. e.waitUntil(
  18. // 安装服务者时,对需要缓存的文件进行缓存
  19. caches.open(cacheName).then(function(cache) {
  20. return cache.addAll(filesToCache);
  21. })
  22. );
  23. });
  24. self.addEventListener('fetch', (e) => {
  25. // 判断地址是不是需要实时去请求,是就继续发送请求
  26. if (e.request.url.indexOf('/api/400/200') > -1) {
  27. e.respondWith(
  28. caches.open(imgCacheName).then(function(cache){
  29. return fetch(e.request).then(function (response){
  30. cache.put(e.request.url, response.clone()); // 每请求一次缓存更新一次新加载的图片
  31. return response;
  32. });
  33. })
  34. );
  35. } else {
  36. e.respondWith(
  37. // 匹配到缓存资源,就从缓存中返回数据
  38. caches.match(e.request).then(function (response) {
  39. return response || fetch(e.request);
  40. })
  41. );
  42. }
  43. });

注册服务

你需要引入 Service Worker,这是一个 js 文件,一般命名为 sw.js ,通常放在项目的根目录。

PWA 通过 ServiceWorker 访问 cache ,所以需要注册 ServiceWorker 工作者。
注册ServiceWorker部分逻辑如下:

  1. if ('serviceWorker' in navigator) {
  2. navigator.serviceWorker.register(sw.js)
  3. .then(function(swReg) {
  4. doSomething()
  5. })
  6. .catch(function(error) {
  7. console.error('Service Worker Error', error);
  8. });
  9. } else {
  10. console.warn('serviceWorker is not supported');
  11. }

移除服务

从浏览器删除的方法是:
chrome://serviceworker-internals/

缓存静态资源

要使我们的Web App离线可用,就需要将所需资源缓存下来。
我们需要一个资源列表,当Service Worker被激活时,会将该列表内的资源缓存进cache。

  1. // sw.js
  2. var cacheName = 'bs-0-2-0';
  3. var cacheFiles = [
  4. '/',
  5. './index.html',
  6. './index.js',
  7. './style.css',
  8. './img/book.png',
  9. './img/loading.svg'
  10. ];
  11. // 监听install事件,安装完成后,进行文件缓存
  12. self.addEventListener('install', function (e) {
  13. console.log('Service Worker 状态: install');
  14. var cacheOpenPromise = caches.open(cacheName).then(function (cache) {
  15. return cache.addAll(cacheFiles);
  16. });
  17. e.waitUntil(cacheOpenPromise);
  18. });

可以看到,首先在cacheFiles中我们列出了所有的静态资源依赖。注意其中的'/',由于根路径也可以访问我们的应用,因此不要忘了将其也缓存下来。
当Service Worker install时,我们就会通过caches.open()cache.addAll()方法将资源缓存起来。
这里我们给缓存起了一个cacheName,这个值会成为这些缓存的key。

使用资源缓存

到目前为止,我们仅仅是注册了一个Service Worker,并在其install时缓存了一些静态资源。
为什么呢?因为我们仅仅缓存了这些资源,然而浏览器并不知道需要如何使用它们;换言之,浏览器仍然会通过向服务器发送请求来等待并使用这些资源。那怎么办?
我们需要用Service Worker来帮我们决定如何使用缓存。

有cache时的静态资源请求流程:

Service Worker - 图7
无cache时的静态资源请求流程:
Service Worker - 图8

  • 浏览器发起请求,请求各类静态资源(html/js/css/img);
  • Service Worker拦截浏览器请求,并查询当前cache;
  • 若存在cache则直接返回,结束;
  • 若不存在cache,则通过fetch方法向服务端发起请求,并返回请求结果给浏览器
  1. // sw.js
  2. self.addEventListener('fetch', function (e) {
  3. // 如果有cache则直接返回,否则通过fetch请求
  4. e.respondWith(
  5. caches.match(e.request).then(function (cache) {
  6. return cache || fetch(e.request);
  7. }).catch(function (err) {
  8. console.log(err);
  9. return fetch(e.request);
  10. })
  11. );
  12. });

fetch事件会监听所有浏览器的请求。e.respondWith()方法接受Promise作为参数,通过它让Service Worker向浏览器返回数据。caches.match(e.request)则可以查看当前的请求是否有一份本地缓存:如果有缓存,则直接向浏览器返回cache;否则Service Worker会向后端服务发起一个fetch(e.request)的请求,并将请求结果返回给浏览器。

更新资源

当我们将资源缓存后,除非注销(unregister)sw.js、手动清除缓存,否则新的静态资源将无法缓存。
解决这个问题的一个简单方法就是修改cacheName
由于浏览器判断sw.js是否更新是通过字节方式,因此修改cacheName会重新触发install并缓存资源。
此外,在activate事件中,我们需要检查cacheName是否变化,如果变化则表示有了新的缓存资源,原有缓存需要删除。

  1. // sw.js
  2. // 监听activate事件,激活后通过cache的key来判断是否更新cache中的静态资源
  3. self.addEventListener('activate', function (e) {
  4. console.log('Service Worker 状态: activate');
  5. var cachePromise = caches.keys().then(function (keys) {
  6. return Promise.all(keys.map(function (key) {
  7. if (key !== cacheName) {
  8. return caches.delete(key);
  9. }
  10. }));
  11. })
  12. e.waitUntil(cachePromise);
  13. return self.clients.claim();
  14. });

类库

sw-toolbox (16年后不再维护)

提供了动态缓存使用的通用策略, 这些动态的资源不合适用 sw-precache 预先缓存。

sw-precache (16年后不再维护)

用来生成配置使 PWA 在安装时进行静态资源的缓存。

workbox

简介

可以把 Workbox 理解为 Google 官方的 PWA 框架。
它解决的就是用底层 API 写 PWA 太过复杂的问题。这里说的底层 API,指的就是去监听 SW 的 install、active、 fetch 事件做相应逻辑处理等。
[ 传送门 ]

策略

Stale-While-Revalidate

Service Worker - 图9

Cache First

Service Worker - 图10

Network First

Service Worker - 图11

Network Only

Service Worker - 图12

Cache Only

Service Worker - 图13

示例

  1. // 首先引入 Workbox 框架
  2. importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.3.0/workbox-sw.js');
  3. workbox.precaching([
  4. // 注册成功后要立即缓存的资源列表
  5. ]);
  6. // html的缓存策略
  7. workbox.routing.registerRoute(
  8. new RegExp(''.*\.html'),
  9. workbox.strategies.networkFirst()
  10. );
  11. workbox.routing.registerRoute(
  12. new RegExp('.*\.(?:js|css)'),
  13. workbox.strategies.cacheFirst()
  14. );
  15. workbox.routing.registerRoute(
  16. new RegExp('https://your\.cdn\.com/'),
  17. workbox.strategies.staleWhileRevalidate()
  18. );
  19. workbox.routing.registerRoute(
  20. new RegExp('https://your\.img\.cdn\.com/'),
  21. workbox.strategies.cacheFirst({
  22. cacheName: 'example:img'
  23. })
  24. );

通过 workbox.precaching 中的是 install 以后要塞进 caches 中的内容,workbox.routing.registerRoute 中第一个参数是一个正则,匹配经过 fetch 事件的所有请求,如果匹配上了,就走相应的缓存策略 workbox.strategies 对象为我们提供了几种最常用的策略。

CDN

国内可以使用阿里的CDN
https://g.alicdn.com/kg/workbox/版本号/workbox-sw.js

  1. importScripts('https://g.alicdn.com/kg/workbox/3.3.0/workbox-sw.js');
  2. workbox.setConfig({
  3. modulePathPrefix: 'https://g.alicdn.com/kg/workbox/3.3.0/'
  4. });

参考链接 https://naturaily.com/blog/pwa-vue-cli-3 https://juejin.im/entry/57f89f938ac2470058ac32c4 https://www.jianshu.com/p/25331bf16543