likes
comments
collection
share

🫧🫧🫧ServiceWorkerGlobalScope 让你重新认识 ServiceWorker

作者站长头像
站长
· 阅读数 23

ServiceWorkerGlobalScope和上两章的概念基本相同,都是继承自WorkerGlobalScope,但是ServiceWorkerGlobalScope 是在ServiceWorker中使用的,所以它的属性和方法也是ServiceWorker中使用的。

ServiceWorkerGlobalScope

ServiceWorkerGlobalScopeServiceWorker的全局作用域,不同于之前讲的两个全局作用域,ServiceWorkerGlobalScope 需要有一个状态来维持它的生命周期,这个生命周期也是之前讲ServiceWorker时提到过。

而这些生命周期都是ServiceWorkerGlobalScope的属性,它们都是只读的,所以我们不能直接修改它们的值,只能通过ServiceWorker 的生命周期来修改它们的值。

说到这些生命周期,同时也提到了属性,那么我们就来看一下ServiceWorkerGlobalScope的属性。

属性

还是老样子通过函数签名来认识一下ServiceWorkerGlobalScope的属性。

interface ServiceWorkerGlobalScope extends WorkerGlobalScope {
    // 返回与 ServiceWorker 关联的 Client 对象
    readonly clients: Clients;
    // 返回当前 ServiceWorker 注册的 ServiceWorkerRegistration 对象
    readonly registration: ServiceWorkerRegistration;
    // 返回代表 ServiceWorker 的 ServiceWorker 对象
    readonly serviceWorker: ServiceWorker;

    // 允许 ServiceWorker 跳过注册等待阶段,直接进入激活状态
    skipWaiting(): Promise<void>;

    // 处在安装状态的 ServiceWorker 会触发该事件
    readonly oninstall: EventHandler;
    // 处在激活状态的 ServiceWorker 会触发该事件
    readonly onactivate: EventHandler;
    // 当发送网络请求时,会触发该事件,前提是该请求在 ServiceWorker 中被拦截
    readonly onfetch: EventHandler;

    // 当客户端向 ServiceWorker 发送消息时,会触发该事件
    readonly onmessage: EventHandler;
    // 当 ServiceWorker 向客户端发送消息时,消息体内容不合规范时,会触发该事件
    readonly onmessageerror: EventHandler;
}

clients

clients属性返回一个Clients对象,这个对象是用来管理ServiceWorker关联的Client对象的,我们可以通过clients 对象来获取ServiceWorker关联的Client对象,也可以通过clients对象来向Client发送消息。

在讲SeviceWorker时并没有介绍过Client对象,这里我们就简单介绍一下Client对象的使用。

  • main.js
navigator.serviceWorker.register('./sw.js').then(registration => {
    // 注册成功
    console.log('注册成功');
});

navigator.serviceWorker.ready.then(registration => {
    registration.active.postMessage('Hello from client');
});

navigator.serviceWorker.onmessage = event => {
    console.log('收到来自服务端发送的消息:', event.data);
};
  • sw.js
self.addEventListener('install', function (event) {
    console.log('installing');
});

self.addEventListener('activate', function (event) {
    console.log('Service worker activating...');

});

self.addEventListener('message', event => {
    console.log('收到来自客户端发送的消息:', event.data);

    // 获取 ServiceWorker 关联的 Client 对象
    self.clients.matchAll().then(clients => {
        clients.forEach(client => {
            // 向 Client 发送消息
            client.postMessage('Hello from service worker');
        });
    });
})

通过这个示例我们可以看到,ServiceWorker可以通过clients对象来获取Client对象,也可以通过clients对象来向Client发送消息。

Client对象其实就是注册了ServiceWorker的客户端实例。

registration

registration属性返回一个ServiceWorkerRegistration对象,这个对象是用来管理ServiceWorker 注册的,我们可以通过registration对象来获取ServiceWorker注册的信息,也可以通过registration对象来更新ServiceWorker

ServiceWorkerRegistration对象其实就是活动的ServiceWorker的注册信息,它是直接继承自EventTarget 的,所以它也可以通过addEventListener来监听事件。

registration🎊🎊🎊深入 ServiceWorker,消息推送,后台同步,一网打尽! 这一章中经常使用,所以就不再有代码示例,直接上函数签名。

interface ServiceWorkerRegistration extends EventTarget {
    // ServiceWorkerRegistration 对象的唯一标识
    readonly scope: string;
    // ServiceWorkerRegistration 对象关联的 ServiceWorker 对象
    readonly active: ServiceWorker;
    // ServiceWorkerRegistration 对象关联的 ServiceWorker 对象
    readonly installing: ServiceWorker;
    // ServiceWorkerRegistration 对象关联的 ServiceWorker 对象
    readonly waiting: ServiceWorker;

    // ServiceWorkerRegistration 对象关联的 NavigationPreloadManager 对象
    readonly navigationPreload: NavigationPreloadManager;

    // 更新 ServiceWorkerRegistration 对象关联的 ServiceWorkerUpdateViaCache 对象
    readonly updateViaCache: ServiceWorkerUpdateViaCache;

    // 更新 ServiceWorker
    update(): Promise<void>;

    // 注销 ServiceWorker
    unregister(): Promise<boolean>;

    // 当 ServiceWorkerRegistration 对象关联的 ServiceWorker 对象状态发生变化时,会触发该事件
    readonly onupdatefound: EventHandler;
}

从上面的函数签名可以看到,ServiceWorkerRegistration对象有三个属性,分别是activeinstallingwaiting

这三个属性分别对应着三个状态,分别是activeinstallingwaiting,同时他们指向的也都是ServiceWorker对象;

通常情况这三个属性只有一个是有值的,而另外两个是null,这三个属性的值是根据ServiceWorker 的状态来决定的,如果想要获得ServiceWorker的状态,可以通过ServiceWorker.state来获取。

然后会有两个函数,分别是updateunregister,这两个函数分别用来更新ServiceWorker和注销ServiceWorker

最后还有一个事件onupdatefound,这个事件会在ServiceWorkerRegistration对象关联的ServiceWorker对象状态发生变化时触发。

这些大家都可以自己去尝试一下,都很简单,这里最主要的是navigationPreload属性和updateViaCache属性。

navigationPreload

navigationPreload属性返回一个NavigationPreloadManager对象,这个对象是用来管理ServiceWorker 预加载的,我们可以通过navigationPreload对象来开启ServiceWorker预加载,也可以通过navigationPreload 对象来关闭ServiceWorker预加载。

来看一下NavigationPreloadManager对象的函数签名:

interface NavigationPreloadManager {
    // 开启 ServiceWorker 预加载
    enable(): Promise<void>;

    // 关闭 ServiceWorker 预加载
    disable(): Promise<void>;

    // 设置 ServiceWorker 预加载头信息,默认值为`true`,该值应该是一个字节序列
    setHeaderValue(value: string): Promise<void>;

    // 获取一个 NavigationPreloadState 对象,该对象包含了 ServiceWorker 预加载的状态
    getState(): Promise<NavigationPreloadState>;
}

type NavigationPreloadState = {
    enabled: boolean;
    headerValue: string;
};

从上面的函数签名可以看到,NavigationPreloadManager对象有四个函数,分别是enabledisablesetHeaderValuegetState

enabledisable分别用来开启和关闭ServiceWorker预加载,这个很简单,不多说;

setHeaderValue函数用来设置ServiceWorker预加载的头信息,这个函数的参数是一个字符串,这个字符串应该是一个字节序列;

其实这个函数和上的enabledisable函数一样的,不同的是上面默认设置头部信息值为truefalse,而这个函数可以自定义头部信息的值;

getState获取的就是一个预加载的状态信息,这个状态信息包括了enabledheaderValue 两个属性,分别表示预加载是否开启和预加载的头信息,和上面的enabledisablesetHeaderValue这些函数的使用情况息息相关。

那么这个具体有什么作用呢?其实预加载这个概念大家应该是都清楚的,不同的是这里的预加载指的是ServiceWorker中使用的资源的预加载,直接看示例:

addEventListener("activate", (event) => {
    event.waitUntil(
        self.registration.navigationPreload.enable()
    );
});

addEventListener("fetch", (event) => {
    event.respondWith(
        event.preloadResponse.then((response) => {
            if (response) {
                return response;
            }
            return fetch(event.request);
        })
    );
});

上面的代码在讲ServiceWorker的时候已经见过,没见过的就是navigationPreload.enable()preloadResponse这两个属性;

navigationPreload.enable()就是开启ServiceWorker预加载,preloadResponse 就是获取预加载的资源,如果预加载的资源存在,就直接返回预加载的资源,如果预加载的资源不存在,就通过fetch方法去获取资源;

这里其实还可以加上缓存的逻辑,这里只是为了演示navigationPreload的使用,所以就没有加上缓存的逻辑。

updateViaCache

updateViaCache属性是ServiceWorkerRegistration对象的属性,用来设置ServiceWorker更新的方式,其值可以是importsall ,默认值是imports

imports表示只更新ServiceWorker的脚本,不更新ServiceWorker的资源;

all表示更新ServiceWorker的脚本和资源;

这个属性的作用就是用来设置ServiceWorker更新的方式,如果设置为imports,那么只会更新ServiceWorker 的脚本,不会更新ServiceWorker的资源,如果设置为all,那么ServiceWorker的脚本和资源都会更新。

使用方式就是在注册ServiceWorker的时候设置这个属性:

navigator.serviceWorker.register("sw.js", {
    updateViaCache: "all"
});

ServiceWorker

上面已经介绍了ServiceWorkerGlobalScope对象的属性和方法,这个时候再来看看ServiceWorker 对象的属性和方法,这个对象是ServiceWorkerGlobalScope对象的属性,用来表示当前的ServiceWorker对象,应该就会有不一样的感觉了。

还是先来看看ServiceWorker对象的函数签名:

interface ServiceWorker extends EventTarget {
    // scriptURL 是 ServiceWorker 的脚本地址
    readonly scriptURL: string;
    // state 是 ServiceWorker 的状态
    readonly state: ServiceWorkerState;
    // 当 service worker 的状态发生变化时触发
    readonly onstatechange: ((this: ServiceWorker, ev: Event) => any) | null;

    // postMessage 向 service worker 发送消息
    postMessage(message: any, transfer?: Transferable[]): void;
}

这些在之前讲ServiceWorker的时候都已经混了个眼熟,使用也很简单,看下面的示例:

if ("serviceWorker" in navigator) {
    navigator.serviceWorker.register("sw.js").then((registration) => {
        console.log('ServiceWorker registration successful with scope: ', registration.scope);
    });
}

有没有发现一个问题,就是ServiceWorker对象的函数签名并没有register方法,但是在上面的示例中却可以使用register方法,这是为什么呢?

因为ServiceWorker对象并不是navigator.serviceWorker对象,navigator.serviceWorker对象是ServiceWorkerContainer

ServiceWorkerContainer

ServiceWorkerContainer对象是navigator.serviceWorker对象,用来表示ServiceWorker容器,这个对象的函数签名如下:

interface ServiceWorkerContainer extends EventTarget {
    // controller 是当前的 ServiceWorker
    readonly controller?: ServiceWorker;
    // ready 是一个 Promise,表示 ServiceWorker 是否准备好
    readonly ready: Promise<ServiceWorkerRegistration>;
    
    // register 注册 ServiceWorker
    register(scriptURL: string, options?: RegistrationOptions): Promise<ServiceWorkerRegistration>;
    
    // getRegistration 获取 ServiceWorkerRegistration
    getRegistration(clientURL?: string): Promise<ServiceWorkerRegistration | undefined>;
    
    // getRegistrations 获取所有的 ServiceWorkerRegistration
    getRegistrations(): Promise<ServiceWorkerRegistration[]>;
    
    // startMessages 启动 ServiceWorker 的消息监听
    startMessages(): void;
    
    // oncontrollerchange 当 ServiceWorker 的 controller 发生变化时触发
    oncontrollerchange: ((this: ServiceWorkerContainer, ev: Event) => any) | null;
    // onmessage 当 ServiceWorker 发送消息时触发
    onmessage: ((this: ServiceWorkerContainer, ev: MessageEvent) => any) | null;
    // onmessageerror 当 ServiceWorker 发送消息时发生错误时触发
    onmessageerror: ((this: ServiceWorkerContainer, ev: MessageEvent) => any) | null;
}

看到上面的这些属性和方法,发现之前基本上都是使用过的,而且有之前的基础也都很好理解,不理解的可以自己动手去试试看,这里就不多讲解了。

这里不认识的也就剩controlleroncontrollerchange了,其实这两个属性都是用来表示当前的ServiceWorker对象的,controllerServiceWorker对象,oncontrollerchangeServiceWorker对象的事件,用来表示ServiceWorker对象的状态发生变化时触发。

回到文章最开始客户端和服务端通讯的示例,之前使用的是navigator.serviceWorker.ready,这个会返回一个Promise,这个Promiseresolve的参数是ServiceWorkerRegistration对象;

然后使用ServiceWorkerRegistration对象的active属性获取ServiceWorker对象,然后使用ServiceWorker对象的postMessage方法发送消息;

现在完全可以使用navigator.serviceWorker.controller获取ServiceWorker对象,然后使用ServiceWorker对象的postMessage方法发送消息,这样就方便很多了。

  • main.js 修改如下:
navigator.serviceWorker.register('./sw.js').then(registration => {
    console.log(registration);
});

navigator.serviceWorker.controller.postMessage('Hello from client');

navigator.serviceWorker.onmessage = event => {
    console.log('收到来自服务端发送的消息:', event.data);
};

这样就可以直接使用SeviceWorker对象的postMessage方法发送消息了;

当然也是需要注意SeviceWorker对象的状态,只有在active状态才能发送消息,所以ready的还是靠谱一些的;

不过也可以使用await来处理一下readyPromise,这里就不做演示了。

总结

这篇从ServiceWorkerGlobalScope开始,详细的认识了ServiceWorkerGlobalScope和一些关联的对象和事件;

然后从ServiceWorkerGlobalScope讲到了ServiceWorker对象,发现ServiceWorker对象并不是注册ServiceWorker时使用的,引出了ServiceWorkerContainer对象;

ServiceWorkerContainer对象是navigator.serviceWorker,这个才是真实的注册ServiceWorker时使用的对象;

最后讲到了ServiceWorkerContainer对象的一些属性和方法,以及ServiceWorker对象的一些属性和方法,发现ServiceWorker对象的postMessage方法可以直接使用,不需要再通过ServiceWorkerRegistration对象来获取ServiceWorker对象了;

到这里才算是真正的认识了ServiceWorkerServiceWorker是前三个Worker中最复杂的一个,也是最重要的一个,因为它可以实现客户端和服务端的通讯,这也是ServiceWorker的最大的作用。

这里只是起点,并不是终点,Worker中还有很多API,例如之前讲到的Push APINotification APICache API等等,这些都是ServiceWorker的重要组成部分,所以这只是一个开始。

历史章节和预告

结语

目前Web Worker系列文章已经其实已经进入了尾声,这里也是原理的最后一篇了,后续的安排暂时还没有,目前的计划是:

  1. 继续深入Web Worker相关的API,例如Push APINotification APICache API等等;
  2. 找一些Web Worker的实际应用,来写一下案例;

目前就想到这两个方面,但是今年还有一个计划就是准备弄一个源码阅读的系列,准备从Vue3源码开始;

这个计划也是我考虑很久的一个计划,因为Web Worker系列其实我也没有真正的在实际项目中使用过,所以也没有什么实际的应用;

虽然文章的案例代码都是我亲自写的,也都运行通过,但是并没有实际项目经验和真实案例的支持,只是自己的一个兴趣使然;

所以准备着重的进攻一下自己的主要技术栈,也就是Vue,从源码开始,那么这个系列肯定就是往后排了,甚至可能直接结束;

当然以项目这个系列来说,其实原理也都深入过了,但是也没有那么深,对于使用肯定是没问题的,但是对于深度使用这些肯定是不够看的;

自己也是没有实战相关的经验,所以这一个系列只是教学的角度,所以没太深入,但是对于初学者来说,应该是足够的;

这里就不再多说了,如果有想继续看Web Worker系列的,可以在评论区留言,我会根据大家的反馈来决定是否继续写下去;

源码阅读相关的准备建一个微信群来共同学习,如果有兴趣的可以加我微信zz1034182321,备注源码阅读,后面我拉你入群;

这里不方便贴二维码,不过可以在我的年终总结工作 6 年从踌躇满志到一事无成,感叹自己还在卷这篇文章的评论区找到我的微信二维码;

不要加群,因为那个是官方的微信群,所以是过审了的,那个群目前已经废弃了,所以不要加;