Web Workers(二) - Service Worker

1,143 阅读6分钟

本篇文章会介绍ServiceWorker的基本概念,基本用法和一些使用场景。

基本概念

ServiceWorker(服务worker)一般作为web应用程序、浏览器和网络(如果可用)之前的代理服务器。它们旨在(除开其他方面)创建有效的离线体验,拦截网络请求,以及根据网络是否可用采取合适的行动并更新驻留在服务器上的资源。他们还将允许访问推送通知和后台同步API。

Service worker是一个注册在指定源和路径下的事件驱动worker。它采用JavaScript控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。你可以完全控制应用在特定情形(最常见的情形是网络不可用)下的表现。

Service worker运行在worker上下文,因此它不能访问DOM。相对于驱动应用的主JavaScript线程,它运行在其他线程中,所以不会造成阻塞。它设计为完全异步,同步API(如XHRlocalStorage (en-US))不能在service worker中使用。

出于安全考量,Service workers只能由HTTPS承载,毕竟修改网络请求的能力暴露给中间人攻击会非常危险。但是针对本地开发(localhost,或127.0.0.1)并没有这个限制,所以还是考虑很周到的。

和Web Worker比较

相同点:

  • 他们都是在JS引擎线程以外开辟了新的JS线程。

不同点:

  • Service Worker 不是服务于某个特定页面的,而是服务于多个页面的。(按照同源策略)
  • Service Worker 会常驻在浏览器中,即便注册它的页面已经关闭,Service Worker 也不会停止。本质上它是一个后台线程,只有你主动终结,或者浏览器回收,这个线程才会结束。
  • 生命周期、可调用的 API 等等也有很大的不同。

可以说,Service Worker是Web Worker进一步发展的产物,Service Worker从功能上要比Web Worker要强大的多。

基本使用

注册

使用 ServiceWorkerContainer.register() 方法首次注册service worker。如果注册成功,service worker就会被下载到客户端并尝试安装或激活,这将作用于整个域内用户可访问的URL,或者其特定子集。

if (navigator.serviceWorker) {
      navigator.serviceWorker.register("serviceWorker.js").then((registration) => {
          if (registration.installing) {
              console.log("client-installing...");
          }
          else if (registration.active) {
              console.log("client-active...");
          }
      }, (err) => {
          console.error('注册serviceWorker.js出错');
      });
}

下载安装和激活

ServiceWorker将遵守以下生命周期。

  1. 下载 - 下载注册的JS文件
  2. 安装
  3. 激活

用户首次访问service worker控制的网站或页面时,service worker会立刻被下载。

ServiceWorker的状态变化:installing → installed → activating → activated。

这些对应的状态,Service Worker有对应的事件名进行捕获。

self.addEventListener('install', function(event) { /* 安装后... */ });
self.addEventListener('activate', function(event) { /* 激活后... */ });

最后,ServiceWorker支持fetch事件监听,来响应和拦截各种请求。

self.addEventListener('fetch', function(event) { /* 请求后... */ });

基本上,目前Service Worker的所有应用都是基于上面3个事件的,例如,我们要介绍的缓存和离线开发,'install'用来缓存文件,'activate'用来缓存更新,'fetch'用来拦截请求直接返回缓存数据。三者齐心,构成了完成的缓存控制结构。

需要注意的是:

如果这是首次启用service worker,页面会首先尝试安装,安装成功后它会被激活。

如果现有service worker已启用,新版本会在后台安装,但不会被激活,这个时序称为worker in waiting。直到所有已加载的页面不再使用旧的service worker才会激活新的service worker。只要页面不再依赖旧的service worker,新的service worker会被激活(成为active worker)。

开发调试过程中,可以把“Update On Reload”选中,这样每次都不用等待,而是直接激活。

因为我们下面的例子要用到cacheStorage,所以还需要了解下cacheStorage的API,但是本文不会对它做介绍,感兴趣的可以去查阅MDN。
developer.mozilla.org/zh-CN/docs/…

serviceWorker的API:

developer.mozilla.org/zh-CN/docs/…

离线缓存的例子

还有传统的浏览器缓存,也就是强缓存和协商缓存,其共同点都是通过设置HTTP Header(expires,cache-control,Last-Modified/If-Modified-Since,Etag/If-None-Match)实现,但是浏览器缓存也有缺点:

  1. 当没有网络的时候,应用无法访问,因为HTML页面需要去服务器获取;
  2. 缓存不可编程,无法通过JS来精细的对缓存进行增删改查;

为了在无网络下也能访问应用,HTML5 规范中设计了应用缓存(Application Cache)这么一个新的概念。通过它,我们可以做离线应用。然而,由于这个 API 的设计有太多的缺陷,被很多人吐槽,最终被废弃。

比如appCache的槽点:

  1. 更新机制:一旦你采用了manifest之后,你将不能清空这些缓存,只能更新缓存,或者得用户自己去清空这些缓存。
  2. manifest本身的编写要求非常严格,稍有疏忽,将导致缓存无效;
  3. 不能直观的看到缓存里有哪些数据,数据信息等;
  4. 限制大小的问题(5MB);
  5. ......

为了能够精细地、可编程地控制缓存,CacheStorage 被设计出来。有了它,就可以用 JS 对缓存进行增删改查,你也可以在 Chrome 的 DevTools 里面直观地查看。对于传统的 Header 缓存(强缓存和协商缓存),你是没法知道有哪些缓存,更加没法对缓存进行操作的。你只能被动地修改 URL 让浏览器抛弃旧的缓存,使用新的资源。

需要注意的是,CacheStorage 并非只有在 Service Worker 中才能用,它是一个全局性的 API,你在控制台中也可以访问到 caches 全局变量。

套路模式

  1. 先在页面上注册一个serviceWorker;
  2. 监听install事件,在回调里缓存想要缓存的资源;
  3. 监听activate事件,在回调里更新缓存;
  4. 监听fetch事件,在回调里捕获请求,并返回缓存的数据;

例子代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Service Worker</title>
</head>
<body>
    <div id="app"></div>
    <script src="./main.js"></script>
    <script>
        if (navigator.serviceWorker) {
            navigator.serviceWorker.register("serviceWorker.js").then((registration) => {
                if (registration.installing) {
                    console.log("client-installing...");
                }
                else if (registration.active) {
                    console.log("client-active...");

                    // 接收消息
                    navigator.serviceWorker.addEventListener("message", function(event){
                        console.log("页面收到消息: ", event.data);
                    });

                    // 像特定serviceWorker发送消息
                    registration.active.postMessage("哈哈!");
                }

            }, (err) => {
                console.error('注册serviceWorker.js出错');
            });
        }
    </script>
</body>
</html>
let app = document.querySelector("#app");
app.innerHTML = "<h1>Hello</h1>";
var cashStorageKey = 'v5';

var cacheList = [
    "index.html",
    "main.js"
];

// 缓存
self.addEventListener('install', function(event){
    console.log("安装serviceWorker");

    event.waitUntil(
        caches.open(cashStorageKey).then((cache) => {
            return cache.addAll(cacheList);
        })
    );
});

// 缓存更新
self.addEventListener("active",function(event){
    console.log("激活serviceWorker");

    event.waitUntil(
        caches.keys().then(function(keyList) {
            return Promise.all(keyList.map(function(key){
                if (key != cashStorageKey) {
                    return caches.delete(key);
                }
            }));
        })
    );

});

self.addEventListener("fetch", function(event){
    console.log("请求了: ", event.request.url);

    event.respondWith(
        caches.match(event.request).then((res) => {
            if (res) {
                // 有缓存
                console.log(new Date(), 'fetch ', event.request.url, '有缓存,从缓存中取');
                return res;
            }
            else {
                // 没有缓存
                console.log(new Date(), 'fetch ', event.request.url, '没有缓存,网络获取');
                return fetch(event.request)
                    .then(function(response){
                        return caches.open(cashStorageKey).then(function(cache){
                            cache.put(event.request, response.clone());
                            return response;
                        });
                    });
            }
        })
    );
});

self.addEventListener('message', (event) => {
    console.log("serviceWorker收到了消息: ", event.data);

    self.clients.matchAll().then(clients => {
        clients.forEach(client => {
            client.postMessage("嘿嘿");
        });
    });
});

运行效果:

serviceWorker:

cache Storage:

Network:

offline:

离线功能从此达成,出乎意料的实用和强大。

另外,Service Worker和主线程通信的例子就不单独演示了,上面的代码已经包含了如何通信,比较简单。

Service Worker使用场景

在网上大部分的文章都会那ServiceWorker实现离线缓存的例子来讲解,但是实际上Service Worker只是一个常驻在浏览器中的JS线程,它能做什么,完全取决使用者和谁搭配一起使用。

  1. 和Fetch搭配使用,可以从浏览器层面拦截请求,做数据Mock。
  2. 和Fetch和CacheStorage搭配使用,可以做离线应用;
  3. 和Push和Notification搭配使用,可以像Native App那样的消息推送;
  4. ......

假如把这些技术融合在一起,差不多就成了PWA了,
总而言之,Service Worker是一种非常关键的技术,有了它,我们可以实现更接近浏览器底层,可以做更多的事情。