美文网首页
Service Worker

Service Worker

作者: 绝尘kinoko | 来源:发表于2022-05-27 10:42 被阅读0次

    上回研究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/<project-name>

    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

          本文链接:https://www.haomeiwen.com/subject/kwriprtx.html