Capacitor 5 iOS端使用Interval导致重复提交问题排查
iOS端离线野火报告重复提交问题排查求助
我开发了一款基于Capacitor 5与Angular的野火报告应用,核心功能:
- 用户网络不佳时提交的报告会被离线存储
- 应用通过RxJS interval结合Capawesome Background Task API,每3分钟检测网络状态,在线时自动提交离线报告
目前浏览器及Android端通过本地存储submissionID的重复提交校验正常,但iOS端校验失效,出现相同submissionID的重复提交情况。
已完成的排查:
- 移除Capawesome后台任务,仅前台运行interval仍会出现重复提交
- 尝试过Ionic Storage、localStorage、Capacitor Preferences多种存储方式,问题未解决
- 使用setInterval与RxJS interval均存在相同问题
请求协助排查:是代码逻辑问题,还是Capacitor的iOS特定Bug?
相关代码
rof-title-page.component.ts
ngOnInit(): void { if (this.reportOfFirePage.currentPage.instance.id === 'first-page') { App.removeAllListeners(); // run background task (async () => { await this.backgroundListener(); })(); } } async backgroundListener() { App.addListener('appStateChange', async ({ isActive }) => { if (isActive) { return; } // The app state has been changed to inactive. // Start the background task by calling `beforeExit`. const taskId = await BackgroundTask.beforeExit(async () => { const self = this; if(!this.intervalRef) { this.intervalRef = interval(30000).subscribe(() => { self.checkStoredRoF(); }); } BackgroundTask.finish({ taskId }); }); }); } async checkStoredRoF() { const self = this; // first check do 24 hour check in storage and remove offline RoF if timeframe has elapsed await this.commonUtilityService.removeInvalidOfflineRoF(); // check if the app is online and if so, check for saved offline RoF to be submitted await this.commonUtilityService.checkOnlineStatus().then(async (result) => { if (result) { await this.reportOfFireService.syncDataWithServer().then(response => { if(response) self?.intervalRef?.unsubscribe(); }); }; }); }
report-of-fire-service.ts
async syncDataWithServer() { let dataSynced = false; let submissionID = null; let duplicateStored = false; let submissionIdList = null; let offlineReport = null; try { // Fetch and submit locally stored data offlineReport = this.storageService.getData('offlineReportData') submissionIdList = this.storageService.getData('submissionIDList') if (offlineReport) { // Check for duplicate, reject if submissionID has already been stored const offlineJson = JSON.parse(offlineReport) if(offlineJson?.resource) { const resourceJson = JSON.parse(offlineJson.resource) submissionID = resourceJson?.submissionID if (submissionID && submissionIdList?.includes(submissionID)) { duplicateStored = true; } } // Reject duplicate if submissionID has already been stored if(duplicateStored) return true; // Send the report to the server const response = await this.submitOfflineReportToServer(offlineReport).then(async response => { if (response.success) { dataSynced = true; // Remove the locally stored data if sync is successful this.storageService.removeData('offlineReportData'); // store submissionID for duplicate check if(submissionID) { submissionIdList = submissionIdList ? submissionIdList + ", " + submissionID : submissionID; this.storageService.saveData('submissionIDList', submissionIdList) } App.removeAllListeners(); } }); } } catch (error) { console.error('Sync failed:', error); } return dataSynced; } async submitOfflineReportToServer(offlineReport?): Promise<any> { // retrieve the offline RoF from the device's storage and convert to FormData for submission // images will already to converted to base64 string from initial submission const rofUrl = this.appConfigService.getConfig().rest['fire-report-api']; const rofJson = JSON.parse(offlineReport); const resource = rofJson.resource; const image1 = rofJson.image1; const image2 = rofJson.image2; const image3 = rofJson.image3; const formData = new FormData(); if (resource) { formData.append('resource', resource); } if (image1) { formData.append('image1', image1); } if (image2) { formData.append('image2', image2); } if (image3) { formData.append('image3', image3); } try { // Make an HTTP POST request to your server's API endpoint const response = await fetch(rofUrl, { method: 'POST', body: formData, }); if (response.ok || response.status == 200) { // Remove the locally stored data if sync is successful this.storageService.removeData('offlineReportData'); App.removeAllListeners(); // The server successfully processed the report return { success: true, message: 'Report submitted successfully' }; } else { // The server encountered an error return { success: false, message: JSON.stringify(response) }; } } catch (error) { // An error occurred during the HTTP request return { success: false, message: 'An error occurred while submitting the report', }; } } } async submitToStorage(formData: FormData) { const object = {}; formData.forEach((value, key) => (object[key] = value)); const json = JSON.stringify(object); const data = this.storageService.getData('offlineReportData') if (data == json) { return; } else this.storageService.saveData('offlineReportData', json); }
local-storage-service.ts
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class LocalStorageService { public saveData(key: string, value: string) { localStorage.setItem(key, value); } public getData(key: string) { return localStorage.getItem(key); } public removeData(key: string) { localStorage.removeItem(key); } public clearData() { localStorage.clear(); } }
可能的问题点及修复建议
1. 存储操作的异步竞态问题
iOS WebView的localStorage可能存在读写延迟,建议改用Capacitor Preferences的异步API,并确保读取操作等待完成,避免读取到旧数据:
// 修改存储读取逻辑为异步 offlineReport = await Preferences.get({ key: 'offlineReportData' }).then(res => res.value); submissionIdList = await Preferences.get({ key: 'submissionIDList' }).then(res => res.value);
2. Interval订阅重复创建
多次触发appStateChange事件时,可能创建多个interval订阅,导致同时触发多次提交。创建前先取消已有订阅:
// 在创建新interval前取消旧订阅 if(this.intervalRef) { this.intervalRef.unsubscribe(); } this.intervalRef = interval(30000).subscribe(() => { self.checkStoredRoF(); });
3. 重复校验逻辑漏洞
用逗号分隔字符串存储submissionID可能因特殊字符导致误判,改用数组JSON格式存储更可靠:
// 读取时解析为数组 const submissionIds = submissionIdList ? JSON.parse(submissionIdList) : []; if(submissionIds.includes(submissionID)) { duplicateStored = true; } // 保存时更新数组 submissionIds.push(submissionID); this.storageService.saveData('submissionIDList', JSON.stringify(submissionIds));
4. iOS后台任务执行限制
iOS对BackgroundTask.beforeExit的执行时间有严格限制,在其中创建interval无法持续运行。建议改用Capacitor BackgroundFetch插件实现定期后台同步,符合iOS后台任务规范。
5. 提交成功后的存储操作重复
submitOfflineReportToServer和syncDataWithServer中都调用了removeData('offlineReportData'),可能导致状态不一致,建议只在syncDataWithServer中统一处理存储更新。
内容的提问来源于stack exchange,提问作者user24471980
相关产品推荐
相关产品推荐

