Service Worker

上回研究Web Worker时偶然看到了Service Worker(后文简称sw),这次来学习一下。

Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。

MDN开卷语解释了sw的使用场景和角色,核心为两点

  • HTTP请求的本地代理服务器
  • 响应和资源的缓存管理器

第二点本质是Cache,后文会提到。
另外补充几点可能的应用场景:

  • 全静态站点,可离线
  • 预加载,首屏渲染性能方案
  • 临时伪造响应,捕获5xx错误码,返回固定数据

生命周期

首先看下Chrome浏览器对sw的支持


sw

最下面update cycle就是生命周期钩子,还有一个download钩子没显示。

注册

sw需要被注册到客户端才能发挥作用,代码如下

if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('sw.js').then(
        function (registration) {
            console.log(':) Success. ', registration.scope);
        },
        function (err) {
            console.log(':( Failed. ', err);
        }
    );
}
success

因为要注册同源文件,所以这里客户端我是用express服务端渲染了一个页面。
注册成功后会经历下载、安装、激活三个生命周期,sw可以监听这三个事件,可以当成是钩子函数。

事件监听

监听是sw内部的处理,分为监听生命周期事件和请求、通信事件

监听生命周期事件

install

const CACHE_NAME = 'cacheName';
const urlsToCache = [
  '/',
  '/styles/main.css',
  '/script/main.js',
  '/imgs/1.jpg'
];
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});

上述做的事就是待sw安装完成后,将一些文件缓存起来。
插入介绍一下一些概念:
cache,这个在浏览器里可以看到


cache

caches,全局变量,是个CacheStorage对象。cache,Cache对象。类似数据库和数据表的关系。API不做介绍,看名字也能知道啥意思。

ExtendableEvent.waitUntil() 方法告诉事件分发器该事件仍在进行。这个方法也可以用于检测进行的任务是否成功。在服务工作线程中,这个方法告诉浏览器事件一直进行,直至 promise 解决,浏览器不应该在事件中的异步操作完成之前终止服务工作线程。

activate

激活事件标志着从这之后的请求将被Service Worker接管,所以通常用于区分Service Worker接管前后的分隔。 但这不意味着当前这次有效完成".register()"的页面会受到Service Worker 管理,因为我们无法预测页面资源先获取完成还是激活事件先响应。

监听请求、通信事件

fetch

self.addEventListener('fetch', (event) => {
    const url = new URL(event.request.url);
    if (url.origin == location.origin && url.pathname.endsWith('/imgs/2.jpg')) {
        event.respondWith(caches.match('/imgs/1.jpg'));
    }
});

监听fetch会处理所有请求,类似express监听所有路由的中间件。
上面是判断请求2.jpg时,返回1.jpg的缓存。另外,img标签也算fetch。
这里也有几个比较陌生的API:

  • FetchEvent.respondWith(Response | network error | fetch)
  • new Response(body?, option?)
  • body: Blob | BufferSource | FormData | ReadableStream | URLSearchParams | USVString

我按照FormData和Blob的格式分别试验了一下返回体

if (url.pathname == '/test1') {
    let blob = new Blob(['test...']);
    event.respondWith(new Response(blob));
}
if (url.pathname == '/test2') {
    let formdata = new FormData();
    formdata.append('name', 'test');
    formdata.append('attr2', 'value2');
    event.respondWith(new Response(formdata));
}

formdata的fetch返回用text()格式化如下


formdata

Blob的fetch返回也是用text()处理,值为构造体数组内的元素,这里就是test...字符串。
PS:本地调试更新sw.js时需要刷新浏览器,并更新service worker


update

要手动点skipWaiting,不然一直waiting。

通信
通过MessageChannel进行通信。

Channel Messaging API的MessageChannel接口允许我们创建一个新的消息通道,并通过它的两个MessagePort属性发送数据。

其构造实例有两个属性port1和port2,port组成为onmessage和onmessageerror。用法是用其中一个port发送,一个port接收。
客户端

sendMessage('test...').then((res) => {
    console.log(res);
});
function sendMessage(message) {
    return new Promise(function (resolve, reject) {
        var messageChannel = new MessageChannel();
        messageChannel.port1.onmessage = function (event) {
            if (event.data.error) {
                reject(event.data.error);
            } else {
                resolve(event.data);
            }
        };
        navigator.serviceWorker.controller.postMessage(message, [messageChannel.port2]);
    });
}

sw.js

addEventListener('message', (event) => {
    console.log(`The client sent me a message: ${event.data}`);
    // clients.matchAll().postMessage({
    //     msg: 'Hey I just got a fetch from you!'
    // });
    event.ports[0].postMessage({
        msg: 'res test...'
    });
});

代码比较简单,就是发送和接收,比较需要注意的是发送的主体为navigator.serviceWorker.controller。


Class

注释掉的部分是参考知乎一篇文章的步骤,但是api不对,但也可以简单了解一下client
clients 提供对Client对象的访问,在service worker中使用
clients.get(id)
clients.matchAll({includeUncontrolled?: boolean, type?: 'window' | 'worker' | 'sharedworker' | 'all'})
这两个都是返回promise,不能直接调用postMessage。

资源加载策略与开源sw框架

这部分我没有实践,纯搬运

  • 仅使用Cache(Cache only)
    几乎没用
self.addEventListener('fetch', function(event) {
  event.respondWith(caches.match(event.request));
});
  • 仅使用网络(Network only)
    需要强制更新的资源,时效性要求很高。如不需要离线访问的 HTML 资源。
self.addEventListener('fetch', function(event) {
  event.respondWith(fetch(event.request));
});
  • 先使用 SW 缓存,没有则使用网络资源
self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      return response || fetch(event.request);
    })
  );
});
  • 缓存资源与网络资源,谁快用谁
function promiseAny(promises) {
  return new Promise((resolve, reject) => {
    promises = promises.map(p => Promise.resolve(p));
    promises.forEach(p => p.then(resolve));
    promises.reduce((a, b) => a.catch(() => b))
      .catch(() => reject(Error("All failed")));
  });
};
self.addEventListener('fetch', function(event) {
  event.respondWith(
    promiseAny([
      caches.match(event.request),
      fetch(event.request)
    ])
  );
});
  • 优先使用网络,失败则使用缓存(Network )
    对于时效性要求比较高的资源,或者关键性需要降级显示的资源。
self.addEventListener('fetch', function(event) {
  event.respondWith(
    fetch(event.request).catch(function() {
      return caches.match(event.request);
    })
  );
});
  • 先使用 SW 缓存,再访问网络更新缓存 (Fastest)
    可以缓存但可以二次请求后生效的资源。保持相对最高的缓存速度和高失效性。
self.addEventListener('fetch', function(event) {
  fetch(event.request).then(res => caches.update(res));
  event.respondWith(
    caches.match(event.request)
  );
});
  • 检查缓存离线时间
    不同缓存的时效时间可以由服务获取,然后开启定期检查,消灭失效资源。
setInterval(async () => {
  const res = await fetch('/pageA/sw-cache-config');
  const data = await res.json();
  caches.checkCacheLifeTime(data);
}, SW_CACHE_INTERVAL);

开源框架
Workbox、sw-toolbox,等用到的时候再研究,都是简化写法的。

Angular Service Worker

Angular体系内也有sw,简单用了一下,基本只有缓存功能,监听没找到api

  1. 添加sw
    ng add @angular/pwa --project *project-name*
    我是直接在主应用加的

  2. 打包
    ng build

  3. 启动打包的项目
    http-server dist/

  4. 模拟网络离线


    offline
  5. 刷新页面


    network

可以看到size列都是sw

  1. 更新dist
    修改内容,重新打包启动
    刷新缓存即可看到变化,这个离线部署的方案感觉还行。

配置文件
主要配置文件是根目录下的ngsw-config.json文件
默认创建的如下:

{
  "$schema": "./node_modules/@angular/service-worker/config/schema.json",
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/manifest.webmanifest",
          "/*.css",
          "/*.js"
        ]
      }
    },
    {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": {
        "files": [
          "/assets/**",
          "/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
        ]
      }
    }
  ]
}

assetsGroups数据结构

interface AssetGroup {
  name: string;
  installMode?: 'prefetch' | 'lazy';
  updateMode?: 'prefetch' | 'lazy';
  resources: {
    files?: string[];
    urls?: string[];
  };
  cacheQueryOptions?: {
    ignoreSearch?: boolean;
  };
}

总结:
sw主要用来做离线缓存,目前接触不到类似的业务场景。
MessageChannel是目前用过最不好的通信api,2个port命名就过于随意,而且可以互换,只要不用同一个port就行。
阮大的博客错误略多,错别字和代码缺失,demo阻塞只能看看其他的,结果收获颇丰;学习新东西还是不能在一个地方死钻,多处借鉴能查漏补缺。

reference:
MDN
https://www.bookstack.cn/read/webapi-tutorial/docs-service-worker.md
https://zhuanlan.zhihu.com/p/161204142
https://angular.cn/guide/service-worker-getting-started
https://juejin.cn/post/6996901512462991374#heading-1

你可能感兴趣的:(Service Worker)