首页 > 解决方案 > 使用 Service Worker 管理服务器端事件

问题描述

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

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

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

我尝试重组以使用私有网络工作者来管理 sse 链接,在网络工作者和主 javascript 线程之间集中消息 - 再次几乎可以正常工作(参见上述存储库的工作者分支)。但这也被关闭了!

我最后的想法是使用服务工作者,但如何构建应用程序?

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

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

方法一

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

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

方法二

在服务工作者中有一组幻像 url,它们模拟所有不同的消息类型(和参数),这些消息类型(和参数)之前将我的选项卡发送到其私有 Web 工作者。

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;

    });
  }
});

fetch 事件的顶部只是直接传递客户端发出的标准 api 请求。我不能缓存这些(尽管我可以更复杂,也许预先拒绝那些不支持的)。第二部分匹配幻像网址/service/something

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

方法 3

与上面的方法类似,我们将有幻像 url 并使用 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 中移至后台时是否会关闭)

标签: service-workerserver-sent-events

解决方案


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

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


推荐阅读