PWA系列 - 我所知道的 Service Worker

624 阅读6分钟

PWA系列

PWA系列 - 我所知道的 Service Worker

PWA系列 - Workbox插件GenerateSW

PWA系列 - Workbox插件InjectManifest

Service Worker 是什么

  • SW 运行中 Web 浏览器和 Web 服务器之间,即是中间代理
  • SW 运行在 独立线程中,不能操作DOM,可访问原生应用功能
  • SW 的工作方式是,先安装(install) -> 启动(activate) -> 控制页面
  • SW 可拦截http请求(fetch)
  • SW 很重要的功能是缓存(Cache),完全独立于 HTTP 缓存的缓存机制,浏览器http 缓存受 HTTP 标头中指定的缓存指令影响,SW 的Cache可自主编程
  • SW 为了安全,只能通过 HTTPS 或 localhost 使用

新建一个 Service Worker

注册 sw.js 文件

监听页面load完成后,注册sw.js即可,register方法有两个参数

  • 第一个是sw.js路径
  • 第二个指明sw的作用范围,sw的范围默认是由页面位置决定的,如路径是 /static/sw.js ,那么范围就是/static和其子目录的html能被/static/sw.js控制。可设置对象 {scope: './'}, sw.js只能控制scope范围内的页面。
  • 注册的sw.js最好就放到根目录下,使得sw.js能控制所有页面
  • scope范围是指定sw.js能控制的页面,但是http请求拦截却是可以拦截任意范围的,包括跨域的请求
  window.addEventListener("load", function () {
    if ("serviceWorker" in navigator) {
      navigator.serviceWorker.register("/sw.js");
    }
  });

sw.js 中监听事件

  • sw类似一个app应用,需要安装,启动打开;因为是中间代理,所以可以拦截http请求
  • sw运行在独立线程中,self变量是全局对象
  • sw默认第一次安装且启动后,不会立即拥有控制当前页面的权限,而是下次打开页面时才能控制页面,所以最好是启动后,立即让sw拥有控制权
  • install事件会在register后执行,install只会第一次触发,sw.js更新后会触发,一把做预请求资源到缓存
  • activate事件会在install后执行,install只会第一次触发,sw.js更新后会触发,一般做新旧缓存资源清理
  • sw会重新安装,情况一般是三种,navigator.serviceWorker.register("/sw3.js", { scope: "./updata" }),sw.js地址变了scope变了sw.js内容变了
self.addEventListener("install", async (event) => {
  console.log("开始安装", event);
  event.waitUntil(self.skipWaiting())
});
self.addEventListener("activate", async (event) => {
  console.log("安装完成,开始启动", event);
  event.waitUntil(self.clients.claim())
});
self.addEventListener("fetch", async (event) => {
  console.log("运行中,拦截请求", event.request);
  const url = new URL(event.request.url);
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
  }
});

sw.js与html通信

postMessage

html js中注册监听 与 发送消息

// 收到消息
navigator.serviceWorker.addEventListener("message",function (event) {
    console.log("收到消息sw->html", event.data);
  }
);

// 发送消息
navigator.serviceWorker.controller.postMessage({
    action: "the action event",
});

sw.js 中注册监听 与 发送消息

self.addEventListener("message", function (event) {
  // 收到消息
  console.log("收到消息html->sw", event.data);
  
  // 发送消息
  this.self.clients.matchAll().then(function (clients) {
    clients.forEach((client) => {
      client.postMessage({
        action: "response message",
      });
    });
  });
});

fetch 拦截

  • 利用fetch可以拦截的特性,html中发送api请求,sw拦截到后计算返回结果达到通信目的

比如: 需要让sw处理某些计算,可以发送一个请求logo.png(注意:这个logo.png需要真实存在),sw.js中拦截到后,处理某些业务再返回

html js中发送请求

fetch("/logo.png?action=redirect").then(async (res) => {
    res = await res.json();
    console.log(res);
});

sw.js 中拦截

self.addEventListener("fetch", async (event) => {
  console.log("运行中,拦截请求", event.request);
  const url = new URL(event.request.url);
  if (
    url.pathname == "/logo.png" &&
    url.searchParams.get("action") == "redirect"
  ) {
    // 拦截到后,处理业务再event.respondWith返回
    request = new Request(
      "http://wthrcdn.etouch.cn/weather_mini?citykey=101280101"
    );
    event.respondWith(fetch(request));
  } else {
    event.respondWith(fetch(event.request));
  }
});

扩展延伸说明

install 事件的 event.waitUntil

  • event.waitUntil是回调函数,接收Promise对象,event.waitUntil(self.skipWaiting())意思就是不等待,这句话也可不写,下面的setTimeout等待10秒后,才会执行后续activate事件,如果waitUntil得到reject,那么安装失败
self.addEventListener("install", async (event) => {
  console.log("开始安装", event);
  event.waitUntil(
    new Promise((resolve) => {
      setTimeout(resolve, 10000);
    })
  );
});

install 事件的 cache 做预请求

  • sw最佳实际是与缓存结合,预请求资源写在install中,讲这些js和css预请求存在MyFancyCacheName_v1的缓存名中,cache.addAll返回Promise,等都请求到后表示install完成,才继续执行后续activate事件
  • 预请求的文件会存放在service worker存储中,页面请求时,在常见http请求缓存时,文件来源是 Memory Cache, 而此时是 Service Worker
self.addEventListener("install", async (event) => {
    const cacheKey = 'MyFancyCacheName_v1';
    event.waitUntil(caches.open(cacheKey).then((cache) => {
      return cache.addAll([
        '/css/global.bc7b80b7.css',
        '/css/home.fe5d0b23.css',
        '/js/home.d3cc4ba4.js',
        '/js/jquery.43ca4933.js'
      ]);
    }));
});

activate 事件中立即接管控制页面

  • event.waitUntil(self.clients.claim()) 写在activate事件事件中,意思是sw启动后,立即接管页面控制,因为默认是下一次页面打开后,sw能接管页面控制权限
self.addEventListener("activate", async (event) => {
  console.log("安装完成,开始启动", event);
  event.waitUntil(self.clients.claim())
});

activate 事件中更新清理缓存

sw.js修改后,浏览器会请求并重新install和activate,如果sw.js更新后,浏览器获得新sw,此时新sw安装后启动,但是默认并没有获得页面控制,页面还是被旧sw控制的,知道下次打开页面,除非执行self.clients.claim()立即获得控制

self.addEventListener('activate', (event) => {
  // 更改缓存名,原来是MyFancyCacheName_v1
  const cacheAllowList = ['MyFancyCacheName_v2'];

  // 获取所有缓存名
  event.waitUntil(caches.keys().then((keys) => {
    // 删除旧缓存
    return Promise.all(keys.map((key) => {
      if (!cacheAllowList.includes(key)) {
        return caches.delete(key);
      }
    }));
  }));
});

fetch的POST获取参数

html js中发送POST请求

fetch("/logo.png", {
    method: "POST",
    mode: "cors",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      action: "dosomejob",
    }),
}).then(async (res) => {
    res = await res.json();
    console.log(res);
});

sw.js中获取参数,但是event.respondWith(fetch(request))不可用了

self.addEventListener("fetch", async (event) => {
  console.log("运行中,拦截请求", event.request);
  const url = new URL(event.request.url);
  if (url.pathname == "/logo.png" && event.request.method == "POST") {
    const body = await event.request.clone().text();
    console.log('body',body)
  }
});

fetch拦截请求,做缓存策略

要理解到这里不是http缓存,是低等级的缓存依靠header头,而且js可编程的缓存,是高级的自定义的缓存

这里介绍五种常用缓存策略

Cache only (仅缓存)

所有资源都先在install中预先请求,不然后续也不会缓存,而且资源永远不会更新,除非sw.js更新后重新安装

  • 这种方式对于js、css、image等资源文件是很好的,因为现在的资源文件都打有hashcode,有更新会重新预缓存
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  const isPrecachedRequest = precachedAssets.includes(url.pathname);
  // 判断是否有缓存
  if (isPrecachedRequest) {
    // 从缓存中取
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request.url);
    }));
  } else {
    // 没有就从请求网络
    return;
  }
});

Network only (仅网络)

所有资源都不缓存,都从网络取

self.addEventListener('fetch', (event) => {
    return;
});

Cache first (缓存优先)

  • 先检查缓存中有没有,如果有就立即返回
  • 如果缓存没有,就网络请求,并设置缓存
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request.url).then((cachedResponse) => {
        // 有就返回
        if (cachedResponse) {
          return cachedResponse;
        }

        // 没有就请求
        return fetch(event.request).then((fetchedResponse) => {
          cache.put(event.request, fetchedResponse.clone());
          return fetchedResponse;
        });
      });
    }));
  } else {
    return;
  }
});

Network first (网络优先)

  • 每次都从网络加载数据,并设置到缓存
  • 如果网络请求失败,就用上次的缓存数据
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  if (event.request.mode === 'navigate') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      // 每次都网络请求
      return fetch(event.request.url).then((fetchedResponse) => {
        cache.put(event.request, fetchedResponse.clone());

        return fetchedResponse;
      }).catch(() => {
        // 如果加载失败
        return cache.match(event.request.url);
      });
    }));
  } else {
    return;
  }
});

Stale-while-revalidate (过时验证缓存)

  • 每次直接返回缓存数据,同时发起网络请求,将网络数据更新本地缓存
  • 后续每次请求都相当于最近一次最新数据
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request).then((cachedResponse) => {
        const fetchedResponse = fetch(event.request).then((networkResponse) => {
          cache.put(event.request, networkResponse.clone());

          return networkResponse;
        });

        return cachedResponse || fetchedResponse;
      });
    }));
  } else {
    return;
  }
});