From a16dd497c41b571281223bafb0bc40250385e570 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 6 Jul 2025 06:34:25 +0000 Subject: [PATCH 1/4] Add comprehensive PWA state management solution for iOS background handling Co-authored-by: jxxghp --- PWA_iOS_Background_Solutions.md | 645 ++++++++++++++++++++++++++++++++ 1 file changed, 645 insertions(+) create mode 100644 PWA_iOS_Background_Solutions.md diff --git a/PWA_iOS_Background_Solutions.md b/PWA_iOS_Background_Solutions.md new file mode 100644 index 00000000..96426bec --- /dev/null +++ b/PWA_iOS_Background_Solutions.md @@ -0,0 +1,645 @@ +# PWA在iOS上防止后台被杀及状态恢复解决方案 + +## 问题概述 + +PWA添加到iOS桌面后,经常遇到以下问题: +- iOS系统积极清理后台应用内存 +- 重新打开PWA时页面刷新,丢失之前状态 +- 用户体验不佳,类似于"冷启动" + +## 核心解决策略 + +### 1. 实现状态持久化 + +#### 使用多层存储策略 +```javascript +// 状态管理类 +class PWAStateManager { + constructor() { + this.storageKey = 'pwa-app-state'; + this.sessionKey = 'pwa-session-state'; + } + + // 保存应用状态 + saveState(state) { + try { + // 主要状态存储到localStorage + localStorage.setItem(this.storageKey, JSON.stringify({ + ...state, + timestamp: Date.now() + })); + + // 临时状态存储到sessionStorage + sessionStorage.setItem(this.sessionKey, JSON.stringify({ + scrollPosition: window.scrollY, + activeTab: state.activeTab, + formData: state.formData + })); + } catch (error) { + console.error('状态保存失败:', error); + } + } + + // 恢复应用状态 + restoreState() { + try { + const savedState = localStorage.getItem(this.storageKey); + const sessionState = sessionStorage.getItem(this.sessionKey); + + if (savedState) { + const state = JSON.parse(savedState); + const sessionData = sessionState ? JSON.parse(sessionState) : {}; + + return { + ...state, + ...sessionData, + isRestored: true + }; + } + } catch (error) { + console.error('状态恢复失败:', error); + } + return null; + } + + // 清除过期状态 + clearExpiredState(maxAge = 24 * 60 * 60 * 1000) { // 24小时 + try { + const savedState = localStorage.getItem(this.storageKey); + if (savedState) { + const state = JSON.parse(savedState); + if (Date.now() - state.timestamp > maxAge) { + localStorage.removeItem(this.storageKey); + sessionStorage.removeItem(this.sessionKey); + } + } + } catch (error) { + console.error('清除过期状态失败:', error); + } + } +} +``` + +#### 使用IndexedDB存储大量数据 +```javascript +// IndexedDB状态管理 +class PWAIndexedDBManager { + constructor() { + this.dbName = 'PWAStateDB'; + this.dbVersion = 1; + this.storeName = 'appState'; + } + + async initDB() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.dbName, this.dbVersion); + + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + + request.onupgradeneeded = (event) => { + const db = event.target.result; + if (!db.objectStoreNames.contains(this.storeName)) { + db.createObjectStore(this.storeName, { keyPath: 'id' }); + } + }; + }); + } + + async saveState(state) { + try { + const db = await this.initDB(); + const transaction = db.transaction([this.storeName], 'readwrite'); + const store = transaction.objectStore(this.storeName); + + await store.put({ + id: 'appState', + data: state, + timestamp: Date.now() + }); + } catch (error) { + console.error('IndexedDB保存失败:', error); + } + } + + async restoreState() { + try { + const db = await this.initDB(); + const transaction = db.transaction([this.storeName], 'readonly'); + const store = transaction.objectStore(this.storeName); + + return new Promise((resolve, reject) => { + const request = store.get('appState'); + request.onsuccess = () => { + const result = request.result; + resolve(result ? result.data : null); + }; + request.onerror = () => reject(request.error); + }); + } catch (error) { + console.error('IndexedDB恢复失败:', error); + return null; + } + } +} +``` + +### 2. 使用Service Worker实现状态共享 + +#### Service Worker状态管理 +```javascript +// sw.js - Service Worker中的状态管理 +const STATE_CACHE_NAME = 'pwa-state-cache'; +const STATE_ENDPOINT = '/api/state'; + +// 激活时立即接管页面 +self.addEventListener('activate', event => { + event.waitUntil(clients.claim()); +}); + +// 拦截状态相关请求 +self.addEventListener('fetch', event => { + const { request } = event; + const url = new URL(request.url); + + if (url.pathname === STATE_ENDPOINT) { + if (request.method === 'POST') { + event.respondWith(saveStateToCache(request)); + } else if (request.method === 'GET') { + event.respondWith(getStateFromCache()); + } + } +}); + +// 保存状态到缓存 +async function saveStateToCache(request) { + try { + const state = await request.json(); + const cache = await caches.open(STATE_CACHE_NAME); + + await cache.put(STATE_ENDPOINT, new Response(JSON.stringify({ + ...state, + timestamp: Date.now() + }))); + + return new Response(JSON.stringify({ success: true })); + } catch (error) { + return new Response(JSON.stringify({ error: error.message }), { + status: 500 + }); + } +} + +// 从缓存获取状态 +async function getStateFromCache() { + try { + const cache = await caches.open(STATE_CACHE_NAME); + const response = await cache.match(STATE_ENDPOINT); + + if (response) { + const state = await response.json(); + return new Response(JSON.stringify(state)); + } + + return new Response(JSON.stringify({})); + } catch (error) { + return new Response(JSON.stringify({ error: error.message }), { + status: 500 + }); + } +} +``` + +#### 客户端状态同步 +```javascript +// 客户端状态同步 +class ServiceWorkerStateSync { + constructor() { + this.stateEndpoint = '/api/state'; + } + + async saveState(state) { + try { + const response = await fetch(this.stateEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(state) + }); + + return await response.json(); + } catch (error) { + console.error('Service Worker状态保存失败:', error); + } + } + + async loadState() { + try { + const response = await fetch(this.stateEndpoint); + return await response.json(); + } catch (error) { + console.error('Service Worker状态加载失败:', error); + return {}; + } + } +} +``` + +### 3. 监听应用生命周期 + +#### 页面可见性监听 +```javascript +// 页面可见性状态管理 +class VisibilityStateManager { + constructor(stateManager) { + this.stateManager = stateManager; + this.setupVisibilityListener(); + } + + setupVisibilityListener() { + // 监听页面可见性变化 + document.addEventListener('visibilitychange', () => { + if (document.hidden) { + // 页面被隐藏时保存状态 + this.handlePageHidden(); + } else { + // 页面显示时恢复状态 + this.handlePageVisible(); + } + }); + + // 监听页面卸载 + window.addEventListener('beforeunload', () => { + this.handlePageUnload(); + }); + + // 监听页面焦点变化 + window.addEventListener('blur', () => { + this.handlePageBlur(); + }); + + window.addEventListener('focus', () => { + this.handlePageFocus(); + }); + } + + handlePageHidden() { + // 页面被隐藏时保存当前状态 + const currentState = this.getCurrentAppState(); + this.stateManager.saveState(currentState); + + console.log('页面被隐藏,已保存状态'); + } + + handlePageVisible() { + // 页面显示时检查是否需要恢复状态 + const restoredState = this.stateManager.restoreState(); + if (restoredState) { + this.restoreAppState(restoredState); + console.log('页面显示,已恢复状态'); + } + } + + handlePageUnload() { + // 页面卸载时最后保存状态 + const currentState = this.getCurrentAppState(); + this.stateManager.saveState(currentState); + } + + handlePageBlur() { + // 失去焦点时保存状态(定时器避免频繁保存) + if (this.blurTimer) clearTimeout(this.blurTimer); + this.blurTimer = setTimeout(() => { + const currentState = this.getCurrentAppState(); + this.stateManager.saveState(currentState); + }, 1000); + } + + handlePageFocus() { + // 获得焦点时清除定时器 + if (this.blurTimer) { + clearTimeout(this.blurTimer); + this.blurTimer = null; + } + } + + getCurrentAppState() { + // 获取当前应用状态(需要根据具体应用实现) + return { + url: window.location.href, + scrollPosition: window.scrollY, + timestamp: Date.now(), + // 添加其他应用特定状态 + }; + } + + restoreAppState(state) { + // 恢复应用状态(需要根据具体应用实现) + if (state.scrollPosition) { + window.scrollTo(0, state.scrollPosition); + } + // 恢复其他应用特定状态 + } +} +``` + +### 4. 实现智能状态恢复 + +#### 状态恢复决策器 +```javascript +class StateRestoreDecision { + constructor() { + this.maxStateAge = 30 * 60 * 1000; // 30分钟 + this.urlChangeThreshold = 5; // URL变化阈值 + } + + shouldRestoreState(savedState, currentContext) { + if (!savedState) return false; + + // 检查状态年龄 + if (this.isStateExpired(savedState)) { + return false; + } + + // 检查URL匹配 + if (!this.isUrlCompatible(savedState.url, currentContext.url)) { + return false; + } + + // 检查设备方向 + if (this.isOrientationChanged(savedState, currentContext)) { + return false; + } + + return true; + } + + isStateExpired(savedState) { + return Date.now() - savedState.timestamp > this.maxStateAge; + } + + isUrlCompatible(savedUrl, currentUrl) { + if (!savedUrl || !currentUrl) return false; + + const savedPath = new URL(savedUrl).pathname; + const currentPath = new URL(currentUrl).pathname; + + return savedPath === currentPath; + } + + isOrientationChanged(savedState, currentContext) { + return savedState.orientation !== currentContext.orientation; + } +} +``` + +### 5. 完整的应用状态管理器 + +#### 统一状态管理 +```javascript +// 完整的PWA状态管理器 +class PWAStateController { + constructor() { + this.stateManager = new PWAStateManager(); + this.indexedDBManager = new PWAIndexedDBManager(); + this.swStateSync = new ServiceWorkerStateSync(); + this.visibilityManager = new VisibilityStateManager(this.stateManager); + this.restoreDecision = new StateRestoreDecision(); + + this.init(); + } + + async init() { + // 清理过期状态 + this.stateManager.clearExpiredState(); + + // 检查是否需要恢复状态 + await this.checkAndRestoreState(); + + // 设置定期保存 + this.setupPeriodicSave(); + } + + async checkAndRestoreState() { + const currentContext = { + url: window.location.href, + orientation: window.orientation || 0, + timestamp: Date.now() + }; + + // 尝试从多个来源恢复状态 + const sources = [ + () => this.stateManager.restoreState(), + () => this.indexedDBManager.restoreState(), + () => this.swStateSync.loadState() + ]; + + for (const source of sources) { + try { + const savedState = await source(); + if (this.restoreDecision.shouldRestoreState(savedState, currentContext)) { + await this.restoreState(savedState); + return; + } + } catch (error) { + console.error('状态恢复失败:', error); + } + } + } + + async saveCurrentState() { + const state = { + url: window.location.href, + scrollPosition: window.scrollY, + orientation: window.orientation || 0, + timestamp: Date.now(), + // 添加应用特定状态 + appData: this.getAppSpecificState() + }; + + // 多重保存策略 + await Promise.allSettled([ + this.stateManager.saveState(state), + this.indexedDBManager.saveState(state), + this.swStateSync.saveState(state) + ]); + } + + async restoreState(state) { + // 恢复滚动位置 + if (state.scrollPosition) { + window.scrollTo(0, state.scrollPosition); + } + + // 恢复应用特定状态 + if (state.appData) { + this.restoreAppSpecificState(state.appData); + } + + // 触发状态恢复事件 + this.dispatchStateRestoreEvent(state); + } + + setupPeriodicSave() { + // 每30秒保存一次状态 + setInterval(() => { + if (!document.hidden) { + this.saveCurrentState(); + } + }, 30000); + } + + getAppSpecificState() { + // 根据具体应用实现 + return { + // 表单数据 + formData: this.getFormData(), + // 用户选择 + userSelections: this.getUserSelections(), + // 其他应用状态 + }; + } + + restoreAppSpecificState(appData) { + // 根据具体应用实现状态恢复 + if (appData.formData) { + this.restoreFormData(appData.formData); + } + if (appData.userSelections) { + this.restoreUserSelections(appData.userSelections); + } + } + + dispatchStateRestoreEvent(state) { + const event = new CustomEvent('stateRestored', { + detail: { state } + }); + window.dispatchEvent(event); + } + + getFormData() { + // 获取表单数据 + const forms = document.querySelectorAll('form'); + const formData = {}; + + forms.forEach((form, index) => { + const data = new FormData(form); + formData[`form-${index}`] = Object.fromEntries(data); + }); + + return formData; + } + + restoreFormData(formData) { + // 恢复表单数据 + Object.entries(formData).forEach(([formId, data]) => { + const formIndex = parseInt(formId.split('-')[1]); + const form = document.querySelectorAll('form')[formIndex]; + + if (form) { + Object.entries(data).forEach(([name, value]) => { + const input = form.querySelector(`[name="${name}"]`); + if (input) { + input.value = value; + } + }); + } + }); + } + + getUserSelections() { + // 获取用户选择状态 + return { + selectedItems: Array.from(document.querySelectorAll('.selected')).map(el => el.id), + activeTab: document.querySelector('.tab.active')?.id + }; + } + + restoreUserSelections(selections) { + // 恢复用户选择 + if (selections.selectedItems) { + selections.selectedItems.forEach(id => { + const element = document.getElementById(id); + if (element) { + element.classList.add('selected'); + } + }); + } + + if (selections.activeTab) { + const tab = document.getElementById(selections.activeTab); + if (tab) { + tab.classList.add('active'); + } + } + } +} +``` + +### 6. 使用方法 + +#### 在应用中集成 +```javascript +// 在应用启动时初始化状态管理 +document.addEventListener('DOMContentLoaded', () => { + const stateController = new PWAStateController(); + + // 监听状态恢复事件 + window.addEventListener('stateRestored', (event) => { + console.log('状态已恢复:', event.detail.state); + // 执行状态恢复后的处理 + }); +}); + +// 在关键操作后手动保存状态 +function onImportantUserAction() { + if (window.stateController) { + window.stateController.saveCurrentState(); + } +} +``` + +### 7. 最佳实践 + +1. **分层存储策略**: + - localStorage:持久性强,用于核心状态 + - sessionStorage:会话级别,用于临时状态 + - IndexedDB:大量数据存储 + - Service Worker缓存:跨页面共享状态 + +2. **智能状态恢复**: + - 检查状态年龄 + - 验证URL匹配 + - 考虑设备方向变化 + - 处理异常情况 + +3. **性能优化**: + - 避免频繁保存状态 + - 使用防抖技术 + - 异步处理状态操作 + - 清理过期状态 + +4. **错误处理**: + - 多重保存策略 + - 降级处理 + - 日志记录 + - 用户通知 + +### 8. 注意事项 + +1. **存储限制**: + - localStorage: 约5-10MB + - sessionStorage: 约5-10MB + - IndexedDB: 较大容量但可能被清理 + - Service Worker缓存: 约50MB + +2. **iOS特殊性**: + - PWA不与Safari共享存储 + - 后台执行时间有限 + - 内存压力时会被清理 + +3. **用户体验**: + - 提供状态恢复指示 + - 处理恢复失败情况 + - 保持操作流畅性 + +这个完整的解决方案应该能够显著改善PWA在iOS上的状态恢复体验,减少用户因为后台被杀而丢失状态的问题。 \ No newline at end of file From 0e440955c8d7bc9ebc50de46e5897e83e1c6691b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 6 Jul 2025 06:44:06 +0000 Subject: [PATCH 2/4] Implement PWA state management for improved iOS background persistence Co-authored-by: jxxghp --- PWA_State_Management_Implementation.md | 270 +++++++++++ src/components/PWAStateDemo.vue | 185 ++++++++ src/composables/usePWAState.ts | 171 +++++++ src/main.ts | 44 ++ src/service-worker.ts | 93 +++- src/types/pwa.d.ts | 26 + src/utils/pwaStateManager.ts | 630 +++++++++++++++++++++++++ 7 files changed, 1417 insertions(+), 2 deletions(-) create mode 100644 PWA_State_Management_Implementation.md create mode 100644 src/components/PWAStateDemo.vue create mode 100644 src/composables/usePWAState.ts create mode 100644 src/types/pwa.d.ts create mode 100644 src/utils/pwaStateManager.ts diff --git a/PWA_State_Management_Implementation.md b/PWA_State_Management_Implementation.md new file mode 100644 index 00000000..9270ce80 --- /dev/null +++ b/PWA_State_Management_Implementation.md @@ -0,0 +1,270 @@ +# PWA状态管理功能实现说明 + +## 概述 + +本次实现为您的MoviePilot项目添加了完整的PWA状态管理功能,专门解决iOS设备上PWA后台被杀导致状态丢失的问题。 + +## 已实现的功能 + +### 1. 核心状态管理器 (`src/utils/pwaStateManager.ts`) + +- ✅ **多层存储策略**:localStorage + sessionStorage + IndexedDB + Service Worker缓存 +- ✅ **智能状态恢复**:基于时间、URL、设备方向的智能决策 +- ✅ **生命周期监听**:自动监听页面可见性、焦点变化、卸载事件 +- ✅ **表单状态保存**:自动保存和恢复表单数据 +- ✅ **滚动位置恢复**:精确恢复页面滚动位置 +- ✅ **UI状态管理**:保存侧边栏、主题等界面状态 + +### 2. Service Worker增强 (`src/service-worker.ts`) + +- ✅ **状态缓存端点**:虚拟的`/api/pwa-state`端点用于状态存储 +- ✅ **消息通信**:支持与主应用的双向状态同步 +- ✅ **缓存管理**:专用的状态缓存空间 +- ✅ **错误处理**:完善的错误处理和降级策略 + +### 3. Vue集成 (`src/main.ts`) + +- ✅ **自动初始化**:PWA模式下自动启动状态管理 +- ✅ **环境检测**:智能检测PWA运行环境 +- ✅ **全局可用**:状态管理器绑定到全局对象 +- ✅ **事件监听**:监听状态恢复事件并处理 + +### 4. Vue组合式API (`src/composables/usePWAState.ts`) + +- ✅ **响应式状态**:提供响应式的状态管理接口 +- ✅ **便捷方法**:封装常用的状态操作方法 +- ✅ **类型安全**:完整的TypeScript类型支持 +- ✅ **组件友好**:易于在Vue组件中使用 + +### 5. 类型声明 (`src/types/pwa.d.ts`) + +- ✅ **类型扩展**:扩展Window和Navigator接口 +- ✅ **自定义事件**:定义状态恢复事件类型 +- ✅ **TypeScript支持**:完整的类型安全保障 + +### 6. 演示组件 (`src/components/PWAStateDemo.vue`) + +- ✅ **功能演示**:展示所有状态管理功能 +- ✅ **测试界面**:提供测试表单和操作按钮 +- ✅ **状态监控**:实时显示状态管理器状态 + +## 使用方法 + +### 在Vue组件中使用 + +```vue + + + +``` + +### 手动操作状态 + +```javascript +// 在浏览器控制台中 +window.pwaStateController.saveCurrentState() // 保存当前状态 +``` + +## 工作原理 + +### 1. 状态保存时机 + +- 🔄 **页面隐藏时**:`visibilitychange`事件触发 +- 🔄 **失去焦点时**:`blur`事件延迟1秒触发 +- 🔄 **页面卸载时**:`beforeunload`事件触发 +- 🔄 **定期保存**:每30秒自动保存一次 +- 🔄 **手动触发**:调用API手动保存 + +### 2. 状态恢复时机 + +- 🔄 **应用启动时**:自动检查并恢复状态 +- 🔄 **页面显示时**:`visibilitychange`事件触发 +- 🔄 **获得焦点时**:清除延迟保存定时器 + +### 3. 存储策略 + +``` +localStorage (主要状态) + ↓ +sessionStorage (临时状态) + ↓ +IndexedDB (大量数据) + ↓ +Service Worker缓存 (跨页面共享) +``` + +### 4. 恢复决策 + +状态恢复需要同时满足: +- ✅ 状态未过期(默认30分钟内) +- ✅ URL路径匹配 +- ✅ 设备方向未显著变化 + +## 配置选项 + +### 修改状态保存间隔 + +```typescript +// 在 PWAStateController 中修改 +private setupPeriodicSave(): void { + setInterval(() => { + if (!document.hidden) { + this.saveCurrentState() + } + }, 60000) // 改为60秒保存一次 +} +``` + +### 修改状态过期时间 + +```typescript +// 在 StateRestoreDecision 中修改 +export class StateRestoreDecision { + private maxStateAge = 60 * 60 * 1000 // 改为60分钟 +} +``` + +### 自定义状态内容 + +```typescript +// 在 PWAStateController 中添加自定义状态 +private getAppSpecificState(): any { + return { + // 现有状态... + + // 添加自定义状态 + customData: { + userPreferences: this.getUserPreferences(), + currentMedia: this.getCurrentMedia(), + searchHistory: this.getSearchHistory() + } + } +} +``` + +## 调试和监控 + +### 控制台调试 + +```javascript +// 检查状态管理器是否可用 +console.log('状态管理器:', window.pwaStateController) + +// 查看当前保存的状态 +console.log('本地状态:', localStorage.getItem('mp-pwa-app-state')) + +// 手动保存状态 +window.pwaStateController?.saveCurrentState() + +// 清除所有状态 +localStorage.removeItem('mp-pwa-app-state') +sessionStorage.removeItem('mp-pwa-session-state') +``` + +### 监听状态事件 + +```javascript +// 监听状态恢复事件 +window.addEventListener('pwaStateRestored', (event) => { + console.log('状态已恢复:', event.detail.state) +}) +``` + +## 注意事项 + +### iOS特殊性 +- PWA不与Safari共享存储空间 +- 后台执行时间有限(约30秒-5分钟) +- 内存压力时会被强制清理 +- Service Worker可能会被暂停 + +### 存储限制 +- **localStorage**: 约5-10MB +- **sessionStorage**: 约5-10MB +- **IndexedDB**: 较大但可能被清理 +- **Service Worker缓存**: 约50MB + +### 性能考虑 +- 避免保存过大的状态对象 +- 使用防抖技术避免频繁保存 +- 异步处理状态操作 +- 定期清理过期状态 + +## 测试方法 + +### 1. 基本功能测试 +1. 将应用添加到iOS桌面 +2. 打开PWA,填写测试表单 +3. 切换到其他应用 +4. 等待几分钟后重新打开PWA +5. 检查表单数据和滚动位置是否恢复 + +### 2. 状态管理测试 +1. 在PWA中访问演示页面:`/pwa-state-demo` +2. 观察状态管理器状态 +3. 测试手动保存和恢复功能 +4. 验证状态恢复通知 + +### 3. 长时间测试 +1. 保持PWA在后台运行几小时 +2. 使用其他应用增加内存压力 +3. 重新打开PWA检查状态恢复 +4. 重启设备后测试状态持久性 + +## 故障排除 + +### 状态未恢复 +1. 检查是否在PWA模式运行 +2. 确认状态管理器已初始化 +3. 检查控制台错误信息 +4. 验证存储权限和配额 + +### 性能问题 +1. 减少状态保存频率 +2. 优化状态对象大小 +3. 检查内存使用情况 +4. 考虑延迟初始化 + +### 兼容性问题 +1. 检查iOS版本支持 +2. 验证Service Worker注册 +3. 测试不同设备和浏览器 +4. 检查网络连接状态 + +## 后续优化建议 + +1. **智能压缩**:对大型状态对象进行压缩 +2. **增量保存**:只保存变化的状态部分 +3. **云端同步**:结合服务器实现跨设备状态同步 +4. **用户偏好**:允许用户自定义状态保存策略 +5. **性能监控**:添加状态管理性能指标 +6. **A/B测试**:测试不同的状态管理策略效果 + +--- + +通过这套完整的解决方案,您的MoviePilot PWA应该能够在iOS设备上提供更好的用户体验,显著减少因后台被杀而导致的状态丢失问题。 \ No newline at end of file diff --git a/src/components/PWAStateDemo.vue b/src/components/PWAStateDemo.vue new file mode 100644 index 00000000..e87199cd --- /dev/null +++ b/src/components/PWAStateDemo.vue @@ -0,0 +1,185 @@ + + + + + \ No newline at end of file diff --git a/src/composables/usePWAState.ts b/src/composables/usePWAState.ts new file mode 100644 index 00000000..fabb8824 --- /dev/null +++ b/src/composables/usePWAState.ts @@ -0,0 +1,171 @@ +/** + * PWA状态管理的Vue组合式API + */ + +import type { PWAState } from '@/utils/pwaStateManager' + +export function usePWAState() { + const isStateRestored = ref(false) + const stateRestoreCount = ref(0) + const lastRestoredState = ref(null) + + // 检查是否在PWA模式下运行 + const isPWAMode = ref(false) + + // 检查PWA模式 + const checkPWAMode = () => { + isPWAMode.value = window.matchMedia('(display-mode: standalone)').matches || + (window.navigator as any).standalone || + document.referrer.includes('android-app://') + } + + // 保存当前状态 + const saveCurrentState = async () => { + if (window.pwaStateController) { + await window.pwaStateController.saveCurrentState() + console.log('手动保存PWA状态') + } + } + + // 手动触发状态恢复检查 + const checkStateRestore = async () => { + if (window.pwaStateController) { + // 这里可以添加手动检查状态恢复的逻辑 + console.log('检查状态恢复') + } + } + + // 监听状态恢复事件 + const handleStateRestored = (event: CustomEvent<{ state: PWAState }>) => { + isStateRestored.value = true + stateRestoreCount.value++ + lastRestoredState.value = event.detail.state + + console.log('Vue组件收到状态恢复通知:', event.detail.state) + } + + // 重置状态恢复标志 + const resetStateRestored = () => { + isStateRestored.value = false + lastRestoredState.value = null + } + + // 获取状态管理器实例 + const getStateController = () => { + return window.pwaStateController + } + + // 检查状态管理器是否可用 + const isStateManagerAvailable = () => { + return !!window.pwaStateController + } + + onMounted(() => { + checkPWAMode() + + // 监听状态恢复事件 + window.addEventListener('pwaStateRestored', handleStateRestored) + + // 监听PWA模式变化 + const mediaQuery = window.matchMedia('(display-mode: standalone)') + const handleDisplayModeChange = (e: MediaQueryListEvent) => { + isPWAMode.value = e.matches + } + + if (mediaQuery.addEventListener) { + mediaQuery.addEventListener('change', handleDisplayModeChange) + } else { + // 兼容旧版本 + mediaQuery.addListener(handleDisplayModeChange) + } + + onUnmounted(() => { + if (mediaQuery.removeEventListener) { + mediaQuery.removeEventListener('change', handleDisplayModeChange) + } else { + // 兼容旧版本 + mediaQuery.removeListener(handleDisplayModeChange) + } + }) + }) + + onUnmounted(() => { + window.removeEventListener('pwaStateRestored', handleStateRestored) + }) + + return { + // 响应式状态 + isPWAMode, + isStateRestored, + stateRestoreCount, + lastRestoredState, + + // 方法 + saveCurrentState, + checkStateRestore, + resetStateRestored, + getStateController, + isStateManagerAvailable, + checkPWAMode + } +} + +/** + * 全局PWA状态管理器 + */ +export function useGlobalPWAState() { + // 检查是否在PWA环境中 + const isPWAEnvironment = () => { + return window.matchMedia('(display-mode: standalone)').matches || + (window.navigator as any).standalone || + document.referrer.includes('android-app://') + } + + // 初始化状态管理器(如果尚未初始化) + const initStateManager = async () => { + if (!window.pwaStateController && isPWAEnvironment()) { + const { PWAStateController } = await import('@/utils/pwaStateManager') + window.pwaStateController = new PWAStateController() + console.log('延迟初始化PWA状态管理器') + } + } + + // 保存应用状态 + const saveAppState = async (customData?: any) => { + await initStateManager() + + if (window.pwaStateController) { + // 如果有自定义数据,可以通过这种方式传递 + if (customData) { + // 临时存储自定义数据 + ;(window as any).tempCustomState = customData + } + + await window.pwaStateController.saveCurrentState() + + // 清除临时数据 + if (customData) { + delete (window as any).tempCustomState + } + } + } + + // 获取存储的状态 + const getStoredState = () => { + return localStorage.getItem('mp-pwa-app-state') + } + + // 清除存储的状态 + const clearStoredState = () => { + localStorage.removeItem('mp-pwa-app-state') + sessionStorage.removeItem('mp-pwa-session-state') + } + + return { + isPWAEnvironment, + initStateManager, + saveAppState, + getStoredState, + clearStoredState + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index ee874039..be10407b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -43,6 +43,9 @@ import HeaderTab from './layouts/components/HeaderTab.vue' // 7. 样式文件 - 合并为单一导入 import '@/styles/main.scss' +// 8. PWA状态管理 +import { PWAStateController } from '@/utils/pwaStateManager' + // 创建Vue实例 const app = createApp(App) @@ -89,3 +92,44 @@ app .use(ConfirmDialog) .use(i18n) .mount('#app') + +// 5. 初始化PWA状态管理器 +let pwaStateController: PWAStateController | null = null + +// 等待DOM准备就绪后初始化状态管理 +document.addEventListener('DOMContentLoaded', () => { + // 检查是否在PWA模式下运行 + const isPWA = window.matchMedia('(display-mode: standalone)').matches || + (window.navigator as any).standalone || + document.referrer.includes('android-app://') + + if (isPWA) { + console.log('检测到PWA模式,初始化状态管理器') + pwaStateController = new PWAStateController() + + // 将状态管理器绑定到全局对象,便于调试和手动操作 + ;(window as any).pwaStateController = pwaStateController + + // 监听状态恢复事件 + window.addEventListener('pwaStateRestored', (event: Event) => { + const customEvent = event as CustomEvent + console.log('PWA状态已恢复:', customEvent.detail.state) + + // 可以在这里添加状态恢复后的处理逻辑 + // 例如:通知Vue组件状态已恢复 + app.config.globalProperties.$pwaStateRestored = true + }) + + // 监听应用即将卸载事件,保存状态 + window.addEventListener('beforeunload', () => { + if (pwaStateController) { + pwaStateController.saveCurrentState() + } + }) + } else { + console.log('非PWA模式,跳过状态管理器初始化') + } +}) + +// 导出状态管理器供其他模块使用 +export { pwaStateController } diff --git a/src/service-worker.ts b/src/service-worker.ts index e8112172..77dbc3ea 100644 --- a/src/service-worker.ts +++ b/src/service-worker.ts @@ -13,6 +13,10 @@ const options = { // 存储未读消息数量的键名 const UNREAD_COUNT_KEY = 'mp_unread_count' +// 状态管理相关的缓存名称和端点 +const STATE_CACHE_NAME = 'mp-pwa-state-cache' +const STATE_ENDPOINT = '/api/pwa-state' + // 从IndexedDB获取未读消息数量 async function getStoredUnreadCount(): Promise { try { @@ -33,6 +37,52 @@ async function setStoredUnreadCount(count: number): Promise { } } +// 保存PWA状态到缓存 +async function saveStateToCache(request: Request): Promise { + try { + const state = await request.json() + const cache = await caches.open(STATE_CACHE_NAME) + + await cache.put(STATE_ENDPOINT, new Response(JSON.stringify({ + ...state, + timestamp: Date.now() + }))) + + return new Response(JSON.stringify({ success: true })) + } catch (error) { + console.error('Failed to save state to cache:', error) + return new Response(JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }) + } +} + +// 从缓存获取PWA状态 +async function getStateFromCache(): Promise { + try { + const cache = await caches.open(STATE_CACHE_NAME) + const response = await cache.match(STATE_ENDPOINT) + + if (response) { + const state = await response.json() + return new Response(JSON.stringify(state), { + headers: { 'Content-Type': 'application/json' } + }) + } + + return new Response(JSON.stringify({}), { + headers: { 'Content-Type': 'application/json' } + }) + } catch (error) { + console.error('Failed to get state from cache:', error) + return new Response(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }) + } +} + // 简单的IndexedDB包装器 async function openDB(): Promise { return new Promise((resolve, reject) => { @@ -123,6 +173,18 @@ self.addEventListener('activate', event => { // 处理API请求,当离线时发送消息到客户端 self.addEventListener('fetch', event => { + const url = new URL(event.request.url) + + // 处理PWA状态管理请求 + if (url.pathname === STATE_ENDPOINT) { + if (event.request.method === 'POST') { + event.respondWith(saveStateToCache(event.request)) + } else if (event.request.method === 'GET') { + event.respondWith(getStateFromCache()) + } + return + } + if (event.request.url.includes('/api/v1/') && event.request.method === 'GET') { event.respondWith( (async () => { @@ -226,7 +288,7 @@ self.addEventListener('message', function (event) { }) .catch(error => { console.error('Failed to clear badge:', error) - event.ports[0]?.postMessage({ success: false, error: error.message }) + event.ports[0]?.postMessage({ success: false, error: error instanceof Error ? error.message : String(error) }) }) } else if (event.data && event.data.type === 'UPDATE_BADGE') { // 更新徽章数量 @@ -238,7 +300,7 @@ self.addEventListener('message', function (event) { }) .catch(error => { console.error('Failed to update badge:', error) - event.ports[0]?.postMessage({ success: false, error: error.message }) + event.ports[0]?.postMessage({ success: false, error: error instanceof Error ? error.message : String(error) }) }) } else if (event.data && event.data.type === 'GET_UNREAD_COUNT') { // 获取未读消息数量 @@ -250,5 +312,32 @@ self.addEventListener('message', function (event) { console.error('Failed to get unread count:', error) event.ports[0]?.postMessage({ count: 0 }) }) + } else if (event.data && event.data.type === 'SAVE_PWA_STATE') { + // 保存PWA状态 + const state = event.data.state || {} + saveStateToCache(new Request(STATE_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(state) + })) + .then(response => response.json()) + .then(result => { + event.ports[0]?.postMessage({ success: result.success }) + }) + .catch(error => { + console.error('Failed to save PWA state:', error) + event.ports[0]?.postMessage({ success: false, error: error instanceof Error ? error.message : String(error) }) + }) + } else if (event.data && event.data.type === 'GET_PWA_STATE') { + // 获取PWA状态 + getStateFromCache() + .then(response => response.json()) + .then(state => { + event.ports[0]?.postMessage({ state }) + }) + .catch(error => { + console.error('Failed to get PWA state:', error) + event.ports[0]?.postMessage({ state: {} }) + }) } }) diff --git a/src/types/pwa.d.ts b/src/types/pwa.d.ts new file mode 100644 index 00000000..9d48a6d3 --- /dev/null +++ b/src/types/pwa.d.ts @@ -0,0 +1,26 @@ +/** + * PWA相关的类型声明 + */ + +// 扩展Window接口 +declare global { + interface Window { + pwaStateController?: import('@/utils/pwaStateManager').PWAStateController + orientation?: number + } + + interface Navigator { + standalone?: boolean + setAppBadge?: (count: number) => Promise + clearAppBadge?: () => Promise + } + + // 自定义事件类型 + interface WindowEventMap { + 'pwaStateRestored': CustomEvent<{ + state: import('@/utils/pwaStateManager').PWAState + }> + } +} + +export {} \ No newline at end of file diff --git a/src/utils/pwaStateManager.ts b/src/utils/pwaStateManager.ts new file mode 100644 index 00000000..ff73c4b0 --- /dev/null +++ b/src/utils/pwaStateManager.ts @@ -0,0 +1,630 @@ +/** + * PWA状态管理器 + * 用于在iOS设备上防止后台被杀时丢失状态,提供状态恢复功能 + */ + +// 应用状态接口 +export interface PWAState { + url: string + scrollPosition: number + orientation: number + timestamp: number + appData?: any + formData?: Record + userSelections?: { + selectedItems: string[] + activeTab?: string + } +} + +// 当前上下文接口 +export interface PWAContext { + url: string + orientation: number + timestamp: number +} + +/** + * 基础状态管理器(使用localStorage和sessionStorage) + */ +export class PWAStateManager { + private storageKey = 'mp-pwa-app-state' + private sessionKey = 'mp-pwa-session-state' + + // 保存应用状态 + saveState(state: PWAState): void { + try { + // 主要状态存储到localStorage + localStorage.setItem(this.storageKey, JSON.stringify({ + ...state, + timestamp: Date.now() + })) + + // 临时状态存储到sessionStorage + sessionStorage.setItem(this.sessionKey, JSON.stringify({ + scrollPosition: state.scrollPosition, + activeTab: state.appData?.activeTab, + formData: state.formData + })) + } catch (error) { + console.error('状态保存失败:', error) + } + } + + // 恢复应用状态 + restoreState(): PWAState | null { + try { + const savedState = localStorage.getItem(this.storageKey) + const sessionState = sessionStorage.getItem(this.sessionKey) + + if (savedState) { + const state = JSON.parse(savedState) + const sessionData = sessionState ? JSON.parse(sessionState) : {} + + return { + ...state, + ...sessionData, + isRestored: true + } + } + } catch (error) { + console.error('状态恢复失败:', error) + } + return null + } + + // 清除过期状态 + clearExpiredState(maxAge = 24 * 60 * 60 * 1000): void { // 24小时 + try { + const savedState = localStorage.getItem(this.storageKey) + if (savedState) { + const state = JSON.parse(savedState) + if (Date.now() - state.timestamp > maxAge) { + localStorage.removeItem(this.storageKey) + sessionStorage.removeItem(this.sessionKey) + } + } + } catch (error) { + console.error('清除过期状态失败:', error) + } + } +} + +/** + * IndexedDB状态管理器 + */ +export class PWAIndexedDBManager { + private dbName = 'MPPWAStateDB' + private dbVersion = 1 + private storeName = 'appState' + + private async initDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.dbName, this.dbVersion) + + request.onerror = () => reject(request.error) + request.onsuccess = () => resolve(request.result) + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result + if (!db.objectStoreNames.contains(this.storeName)) { + db.createObjectStore(this.storeName, { keyPath: 'id' }) + } + } + }) + } + + async saveState(state: PWAState): Promise { + try { + const db = await this.initDB() + const transaction = db.transaction([this.storeName], 'readwrite') + const store = transaction.objectStore(this.storeName) + + await store.put({ + id: 'appState', + data: state, + timestamp: Date.now() + }) + } catch (error) { + console.error('IndexedDB保存失败:', error) + } + } + + async restoreState(): Promise { + try { + const db = await this.initDB() + const transaction = db.transaction([this.storeName], 'readonly') + const store = transaction.objectStore(this.storeName) + + return new Promise((resolve, reject) => { + const request = store.get('appState') + request.onsuccess = () => { + const result = request.result + resolve(result ? result.data : null) + } + request.onerror = () => reject(request.error) + }) + } catch (error) { + console.error('IndexedDB恢复失败:', error) + return null + } + } +} + +/** + * Service Worker状态同步 + */ +export class ServiceWorkerStateSync { + private stateEndpoint = '/api/pwa-state' + + async saveState(state: PWAState): Promise { + try { + const response = await fetch(this.stateEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(state) + }) + + const result = await response.json() + return result.success + } catch (error) { + console.error('Service Worker状态保存失败:', error) + return false + } + } + + async loadState(): Promise { + try { + const response = await fetch(this.stateEndpoint) + const state = await response.json() + return Object.keys(state).length > 0 ? state : null + } catch (error) { + console.error('Service Worker状态加载失败:', error) + return null + } + } + + // 使用MessageChannel与Service Worker通信 + async saveStateViaMessage(state: PWAState): Promise { + return new Promise((resolve) => { + if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { + const channel = new MessageChannel() + channel.port1.onmessage = (event) => { + resolve(event.data.success) + } + + navigator.serviceWorker.controller.postMessage({ + type: 'SAVE_PWA_STATE', + state + }, [channel.port2]) + } else { + resolve(false) + } + }) + } + + async loadStateViaMessage(): Promise { + return new Promise((resolve) => { + if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { + const channel = new MessageChannel() + channel.port1.onmessage = (event) => { + resolve(event.data.state || null) + } + + navigator.serviceWorker.controller.postMessage({ + type: 'GET_PWA_STATE' + }, [channel.port2]) + } else { + resolve(null) + } + }) + } +} + +/** + * 状态恢复决策器 + */ +export class StateRestoreDecision { + private maxStateAge = 30 * 60 * 1000 // 30分钟 + + shouldRestoreState(savedState: PWAState | null, currentContext: PWAContext): boolean { + if (!savedState) return false + + // 检查状态年龄 + if (this.isStateExpired(savedState)) { + return false + } + + // 检查URL匹配 + if (!this.isUrlCompatible(savedState.url, currentContext.url)) { + return false + } + + // 检查设备方向 + if (this.isOrientationChanged(savedState, currentContext)) { + return false + } + + return true + } + + private isStateExpired(savedState: PWAState): boolean { + return Date.now() - savedState.timestamp > this.maxStateAge + } + + private isUrlCompatible(savedUrl: string, currentUrl: string): boolean { + if (!savedUrl || !currentUrl) return false + + try { + const savedPath = new URL(savedUrl).pathname + const currentPath = new URL(currentUrl).pathname + return savedPath === currentPath + } catch { + return false + } + } + + private isOrientationChanged(savedState: PWAState, currentContext: PWAContext): boolean { + return savedState.orientation !== currentContext.orientation + } +} + +/** + * 页面可见性状态管理器 + */ +export class VisibilityStateManager { + private stateManager: PWAStateManager + private blurTimer: number | null = null + + constructor(stateManager: PWAStateManager) { + this.stateManager = stateManager + this.setupVisibilityListener() + } + + private setupVisibilityListener(): void { + // 监听页面可见性变化 + document.addEventListener('visibilitychange', () => { + if (document.hidden) { + this.handlePageHidden() + } else { + this.handlePageVisible() + } + }) + + // 监听页面卸载 + window.addEventListener('beforeunload', () => { + this.handlePageUnload() + }) + + // 监听页面焦点变化 + window.addEventListener('blur', () => { + this.handlePageBlur() + }) + + window.addEventListener('focus', () => { + this.handlePageFocus() + }) + } + + private handlePageHidden(): void { + const currentState = this.getCurrentAppState() + this.stateManager.saveState(currentState) + console.log('页面被隐藏,已保存状态') + } + + private handlePageVisible(): void { + const restoredState = this.stateManager.restoreState() + if (restoredState) { + this.restoreAppState(restoredState) + console.log('页面显示,已恢复状态') + } + } + + private handlePageUnload(): void { + const currentState = this.getCurrentAppState() + this.stateManager.saveState(currentState) + } + + private handlePageBlur(): void { + if (this.blurTimer) clearTimeout(this.blurTimer) + this.blurTimer = window.setTimeout(() => { + const currentState = this.getCurrentAppState() + this.stateManager.saveState(currentState) + }, 1000) + } + + private handlePageFocus(): void { + if (this.blurTimer) { + clearTimeout(this.blurTimer) + this.blurTimer = null + } + } + + private getCurrentAppState(): PWAState { + return { + url: window.location.href, + scrollPosition: window.scrollY, + orientation: window.orientation || 0, + timestamp: Date.now(), + appData: this.getAppSpecificState() + } + } + + private restoreAppState(state: PWAState): void { + if (state.scrollPosition) { + window.scrollTo(0, state.scrollPosition) + } + if (state.appData) { + this.restoreAppSpecificState(state.appData) + } + } + + private getAppSpecificState(): any { + // 获取应用特定状态 + return { + formData: this.getFormData(), + userSelections: this.getUserSelections() + } + } + + private restoreAppSpecificState(appData: any): void { + if (appData.formData) { + this.restoreFormData(appData.formData) + } + if (appData.userSelections) { + this.restoreUserSelections(appData.userSelections) + } + } + + private getFormData(): Record { + const forms = document.querySelectorAll('form') + const formData: Record = {} + + forms.forEach((form, index) => { + const data = new FormData(form) + formData[`form-${index}`] = Object.fromEntries(data) + }) + + return formData + } + + private restoreFormData(formData: Record): void { + Object.entries(formData).forEach(([formId, data]) => { + const formIndex = parseInt(formId.split('-')[1]) + const form = document.querySelectorAll('form')[formIndex] + + if (form) { + Object.entries(data).forEach(([name, value]) => { + const input = form.querySelector(`[name="${name}"]`) as HTMLInputElement + if (input) { + input.value = value as string + } + }) + } + }) + } + + private getUserSelections(): any { + return { + selectedItems: Array.from(document.querySelectorAll('.selected')).map(el => el.id), + activeTab: document.querySelector('.tab.active')?.id + } + } + + private restoreUserSelections(selections: any): void { + if (selections.selectedItems) { + selections.selectedItems.forEach((id: string) => { + const element = document.getElementById(id) + if (element) { + element.classList.add('selected') + } + }) + } + + if (selections.activeTab) { + const tab = document.getElementById(selections.activeTab) + if (tab) { + tab.classList.add('active') + } + } + } +} + +/** + * 完整的PWA状态管理器 + */ +export class PWAStateController { + private stateManager: PWAStateManager + private indexedDBManager: PWAIndexedDBManager + private swStateSync: ServiceWorkerStateSync + private visibilityManager: VisibilityStateManager + private restoreDecision: StateRestoreDecision + + constructor() { + this.stateManager = new PWAStateManager() + this.indexedDBManager = new PWAIndexedDBManager() + this.swStateSync = new ServiceWorkerStateSync() + this.visibilityManager = new VisibilityStateManager(this.stateManager) + this.restoreDecision = new StateRestoreDecision() + + this.init() + } + + private async init(): Promise { + // 清理过期状态 + this.stateManager.clearExpiredState() + + // 检查是否需要恢复状态 + await this.checkAndRestoreState() + + // 设置定期保存 + this.setupPeriodicSave() + } + + private async checkAndRestoreState(): Promise { + const currentContext: PWAContext = { + url: window.location.href, + orientation: window.orientation || 0, + timestamp: Date.now() + } + + // 尝试从多个来源恢复状态 + const sources = [ + () => this.stateManager.restoreState(), + () => this.indexedDBManager.restoreState(), + () => this.swStateSync.loadState(), + () => this.swStateSync.loadStateViaMessage() + ] + + for (const source of sources) { + try { + const savedState = await source() + if (this.restoreDecision.shouldRestoreState(savedState, currentContext)) { + await this.restoreState(savedState!) + return + } + } catch (error) { + console.error('状态恢复失败:', error) + } + } + } + + async saveCurrentState(): Promise { + const state: PWAState = { + url: window.location.href, + scrollPosition: window.scrollY, + orientation: window.orientation || 0, + timestamp: Date.now(), + appData: this.getAppSpecificState() + } + + // 多重保存策略 + await Promise.allSettled([ + this.stateManager.saveState(state), + this.indexedDBManager.saveState(state), + this.swStateSync.saveState(state), + this.swStateSync.saveStateViaMessage(state) + ]) + } + + private async restoreState(state: PWAState): Promise { + // 恢复滚动位置 + if (state.scrollPosition) { + window.scrollTo(0, state.scrollPosition) + } + + // 恢复应用特定状态 + if (state.appData) { + this.restoreAppSpecificState(state.appData) + } + + // 触发状态恢复事件 + this.dispatchStateRestoreEvent(state) + } + + private setupPeriodicSave(): void { + // 每30秒保存一次状态 + setInterval(() => { + if (!document.hidden) { + this.saveCurrentState() + } + }, 30000) + } + + private getAppSpecificState(): any { + // 可以在这里添加MoviePilot特定的状态 + return { + // 路由状态 + routerState: this.getRouterState(), + // 用户界面状态 + uiState: this.getUIState(), + // 表单状态 + formState: this.getFormState() + } + } + + private getRouterState(): any { + // 获取Vue Router状态 + return { + currentRoute: window.location.pathname, + query: window.location.search, + hash: window.location.hash + } + } + + private getUIState(): any { + // 获取UI状态 + return { + sidebarOpen: document.querySelector('.v-navigation-drawer--active') !== null, + darkMode: document.documentElement.classList.contains('dark') || + document.documentElement.getAttribute('data-theme') === 'dark' + } + } + + private getFormState(): any { + // 获取表单状态 + const forms = document.querySelectorAll('form') + const formData: Record = {} + + forms.forEach((form, index) => { + const inputs = form.querySelectorAll('input, select, textarea') + const data: Record = {} + + inputs.forEach((input) => { + const element = input as HTMLInputElement + if (element.name) { + data[element.name] = element.value + } + }) + + if (Object.keys(data).length > 0) { + formData[`form-${index}`] = data + } + }) + + return formData + } + + private restoreAppSpecificState(appData: any): void { + if (appData.uiState) { + this.restoreUIState(appData.uiState) + } + if (appData.formState) { + this.restoreFormState(appData.formState) + } + } + + private restoreUIState(uiState: any): void { + // 恢复UI状态 + if (uiState.darkMode !== undefined) { + // 这里可以根据实际的主题切换逻辑来恢复 + console.log('恢复主题状态:', uiState.darkMode) + } + } + + private restoreFormState(formState: any): void { + // 恢复表单状态 + Object.entries(formState).forEach(([formId, data]) => { + const formIndex = parseInt(formId.split('-')[1]) + const form = document.querySelectorAll('form')[formIndex] + + if (form) { + Object.entries(data as Record).forEach(([name, value]) => { + const input = form.querySelector(`[name="${name}"]`) as HTMLInputElement + if (input) { + input.value = value as string + // 触发change事件,以便Vue能够响应 + input.dispatchEvent(new Event('input', { bubbles: true })) + } + }) + } + }) + } + + private dispatchStateRestoreEvent(state: PWAState): void { + const event = new CustomEvent('pwaStateRestored', { + detail: { state } + }) + window.dispatchEvent(event) + } +} \ No newline at end of file From e1ff50e1e30a6ec08f86c74e853a32327bb8379c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 6 Jul 2025 06:50:30 +0000 Subject: [PATCH 3/4] Refactor usePWAState and useGlobalPWAState composables Co-authored-by: jxxghp --- src/composables/usePWAState.ts | 59 ++++------------------------------ 1 file changed, 7 insertions(+), 52 deletions(-) diff --git a/src/composables/usePWAState.ts b/src/composables/usePWAState.ts index fabb8824..99fa89f7 100644 --- a/src/composables/usePWAState.ts +++ b/src/composables/usePWAState.ts @@ -1,15 +1,10 @@ -/** - * PWA状态管理的Vue组合式API - */ - +import { ref, onMounted, onUnmounted, watch } from 'vue' import type { PWAState } from '@/utils/pwaStateManager' export function usePWAState() { const isStateRestored = ref(false) const stateRestoreCount = ref(0) const lastRestoredState = ref(null) - - // 检查是否在PWA模式下运行 const isPWAMode = ref(false) // 检查PWA模式 @@ -30,18 +25,18 @@ export function usePWAState() { // 手动触发状态恢复检查 const checkStateRestore = async () => { if (window.pwaStateController) { - // 这里可以添加手动检查状态恢复的逻辑 console.log('检查状态恢复') } } // 监听状态恢复事件 - const handleStateRestored = (event: CustomEvent<{ state: PWAState }>) => { + const handleStateRestored = (event: Event) => { + const customEvent = event as CustomEvent<{ state: PWAState }> isStateRestored.value = true stateRestoreCount.value++ - lastRestoredState.value = event.detail.state + lastRestoredState.value = customEvent.detail.state - console.log('Vue组件收到状态恢复通知:', event.detail.state) + console.log('Vue组件收到状态恢复通知:', customEvent.detail.state) } // 重置状态恢复标志 @@ -50,11 +45,6 @@ export function usePWAState() { lastRestoredState.value = null } - // 获取状态管理器实例 - const getStateController = () => { - return window.pwaStateController - } - // 检查状态管理器是否可用 const isStateManagerAvailable = () => { return !!window.pwaStateController @@ -75,7 +65,6 @@ export function usePWAState() { if (mediaQuery.addEventListener) { mediaQuery.addEventListener('change', handleDisplayModeChange) } else { - // 兼容旧版本 mediaQuery.addListener(handleDisplayModeChange) } @@ -83,7 +72,6 @@ export function usePWAState() { if (mediaQuery.removeEventListener) { mediaQuery.removeEventListener('change', handleDisplayModeChange) } else { - // 兼容旧版本 mediaQuery.removeListener(handleDisplayModeChange) } }) @@ -104,15 +92,12 @@ export function usePWAState() { saveCurrentState, checkStateRestore, resetStateRestored, - getStateController, isStateManagerAvailable, checkPWAMode } } -/** - * 全局PWA状态管理器 - */ +// 全局PWA状态管理器 export function useGlobalPWAState() { // 检查是否在PWA环境中 const isPWAEnvironment = () => { @@ -121,35 +106,6 @@ export function useGlobalPWAState() { document.referrer.includes('android-app://') } - // 初始化状态管理器(如果尚未初始化) - const initStateManager = async () => { - if (!window.pwaStateController && isPWAEnvironment()) { - const { PWAStateController } = await import('@/utils/pwaStateManager') - window.pwaStateController = new PWAStateController() - console.log('延迟初始化PWA状态管理器') - } - } - - // 保存应用状态 - const saveAppState = async (customData?: any) => { - await initStateManager() - - if (window.pwaStateController) { - // 如果有自定义数据,可以通过这种方式传递 - if (customData) { - // 临时存储自定义数据 - ;(window as any).tempCustomState = customData - } - - await window.pwaStateController.saveCurrentState() - - // 清除临时数据 - if (customData) { - delete (window as any).tempCustomState - } - } - } - // 获取存储的状态 const getStoredState = () => { return localStorage.getItem('mp-pwa-app-state') @@ -159,12 +115,11 @@ export function useGlobalPWAState() { const clearStoredState = () => { localStorage.removeItem('mp-pwa-app-state') sessionStorage.removeItem('mp-pwa-session-state') + console.log('已清除PWA存储状态') } return { isPWAEnvironment, - initStateManager, - saveAppState, getStoredState, clearStoredState } From c0ee9988741eca4e43ca809a33004bc367260ef7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 6 Jul 2025 06:52:18 +0000 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0PWA=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=AE=A1=E7=90=86=E9=98=B2=E6=AD=A2iOS=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E8=A2=AB=E6=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加多层存储策略(localStorage + sessionStorage + IndexedDB + Service Worker缓存) - 实现智能状态恢复决策机制 - 自动监听页面生命周期事件进行状态保存和恢复 - 支持表单数据、滚动位置、UI状态的自动保存 - 专为iOS设备PWA优化,解决后台被杀导致状态丢失的问题 - 版本号更新至 2.6.3 --- PWA_State_Management_Implementation.md | 270 ----------- PWA_iOS_Background_Solutions.md | 645 ------------------------- package.json | 2 +- src/components/PWAStateDemo.vue | 185 ------- 4 files changed, 1 insertion(+), 1101 deletions(-) delete mode 100644 PWA_State_Management_Implementation.md delete mode 100644 PWA_iOS_Background_Solutions.md delete mode 100644 src/components/PWAStateDemo.vue diff --git a/PWA_State_Management_Implementation.md b/PWA_State_Management_Implementation.md deleted file mode 100644 index 9270ce80..00000000 --- a/PWA_State_Management_Implementation.md +++ /dev/null @@ -1,270 +0,0 @@ -# PWA状态管理功能实现说明 - -## 概述 - -本次实现为您的MoviePilot项目添加了完整的PWA状态管理功能,专门解决iOS设备上PWA后台被杀导致状态丢失的问题。 - -## 已实现的功能 - -### 1. 核心状态管理器 (`src/utils/pwaStateManager.ts`) - -- ✅ **多层存储策略**:localStorage + sessionStorage + IndexedDB + Service Worker缓存 -- ✅ **智能状态恢复**:基于时间、URL、设备方向的智能决策 -- ✅ **生命周期监听**:自动监听页面可见性、焦点变化、卸载事件 -- ✅ **表单状态保存**:自动保存和恢复表单数据 -- ✅ **滚动位置恢复**:精确恢复页面滚动位置 -- ✅ **UI状态管理**:保存侧边栏、主题等界面状态 - -### 2. Service Worker增强 (`src/service-worker.ts`) - -- ✅ **状态缓存端点**:虚拟的`/api/pwa-state`端点用于状态存储 -- ✅ **消息通信**:支持与主应用的双向状态同步 -- ✅ **缓存管理**:专用的状态缓存空间 -- ✅ **错误处理**:完善的错误处理和降级策略 - -### 3. Vue集成 (`src/main.ts`) - -- ✅ **自动初始化**:PWA模式下自动启动状态管理 -- ✅ **环境检测**:智能检测PWA运行环境 -- ✅ **全局可用**:状态管理器绑定到全局对象 -- ✅ **事件监听**:监听状态恢复事件并处理 - -### 4. Vue组合式API (`src/composables/usePWAState.ts`) - -- ✅ **响应式状态**:提供响应式的状态管理接口 -- ✅ **便捷方法**:封装常用的状态操作方法 -- ✅ **类型安全**:完整的TypeScript类型支持 -- ✅ **组件友好**:易于在Vue组件中使用 - -### 5. 类型声明 (`src/types/pwa.d.ts`) - -- ✅ **类型扩展**:扩展Window和Navigator接口 -- ✅ **自定义事件**:定义状态恢复事件类型 -- ✅ **TypeScript支持**:完整的类型安全保障 - -### 6. 演示组件 (`src/components/PWAStateDemo.vue`) - -- ✅ **功能演示**:展示所有状态管理功能 -- ✅ **测试界面**:提供测试表单和操作按钮 -- ✅ **状态监控**:实时显示状态管理器状态 - -## 使用方法 - -### 在Vue组件中使用 - -```vue - - - -``` - -### 手动操作状态 - -```javascript -// 在浏览器控制台中 -window.pwaStateController.saveCurrentState() // 保存当前状态 -``` - -## 工作原理 - -### 1. 状态保存时机 - -- 🔄 **页面隐藏时**:`visibilitychange`事件触发 -- 🔄 **失去焦点时**:`blur`事件延迟1秒触发 -- 🔄 **页面卸载时**:`beforeunload`事件触发 -- 🔄 **定期保存**:每30秒自动保存一次 -- 🔄 **手动触发**:调用API手动保存 - -### 2. 状态恢复时机 - -- 🔄 **应用启动时**:自动检查并恢复状态 -- 🔄 **页面显示时**:`visibilitychange`事件触发 -- 🔄 **获得焦点时**:清除延迟保存定时器 - -### 3. 存储策略 - -``` -localStorage (主要状态) - ↓ -sessionStorage (临时状态) - ↓ -IndexedDB (大量数据) - ↓ -Service Worker缓存 (跨页面共享) -``` - -### 4. 恢复决策 - -状态恢复需要同时满足: -- ✅ 状态未过期(默认30分钟内) -- ✅ URL路径匹配 -- ✅ 设备方向未显著变化 - -## 配置选项 - -### 修改状态保存间隔 - -```typescript -// 在 PWAStateController 中修改 -private setupPeriodicSave(): void { - setInterval(() => { - if (!document.hidden) { - this.saveCurrentState() - } - }, 60000) // 改为60秒保存一次 -} -``` - -### 修改状态过期时间 - -```typescript -// 在 StateRestoreDecision 中修改 -export class StateRestoreDecision { - private maxStateAge = 60 * 60 * 1000 // 改为60分钟 -} -``` - -### 自定义状态内容 - -```typescript -// 在 PWAStateController 中添加自定义状态 -private getAppSpecificState(): any { - return { - // 现有状态... - - // 添加自定义状态 - customData: { - userPreferences: this.getUserPreferences(), - currentMedia: this.getCurrentMedia(), - searchHistory: this.getSearchHistory() - } - } -} -``` - -## 调试和监控 - -### 控制台调试 - -```javascript -// 检查状态管理器是否可用 -console.log('状态管理器:', window.pwaStateController) - -// 查看当前保存的状态 -console.log('本地状态:', localStorage.getItem('mp-pwa-app-state')) - -// 手动保存状态 -window.pwaStateController?.saveCurrentState() - -// 清除所有状态 -localStorage.removeItem('mp-pwa-app-state') -sessionStorage.removeItem('mp-pwa-session-state') -``` - -### 监听状态事件 - -```javascript -// 监听状态恢复事件 -window.addEventListener('pwaStateRestored', (event) => { - console.log('状态已恢复:', event.detail.state) -}) -``` - -## 注意事项 - -### iOS特殊性 -- PWA不与Safari共享存储空间 -- 后台执行时间有限(约30秒-5分钟) -- 内存压力时会被强制清理 -- Service Worker可能会被暂停 - -### 存储限制 -- **localStorage**: 约5-10MB -- **sessionStorage**: 约5-10MB -- **IndexedDB**: 较大但可能被清理 -- **Service Worker缓存**: 约50MB - -### 性能考虑 -- 避免保存过大的状态对象 -- 使用防抖技术避免频繁保存 -- 异步处理状态操作 -- 定期清理过期状态 - -## 测试方法 - -### 1. 基本功能测试 -1. 将应用添加到iOS桌面 -2. 打开PWA,填写测试表单 -3. 切换到其他应用 -4. 等待几分钟后重新打开PWA -5. 检查表单数据和滚动位置是否恢复 - -### 2. 状态管理测试 -1. 在PWA中访问演示页面:`/pwa-state-demo` -2. 观察状态管理器状态 -3. 测试手动保存和恢复功能 -4. 验证状态恢复通知 - -### 3. 长时间测试 -1. 保持PWA在后台运行几小时 -2. 使用其他应用增加内存压力 -3. 重新打开PWA检查状态恢复 -4. 重启设备后测试状态持久性 - -## 故障排除 - -### 状态未恢复 -1. 检查是否在PWA模式运行 -2. 确认状态管理器已初始化 -3. 检查控制台错误信息 -4. 验证存储权限和配额 - -### 性能问题 -1. 减少状态保存频率 -2. 优化状态对象大小 -3. 检查内存使用情况 -4. 考虑延迟初始化 - -### 兼容性问题 -1. 检查iOS版本支持 -2. 验证Service Worker注册 -3. 测试不同设备和浏览器 -4. 检查网络连接状态 - -## 后续优化建议 - -1. **智能压缩**:对大型状态对象进行压缩 -2. **增量保存**:只保存变化的状态部分 -3. **云端同步**:结合服务器实现跨设备状态同步 -4. **用户偏好**:允许用户自定义状态保存策略 -5. **性能监控**:添加状态管理性能指标 -6. **A/B测试**:测试不同的状态管理策略效果 - ---- - -通过这套完整的解决方案,您的MoviePilot PWA应该能够在iOS设备上提供更好的用户体验,显著减少因后台被杀而导致的状态丢失问题。 \ No newline at end of file diff --git a/PWA_iOS_Background_Solutions.md b/PWA_iOS_Background_Solutions.md deleted file mode 100644 index 96426bec..00000000 --- a/PWA_iOS_Background_Solutions.md +++ /dev/null @@ -1,645 +0,0 @@ -# PWA在iOS上防止后台被杀及状态恢复解决方案 - -## 问题概述 - -PWA添加到iOS桌面后,经常遇到以下问题: -- iOS系统积极清理后台应用内存 -- 重新打开PWA时页面刷新,丢失之前状态 -- 用户体验不佳,类似于"冷启动" - -## 核心解决策略 - -### 1. 实现状态持久化 - -#### 使用多层存储策略 -```javascript -// 状态管理类 -class PWAStateManager { - constructor() { - this.storageKey = 'pwa-app-state'; - this.sessionKey = 'pwa-session-state'; - } - - // 保存应用状态 - saveState(state) { - try { - // 主要状态存储到localStorage - localStorage.setItem(this.storageKey, JSON.stringify({ - ...state, - timestamp: Date.now() - })); - - // 临时状态存储到sessionStorage - sessionStorage.setItem(this.sessionKey, JSON.stringify({ - scrollPosition: window.scrollY, - activeTab: state.activeTab, - formData: state.formData - })); - } catch (error) { - console.error('状态保存失败:', error); - } - } - - // 恢复应用状态 - restoreState() { - try { - const savedState = localStorage.getItem(this.storageKey); - const sessionState = sessionStorage.getItem(this.sessionKey); - - if (savedState) { - const state = JSON.parse(savedState); - const sessionData = sessionState ? JSON.parse(sessionState) : {}; - - return { - ...state, - ...sessionData, - isRestored: true - }; - } - } catch (error) { - console.error('状态恢复失败:', error); - } - return null; - } - - // 清除过期状态 - clearExpiredState(maxAge = 24 * 60 * 60 * 1000) { // 24小时 - try { - const savedState = localStorage.getItem(this.storageKey); - if (savedState) { - const state = JSON.parse(savedState); - if (Date.now() - state.timestamp > maxAge) { - localStorage.removeItem(this.storageKey); - sessionStorage.removeItem(this.sessionKey); - } - } - } catch (error) { - console.error('清除过期状态失败:', error); - } - } -} -``` - -#### 使用IndexedDB存储大量数据 -```javascript -// IndexedDB状态管理 -class PWAIndexedDBManager { - constructor() { - this.dbName = 'PWAStateDB'; - this.dbVersion = 1; - this.storeName = 'appState'; - } - - async initDB() { - return new Promise((resolve, reject) => { - const request = indexedDB.open(this.dbName, this.dbVersion); - - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result); - - request.onupgradeneeded = (event) => { - const db = event.target.result; - if (!db.objectStoreNames.contains(this.storeName)) { - db.createObjectStore(this.storeName, { keyPath: 'id' }); - } - }; - }); - } - - async saveState(state) { - try { - const db = await this.initDB(); - const transaction = db.transaction([this.storeName], 'readwrite'); - const store = transaction.objectStore(this.storeName); - - await store.put({ - id: 'appState', - data: state, - timestamp: Date.now() - }); - } catch (error) { - console.error('IndexedDB保存失败:', error); - } - } - - async restoreState() { - try { - const db = await this.initDB(); - const transaction = db.transaction([this.storeName], 'readonly'); - const store = transaction.objectStore(this.storeName); - - return new Promise((resolve, reject) => { - const request = store.get('appState'); - request.onsuccess = () => { - const result = request.result; - resolve(result ? result.data : null); - }; - request.onerror = () => reject(request.error); - }); - } catch (error) { - console.error('IndexedDB恢复失败:', error); - return null; - } - } -} -``` - -### 2. 使用Service Worker实现状态共享 - -#### Service Worker状态管理 -```javascript -// sw.js - Service Worker中的状态管理 -const STATE_CACHE_NAME = 'pwa-state-cache'; -const STATE_ENDPOINT = '/api/state'; - -// 激活时立即接管页面 -self.addEventListener('activate', event => { - event.waitUntil(clients.claim()); -}); - -// 拦截状态相关请求 -self.addEventListener('fetch', event => { - const { request } = event; - const url = new URL(request.url); - - if (url.pathname === STATE_ENDPOINT) { - if (request.method === 'POST') { - event.respondWith(saveStateToCache(request)); - } else if (request.method === 'GET') { - event.respondWith(getStateFromCache()); - } - } -}); - -// 保存状态到缓存 -async function saveStateToCache(request) { - try { - const state = await request.json(); - const cache = await caches.open(STATE_CACHE_NAME); - - await cache.put(STATE_ENDPOINT, new Response(JSON.stringify({ - ...state, - timestamp: Date.now() - }))); - - return new Response(JSON.stringify({ success: true })); - } catch (error) { - return new Response(JSON.stringify({ error: error.message }), { - status: 500 - }); - } -} - -// 从缓存获取状态 -async function getStateFromCache() { - try { - const cache = await caches.open(STATE_CACHE_NAME); - const response = await cache.match(STATE_ENDPOINT); - - if (response) { - const state = await response.json(); - return new Response(JSON.stringify(state)); - } - - return new Response(JSON.stringify({})); - } catch (error) { - return new Response(JSON.stringify({ error: error.message }), { - status: 500 - }); - } -} -``` - -#### 客户端状态同步 -```javascript -// 客户端状态同步 -class ServiceWorkerStateSync { - constructor() { - this.stateEndpoint = '/api/state'; - } - - async saveState(state) { - try { - const response = await fetch(this.stateEndpoint, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(state) - }); - - return await response.json(); - } catch (error) { - console.error('Service Worker状态保存失败:', error); - } - } - - async loadState() { - try { - const response = await fetch(this.stateEndpoint); - return await response.json(); - } catch (error) { - console.error('Service Worker状态加载失败:', error); - return {}; - } - } -} -``` - -### 3. 监听应用生命周期 - -#### 页面可见性监听 -```javascript -// 页面可见性状态管理 -class VisibilityStateManager { - constructor(stateManager) { - this.stateManager = stateManager; - this.setupVisibilityListener(); - } - - setupVisibilityListener() { - // 监听页面可见性变化 - document.addEventListener('visibilitychange', () => { - if (document.hidden) { - // 页面被隐藏时保存状态 - this.handlePageHidden(); - } else { - // 页面显示时恢复状态 - this.handlePageVisible(); - } - }); - - // 监听页面卸载 - window.addEventListener('beforeunload', () => { - this.handlePageUnload(); - }); - - // 监听页面焦点变化 - window.addEventListener('blur', () => { - this.handlePageBlur(); - }); - - window.addEventListener('focus', () => { - this.handlePageFocus(); - }); - } - - handlePageHidden() { - // 页面被隐藏时保存当前状态 - const currentState = this.getCurrentAppState(); - this.stateManager.saveState(currentState); - - console.log('页面被隐藏,已保存状态'); - } - - handlePageVisible() { - // 页面显示时检查是否需要恢复状态 - const restoredState = this.stateManager.restoreState(); - if (restoredState) { - this.restoreAppState(restoredState); - console.log('页面显示,已恢复状态'); - } - } - - handlePageUnload() { - // 页面卸载时最后保存状态 - const currentState = this.getCurrentAppState(); - this.stateManager.saveState(currentState); - } - - handlePageBlur() { - // 失去焦点时保存状态(定时器避免频繁保存) - if (this.blurTimer) clearTimeout(this.blurTimer); - this.blurTimer = setTimeout(() => { - const currentState = this.getCurrentAppState(); - this.stateManager.saveState(currentState); - }, 1000); - } - - handlePageFocus() { - // 获得焦点时清除定时器 - if (this.blurTimer) { - clearTimeout(this.blurTimer); - this.blurTimer = null; - } - } - - getCurrentAppState() { - // 获取当前应用状态(需要根据具体应用实现) - return { - url: window.location.href, - scrollPosition: window.scrollY, - timestamp: Date.now(), - // 添加其他应用特定状态 - }; - } - - restoreAppState(state) { - // 恢复应用状态(需要根据具体应用实现) - if (state.scrollPosition) { - window.scrollTo(0, state.scrollPosition); - } - // 恢复其他应用特定状态 - } -} -``` - -### 4. 实现智能状态恢复 - -#### 状态恢复决策器 -```javascript -class StateRestoreDecision { - constructor() { - this.maxStateAge = 30 * 60 * 1000; // 30分钟 - this.urlChangeThreshold = 5; // URL变化阈值 - } - - shouldRestoreState(savedState, currentContext) { - if (!savedState) return false; - - // 检查状态年龄 - if (this.isStateExpired(savedState)) { - return false; - } - - // 检查URL匹配 - if (!this.isUrlCompatible(savedState.url, currentContext.url)) { - return false; - } - - // 检查设备方向 - if (this.isOrientationChanged(savedState, currentContext)) { - return false; - } - - return true; - } - - isStateExpired(savedState) { - return Date.now() - savedState.timestamp > this.maxStateAge; - } - - isUrlCompatible(savedUrl, currentUrl) { - if (!savedUrl || !currentUrl) return false; - - const savedPath = new URL(savedUrl).pathname; - const currentPath = new URL(currentUrl).pathname; - - return savedPath === currentPath; - } - - isOrientationChanged(savedState, currentContext) { - return savedState.orientation !== currentContext.orientation; - } -} -``` - -### 5. 完整的应用状态管理器 - -#### 统一状态管理 -```javascript -// 完整的PWA状态管理器 -class PWAStateController { - constructor() { - this.stateManager = new PWAStateManager(); - this.indexedDBManager = new PWAIndexedDBManager(); - this.swStateSync = new ServiceWorkerStateSync(); - this.visibilityManager = new VisibilityStateManager(this.stateManager); - this.restoreDecision = new StateRestoreDecision(); - - this.init(); - } - - async init() { - // 清理过期状态 - this.stateManager.clearExpiredState(); - - // 检查是否需要恢复状态 - await this.checkAndRestoreState(); - - // 设置定期保存 - this.setupPeriodicSave(); - } - - async checkAndRestoreState() { - const currentContext = { - url: window.location.href, - orientation: window.orientation || 0, - timestamp: Date.now() - }; - - // 尝试从多个来源恢复状态 - const sources = [ - () => this.stateManager.restoreState(), - () => this.indexedDBManager.restoreState(), - () => this.swStateSync.loadState() - ]; - - for (const source of sources) { - try { - const savedState = await source(); - if (this.restoreDecision.shouldRestoreState(savedState, currentContext)) { - await this.restoreState(savedState); - return; - } - } catch (error) { - console.error('状态恢复失败:', error); - } - } - } - - async saveCurrentState() { - const state = { - url: window.location.href, - scrollPosition: window.scrollY, - orientation: window.orientation || 0, - timestamp: Date.now(), - // 添加应用特定状态 - appData: this.getAppSpecificState() - }; - - // 多重保存策略 - await Promise.allSettled([ - this.stateManager.saveState(state), - this.indexedDBManager.saveState(state), - this.swStateSync.saveState(state) - ]); - } - - async restoreState(state) { - // 恢复滚动位置 - if (state.scrollPosition) { - window.scrollTo(0, state.scrollPosition); - } - - // 恢复应用特定状态 - if (state.appData) { - this.restoreAppSpecificState(state.appData); - } - - // 触发状态恢复事件 - this.dispatchStateRestoreEvent(state); - } - - setupPeriodicSave() { - // 每30秒保存一次状态 - setInterval(() => { - if (!document.hidden) { - this.saveCurrentState(); - } - }, 30000); - } - - getAppSpecificState() { - // 根据具体应用实现 - return { - // 表单数据 - formData: this.getFormData(), - // 用户选择 - userSelections: this.getUserSelections(), - // 其他应用状态 - }; - } - - restoreAppSpecificState(appData) { - // 根据具体应用实现状态恢复 - if (appData.formData) { - this.restoreFormData(appData.formData); - } - if (appData.userSelections) { - this.restoreUserSelections(appData.userSelections); - } - } - - dispatchStateRestoreEvent(state) { - const event = new CustomEvent('stateRestored', { - detail: { state } - }); - window.dispatchEvent(event); - } - - getFormData() { - // 获取表单数据 - const forms = document.querySelectorAll('form'); - const formData = {}; - - forms.forEach((form, index) => { - const data = new FormData(form); - formData[`form-${index}`] = Object.fromEntries(data); - }); - - return formData; - } - - restoreFormData(formData) { - // 恢复表单数据 - Object.entries(formData).forEach(([formId, data]) => { - const formIndex = parseInt(formId.split('-')[1]); - const form = document.querySelectorAll('form')[formIndex]; - - if (form) { - Object.entries(data).forEach(([name, value]) => { - const input = form.querySelector(`[name="${name}"]`); - if (input) { - input.value = value; - } - }); - } - }); - } - - getUserSelections() { - // 获取用户选择状态 - return { - selectedItems: Array.from(document.querySelectorAll('.selected')).map(el => el.id), - activeTab: document.querySelector('.tab.active')?.id - }; - } - - restoreUserSelections(selections) { - // 恢复用户选择 - if (selections.selectedItems) { - selections.selectedItems.forEach(id => { - const element = document.getElementById(id); - if (element) { - element.classList.add('selected'); - } - }); - } - - if (selections.activeTab) { - const tab = document.getElementById(selections.activeTab); - if (tab) { - tab.classList.add('active'); - } - } - } -} -``` - -### 6. 使用方法 - -#### 在应用中集成 -```javascript -// 在应用启动时初始化状态管理 -document.addEventListener('DOMContentLoaded', () => { - const stateController = new PWAStateController(); - - // 监听状态恢复事件 - window.addEventListener('stateRestored', (event) => { - console.log('状态已恢复:', event.detail.state); - // 执行状态恢复后的处理 - }); -}); - -// 在关键操作后手动保存状态 -function onImportantUserAction() { - if (window.stateController) { - window.stateController.saveCurrentState(); - } -} -``` - -### 7. 最佳实践 - -1. **分层存储策略**: - - localStorage:持久性强,用于核心状态 - - sessionStorage:会话级别,用于临时状态 - - IndexedDB:大量数据存储 - - Service Worker缓存:跨页面共享状态 - -2. **智能状态恢复**: - - 检查状态年龄 - - 验证URL匹配 - - 考虑设备方向变化 - - 处理异常情况 - -3. **性能优化**: - - 避免频繁保存状态 - - 使用防抖技术 - - 异步处理状态操作 - - 清理过期状态 - -4. **错误处理**: - - 多重保存策略 - - 降级处理 - - 日志记录 - - 用户通知 - -### 8. 注意事项 - -1. **存储限制**: - - localStorage: 约5-10MB - - sessionStorage: 约5-10MB - - IndexedDB: 较大容量但可能被清理 - - Service Worker缓存: 约50MB - -2. **iOS特殊性**: - - PWA不与Safari共享存储 - - 后台执行时间有限 - - 内存压力时会被清理 - -3. **用户体验**: - - 提供状态恢复指示 - - 处理恢复失败情况 - - 保持操作流畅性 - -这个完整的解决方案应该能够显著改善PWA在iOS上的状态恢复体验,减少用户因为后台被杀而丢失状态的问题。 \ No newline at end of file diff --git a/package.json b/package.json index ee40bc8a..02d4eac8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moviepilot", - "version": "2.6.2", + "version": "2.6.3", "private": true, "type": "module", "bin": "dist/service.js", diff --git a/src/components/PWAStateDemo.vue b/src/components/PWAStateDemo.vue deleted file mode 100644 index e87199cd..00000000 --- a/src/components/PWAStateDemo.vue +++ /dev/null @@ -1,185 +0,0 @@ - - - - - \ No newline at end of file