使用 Service Worker 管理服务器端事件

Managing Server Side Events with a Service Worker

我正在构建一个网络应用程序以显示在我的 iPad 上以控制我的 raspberry pi 作为录音机。部分需要是保持事件源打开,以便服务器可以发送服务器端事件。该应用程序的特定实例可以控制录制过程,但如果服务器发现 sse link 关闭,则会失去控制。这只是为了防止客户端消失并保留控制权(对进程的控制确实需要至少每 5 分钟更新一次 - 但我真的不想在正常情况下等待那么久,因为有人只是关闭浏览器标签。)

我的部分需求是将浏览器推送到后台,这样我就可以打开相机并录制视频。

我构建了这个应用程序并且几乎可以正常工作,请参阅 https://github.com/akc42/pi_record.git(主分支)。
直到我把浏览器推到后台,发现IOS关闭了页面,把sse弄坏了link。

我尝试重组以使用私有 Web Worker 来管理 sse link,在 Web Worker 和主 javascript 线程之间聚集消息 - 再次几乎可以正常工作(请参阅上述存储库的 workers 分支).但它也被关闭了!

我最后的想法是使用 service worker,但是如何构建应用程序?

很明显,service worker 必须充当服务器端事件的服务器客户端。它必须保持连接打开,但它还需要跟踪浏览器中的多个选项卡,这些选项卡可能会或可能不会尝试获取界面的控制权,并且只允许一个选项卡这样做。

我可以想到三种方法 - 但很难看出哪种更好。至少我什至从未在下面看到任何提及方法 2 和 3 的内容,但在我看来,这两种方法中的一种实际上可能是最简单的。

方法一

将我现在为单独的网络工作者编写的代码移到服务工作者中。然而,我们需要在 window 和服务之间添加某种形式的 ID 传递到消息中。所以我可以记录哪个选项卡实际控制了界面,因此排除了其他选项卡(即模拟失败的控制尝试)。

据我所知,MessageEvent.ports[0] 可能是一个独特的对象,我可以将其存储在某个地方的 Map 中,但我并不完全相信如果浏览器 MessageChannel 不会关闭移到后台。

方法二

在服务工作者中有一组幻影 urls 模拟所有不同的消息类型(和参数),这些消息类型(和参数)以前将我的选项卡发送到它的私有网络工作者。

fetch 事件提供了一个 clientid(我可以用它来区分谁实际抓住了控制权)然后我可以用它来做 Clients.get(clientid).postMessage()(或者 Clients.matchAll 当需要广播响应时)

代码类似于

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

  const requestURL = new URL(event.request.url);
  if (/^\/api\//.test(requestURL.pathname)) {
    event.respondWith(fetch(event.request));  //all api requests are a direct pass through
  } else if (/^\/service\//.test(requestURL.pathname)) {
    /*
      process these like a message passing with one extra to say the client is going away.

    */
    if (urlRecognised) {
      event.respondWith(new Response('OK', {status: 200}));
    } else {
      event.respondWith(new Response(`Unknown request ${requestURL.pathname}`, {status: 404}));
    }
  } else {
    event.respondWith(async () => {
      const cache = await caches.open('recorder');
      const cachedResponse = await cache.match(event.request);
      const networkResponsePromise = fetch(event.request);

      event.waitUntil(async () => {
        const networkResponse = await networkResponsePromise;
        await cache.put(event.request, networkResponse.clone());
      });

      // Returned the cached response if we have one, otherwise return the network response.
      return cachedResponse || networkResponsePromise;

    });
  }
});

获取事件的顶部直接传递客户端发出的标准 api 请求。我无法缓存这些(尽管我可以更复杂,并且可能会预先拒绝那些不受支持的)。 第二段匹配phantom urls /service/something

最后一节取自 Jake Archibald 的离线食谱并尝试使用缓存,但如果任何静态文件发生更改,则会在后台更新缓存。

方法 3

类似于上面的方法,因为我们会有幻影 urls 并使用 clientid 作为唯一标记,但实际上尝试用一个 url 模拟服务器端事件流。

我认为代码更像

...
 } else if (/^\/service\//.test(requestURL.pathname)) {
   const stream = new TransformStream();
   const writer = stream.writeable.getWriter();
   event.respondWith(async () => {
    const streamFinishedPromise = new Promise(async (resolve,reject) => {
      event.waitUntil(async () => {
        /* eventually close the link  */
        await streamFinishedPromise;
      });
      try {
        while (true) writer.write(await nextMessageFromServerSideEventStream());
      } catch(e) {
        writer.close();
        resolve();
      }
    });
    return new Response(stream.readable,{status:200}) //probably need eventstream headers too 

 }

我认为 方法 2 可能是最简单的方法,考虑到我现在所在的位置,但我担心在搜索如何使用讨论的服务工作者时我什么也看不到这个幻影 url 方法。

任何人都可以评论这些方法中的任何一种并提供有关如何最好地编程棘手位的指导(例如,当浏览器在 iPad 上移至后台时方法 1 消息通道是否关闭,或者在方法 3)

中,您如何真正保持响应通道打开,以及当浏览器移至后台时关闭该通道?

简单的事实是 none 这些方法都行得通。当我问这个问题时,我没有意识到的是,只要有事情要做,服务工作者就会被浏览器重新 运行 并且 运行 只持续一个事件的处理。尽管 eventWaitUntil 可以延长它,但我能找到多长时间的唯一参考是浏览器仍然可以自由取消它,如果它看起来可能永远不会关闭。我无法想象在几个小时内它不会被取消。因此,事件源将关闭并有效终止其对服务器的 link。

所以我实现我想要的唯一选择是让服务器在事件源关闭时继续运行,并找到一些其他机制来释放代表客户端持有的资源