You Don't Know Service Worker——跨页签操作

234 阅读3分钟

1、Service Worker简介

Service Worker 是一个运行在后台的 Worker 线程,然后它会长期运行,充当一个服务bulabulabula...

很多提到 Service Worker 的内容,都会将其作为处理缓存、处理离线问题的工具。

但因为其独立于浏览器页签执行,我们其实也可以利用它来统领各浏览器页签,使我们的页面支持一些需要跨页签的能力!

2、为何我需要跨页签

我们来想象一个场景,假设我们的网站需要弹出一个桌面的通知,这个我们可以通过 Notification 来弹出:

new Notification('tips', {
    body: 'hello notification',
});

效果如下:

image.png

我们桌面的右下角出现了一个消息提醒,但只是提醒似乎并不足够。通常这类消息提醒是要可以通过点击,进入到消息来源的页面的。

但简单粗暴的重置当前页签的页面,视乎并不太友好,会打断用户进行中的一些操作!

那,新开页签怎么样呢?

似乎也不太行,这会让用户在使用的过程中,不断的产生新页签!

那能不能在弹出新页签之前判断一下,假设我们点击消息提醒是需要跳转页面a的,就判断下是否存在一个打开着页面a的页签,如果存在,就定位到该页签,如果不存在,就新开页签。

这样就不会打断用户在其他页签的操作,也不会不断的产生新的页签,但这就需要跨页签的能力

3、跨页签能力初体验

我们先来看下页面内的代码:

<!-- 通知的跨页签操作 -->
<button id="showNotification">弹出消息通知</button>

<script>
// 启动service worker线程
navigator.serviceWorker
  .register(
	'http://localhost:8080/src/worker-space-1/worker-1-1.js',
	{}
  )
  
document
	.getElementById('showNotification')
	.addEventListener('click', () => {
	  // 因为一个网站可以启动多个service worker线程,我们需要找到我们用来处理消息通知的那个
	  navigator.serviceWorker.getRegistrations().then(workerList => {
		const worker = workerList.find(
		  w =>
			w.active.scriptURL ===
			'http://localhost:8080/src/worker-space-1/worker-1-1.js'
		);
		// 这里想通过service worker来弹出消息通知,就需要用这种方式
		worker.showNotification('Hello World');
	  });
});
</script>

然后我们再来看看给service worker执行的js代码

self.addEventListener('notificationclick', function (event) {
  // 关闭消息通知
  event.notification.close();

  event.waitUntil(
    // 获取浏览器相同域名下的所有页签!
    self.clients
      .matchAll({
        includeUncontrolled: true,
        type: 'window',
      })
      .then(function (clientList) {
        for (var i = 0; i < clientList.length; i++) {
          var client = clientList[i];
		   // 寻找指定页签,如果有,就打开它
		  // 这里我们用是否携带targetClient参数,来区分哪个是我们消息通知需要打开的页签
          if (client.url.includes('targetClient') && 'focus' in client)
            return client.focus();
        }
		// 如果没有,就打开新页签!
        if (self.clients.openWindow)
          return self.clients.openWindow(
            'http://localhost:8080?targetClient=1'
          );
      })
  );
});

试试看效果:

20240814_164723 -original-original.gif

4、跨页签的通讯

service worker 还支持与页签互相通讯:

/* --------------------页签文件内-------------- */
// 页签接受service worker的消息
navigator.serviceWorker.addEventListener('message', e => {
    // ...
});
// 页签给service worker发送消息
navigator.serviceWorker.getRegistrations().then(workerList => {
  const worker = workerList.find(
    w =>
      w.active.scriptURL ===
      'http://localhost:8080/src/worker-space-1/worker-1-1.js'
  );
  worker.active.postMessage({ type: 'chat', value });
});

/* --------------------service worker文件内-------------- */
// service worker接收页签消息
self.addEventListener('message', function (event) {
    // 获取所有页签
    self.clients
      .matchAll({
            includeUncontrolled: true,
            type: 'window',
      })
      .then(function (clientList) {
            for (var i = 0; i < clientList.length; i++) {
              var client = clientList[i];
              if (event.source.id === client.id) continue;
              console.log(client);
              // service worker给页签发消息
              client.postMessage({ ...event.data, sourceId: event.source.id });
            }
      })
    );
});

所以,我们其实可以利用service worker做一个页签间的聊天室!虽然这并没什么实际用处(不是

892a8a5e-790c-4d9c-b049-643024da3991.gif

5、Service Worker的一些机制

5.1、事件驱动

service worker 是事件驱动的,只能通过监听事件,来对页签的相关操作做响应。

// 通讯事件
self.addEventListener('message', function (event) {
});

// 消息通知点击事件
self.addEventListener('notificationclick', function (event) {
});

5.2、作用域与更新

service worker 是用文件路径来做命名空间的:

image.png

对于相同路径的不同文件,service worker 会将其当作同一个 service worker 对其进行更新,而不是新建一个 worker 线程:

image.png

这就避免我们开多个页签时,多次执行相同 service worker 代码后,不至于启动多个 worker 线程

// 每个页签都会执行这段代码,但只会有一个 worker 线程被启用
navigator.serviceWorker
	.register(
	'http://localhost:8080/src/worker-space-1/worker-1-1.js',
	{}
)