React.ts PWA生成APK后,设备休眠时保持WebSocket连接
问题描述
我正在开发一个React.ts PWA项目,通过Bubblewrap库生成APK适配Android设备。项目已集成SignalR,收到HUB更新时会更新Redux数据并发送通知,但设备进入休眠模式时WebSocket连接会断开,需要实现持续实时通信并能通知用户。
export const SignalRWrapper = ({ children }: Props) => { const dispatch = useAppDispatch(); const userRedux = useAppSelector((state) => state.user); useEffect(() => { if(Notification.permission === 'default') { Notification.requestPermission(); } SignalRConnection.on("tatus", (planningReadDto: PlanningReadDto) => { dispatch(updateBooking(planning)); if(Notification.permission === 'granted' && userRedux.user && isMyTruck(planningReadDto.currentStatus, userRedux.user?.habilitations)) { showNotification(planning.name, planningReadDto.currentStatus); } }); SignalRConnection.onclose(()=>{ SignalRConnection.invoke("KeepAlive"); }) setInterval(() => { SignalRConnection.invoke("KeepAlive"); }, 3000); }, [userRedux.user]); function showNotification(name: string, currentStatus : TruckStatus) { navigator.serviceWorker.ready.then((registration) => { registration.showNotification(`${name} : Nouveau statut`,{ body: `${name} passe au statut ${getNameStatus[currentStatus]}` }) }) }
解决方案
1. 修复SignalR重连逻辑
现有onclose回调直接调用invoke("KeepAlive")无效——连接断开后无法执行invoke操作,必须重新发起连接请求。改用指数退避重连策略,避免频繁重试耗尽系统资源:
SignalRConnection.onclose(async (error) => { console.log('SignalR连接断开,启动重连:', error); let retryDelay = 1000; while (true) { try { await SignalRConnection.start(); console.log('SignalR重连成功'); break; } catch (retryError) { console.error('重连失败,等待重试:', retryError); await new Promise(resolve => setTimeout(resolve, retryDelay)); retryDelay = Math.min(retryDelay * 2, 30000); // 最大延迟限制为30秒 } } });
同时移除手动设置的3秒KeepAlive定时器——SignalR自身内置心跳机制(默认30秒一次),重复调用会造成冗余请求。
2. 利用Android后台能力维持连接
设备休眠时,系统会切断普通WebSocket连接,需结合PWA后台同步和Android权限实现唤醒:
- 修改Bubblewrap的
twa-manifest.json,添加必要权限:"permissions": [ "WAKE_LOCK", "ACCESS_NETWORK_STATE" ] - 在Service Worker中注册后台同步事件,定期检查并恢复SignalR连接:
self.addEventListener('sync', (event) => { if (event.tag === 'signalr-reconnect') { event.waitUntil( new Promise(async (resolve) => { try { // 重新初始化或启动SignalR连接 await signalRConnection.start(); resolve(); } catch (e) { // 重连失败,交由下次同步任务处理 resolve(); } }) ); } }); - 前端在连接断开时触发后台同步请求:
if ('serviceWorker' in navigator && 'SyncManager' in window) { navigator.serviceWorker.ready.then(reg => { reg.sync.register('signalr-reconnect'); }); }
3. 改用FCM推送实现可靠通知
设备休眠时WebSocket无法稳定维持,最可靠的方案是替换SignalR实时推送为Firebase Cloud Messaging(FCM):
- 后端不再通过SignalR推送,而是在事件触发时向FCM服务器发送消息
- Service Worker监听FCM推送事件,展示通知并同步更新前端Redux:
self.addEventListener('push', (event) => { const data = event.data?.json() || {}; const notificationOptions = { body: data.body, icon: '/icons/icon-192x192.png', requireInteraction: true // 通知保持至用户手动关闭 }; event.waitUntil( self.registration.showNotification(data.title, notificationOptions) ); // 向前端发送消息更新Redux self.clients.matchAll({ type: 'window' }).then(clients => { clients.forEach(client => { client.postMessage({ type: 'UPDATE_BOOKING', payload: data.planning }); }); }); }); - 前端监听Service Worker消息,更新Redux状态:
useEffect(() => { const handleServiceWorkerMessage = (event: MessageEvent) => { if (event.data.type === 'UPDATE_BOOKING') { dispatch(updateBooking(event.data.payload)); } }; navigator.serviceWorker.addEventListener('message', handleServiceWorkerMessage); return () => { navigator.serviceWorker.removeEventListener('message', handleServiceWorkerMessage); }; }, [dispatch]);
4. 优化通知权限与展示逻辑
完善通知权限请求流程,确保授权后通知能正常触发:
useEffect(() => { const requestNotificationPermission = async () => { if (Notification.permission === 'default') { const permission = await Notification.requestPermission(); if (permission !== 'granted') { console.warn('用户拒绝通知权限,将无法接收实时提醒'); } } }; requestNotificationPermission(); }, []);
同时优化通知展示函数,增加错误捕获:
async function showNotification(name: string, currentStatus: TruckStatus) { try { const registration = await navigator.serviceWorker.ready; await registration.showNotification(`${name} : Nouveau statut`, { body: `${name} passe au statut ${getNameStatus[currentStatus]}`, requireInteraction: true, }); } catch (error) { console.error('通知展示失败:', error); } }
内容的提问来源于stack exchange,提问作者Tarik
相关产品推荐
相关产品推荐

