【发布时间】:2020-04-05 10:48:34
【问题描述】:
我正在构建一个 Web 应用程序以显示在我的 iPad 上,以控制我的树莓派充当录音机。部分需要是保持事件源打开,以便服务器可以发送服务器端事件。应用程序的特定实例可以控制录制过程,但如果服务器看到 sse 链接关闭,则会失去控制。这只是防止客户端消失并保持控制(进程控制确实需要至少每 5 分钟更新一次 - 但在正常情况下我真的不想等那么久,因为有人只是关闭浏览器选项卡。)
我的部分需要是将浏览器推到后台,这样我就可以打开相机并录制视频。
我构建了这个应用程序并让它几乎可以工作,请参阅https://github.com/akc42/pi_record.git(主分支)。
直到我把浏览器推到后台,发现IOS关闭了页面,破坏了sse链接。
我尝试重组以使用私有网络工作者来管理 sse 链接,在网络工作者和主 javascript 线程之间集中消息 - 再次几乎可以正常工作(参见上述存储库的工作者分支)。但这也被关闭了!
我最后的想法是使用 service worker,但是如何构建应用程序?
显然,Service Worker 必须充当服务器端事件的客户端。它必须保持连接打开,但它还需要跟踪浏览器中的多个选项卡,这些选项卡可能会或可能不会尝试控制界面,并且只允许一个选项卡这样做。
我可以想到三种方法 - 但很难看出哪种方法更好。至少我什至从未见过下面提到的方法 2 和 3,但在我看来,这两种方法中的一种实际上可能是最简单的。
方法 1
将我现在为单独的 Web 工作者提供的代码移动到服务工作者中。但是,我们需要在窗口和服务之间传递某种形式的 ID 添加到消息中。所以我可以记录哪个选项卡实际控制了界面,从而排除其他选项卡这样做(即模拟失败的控制尝试)。
据我所知,MessageEvent.ports[0] 可能是一个独特的对象,我可以将其存储在某处的 Map 中,但我并不完全相信如果浏览器移至后台,MessageChannel 不会关闭.
方法 2
在 Service Worker 中有一组幻像 url,它们模拟所有不同的消息类型(和参数),这些消息类型(和参数)之前将我的选项卡发送到其私有 Web Worker。
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-worker server-sent-events