mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-25 17:44:13 +08:00
feat: 实现PWA状态管理防止iOS后台被杀
- 添加多层存储策略(localStorage + sessionStorage + IndexedDB + Service Worker缓存) - 实现智能状态恢复决策机制 - 自动监听页面生命周期事件进行状态保存和恢复 - 支持表单数据、滚动位置、UI状态的自动保存 - 专为iOS设备PWA优化,解决后台被杀导致状态丢失的问题 - 版本号更新至 2.6.3
This commit is contained in:
@@ -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
|
||||
<script setup lang="ts">
|
||||
import { usePWAState } from '@/composables/usePWAState'
|
||||
|
||||
const {
|
||||
isPWAMode,
|
||||
isStateRestored,
|
||||
saveCurrentState,
|
||||
resetStateRestored
|
||||
} = usePWAState()
|
||||
|
||||
// 手动保存状态
|
||||
const handleImportantAction = async () => {
|
||||
await saveCurrentState()
|
||||
// 执行重要操作
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VAlert v-if="isStateRestored" type="success">
|
||||
状态已恢复!
|
||||
</VAlert>
|
||||
|
||||
<VBtn @click="handleImportantAction">
|
||||
执行重要操作
|
||||
</VBtn>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 手动操作状态
|
||||
|
||||
```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设备上提供更好的用户体验,显著减少因后台被杀而导致的状态丢失问题。
|
||||
@@ -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上的状态恢复体验,减少用户因为后台被杀而丢失状态的问题。
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "2.6.2",
|
||||
"version": "2.6.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"bin": "dist/service.js",
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
<template>
|
||||
<VCard class="ma-4" title="PWA状态管理演示">
|
||||
<VCardText>
|
||||
<VAlert
|
||||
v-if="isPWAMode"
|
||||
type="success"
|
||||
class="mb-4"
|
||||
>
|
||||
<VIcon icon="mdi-check-circle" class="me-2" />
|
||||
检测到PWA模式,状态管理功能已启用
|
||||
</VAlert>
|
||||
|
||||
<VAlert
|
||||
v-else
|
||||
type="info"
|
||||
class="mb-4"
|
||||
>
|
||||
<VIcon icon="mdi-information" class="me-2" />
|
||||
当前在浏览器模式,请添加到桌面后体验状态管理功能
|
||||
</VAlert>
|
||||
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VCard variant="outlined">
|
||||
<VCardTitle>状态信息</VCardTitle>
|
||||
<VCardText>
|
||||
<VList density="compact">
|
||||
<VListItem>
|
||||
<VListItemTitle>PWA模式</VListItemTitle>
|
||||
<VListItemSubtitle>{{ isPWAMode ? '是' : '否' }}</VListItemSubtitle>
|
||||
</VListItem>
|
||||
<VListItem>
|
||||
<VListItemTitle>状态管理器可用</VListItemTitle>
|
||||
<VListItemSubtitle>{{ isStateManagerAvailable() ? '是' : '否' }}</VListItemSubtitle>
|
||||
</VListItem>
|
||||
<VListItem>
|
||||
<VListItemTitle>状态恢复次数</VListItemTitle>
|
||||
<VListItemSubtitle>{{ stateRestoreCount }}</VListItemSubtitle>
|
||||
</VListItem>
|
||||
<VListItem v-if="isStateRestored">
|
||||
<VListItemTitle>最后恢复时间</VListItemTitle>
|
||||
<VListItemSubtitle>{{ lastRestoredState?.timestamp ? new Date(lastRestoredState.timestamp).toLocaleString() : '无' }}</VListItemSubtitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<VCard variant="outlined">
|
||||
<VCardTitle>操作面板</VCardTitle>
|
||||
<VCardText class="d-flex flex-column ga-3">
|
||||
<VBtn
|
||||
@click="saveCurrentState"
|
||||
:disabled="!isStateManagerAvailable()"
|
||||
color="primary"
|
||||
prepend-icon="mdi-content-save"
|
||||
>
|
||||
手动保存状态
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
@click="checkStateRestore"
|
||||
:disabled="!isStateManagerAvailable()"
|
||||
color="secondary"
|
||||
prepend-icon="mdi-restore"
|
||||
>
|
||||
检查状态恢复
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
@click="clearStoredState"
|
||||
color="warning"
|
||||
prepend-icon="mdi-delete"
|
||||
>
|
||||
清除存储状态
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
@click="resetStateRestored"
|
||||
v-if="isStateRestored"
|
||||
color="info"
|
||||
prepend-icon="mdi-refresh"
|
||||
>
|
||||
重置恢复标志
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<!-- 测试表单 -->
|
||||
<VCard variant="outlined" class="mt-4">
|
||||
<VCardTitle>测试表单(用于验证状态恢复)</VCardTitle>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="testForm.name"
|
||||
label="姓名"
|
||||
name="test-name"
|
||||
persistent-hint
|
||||
hint="切换应用后再回来,这个值应该被恢复"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="testForm.email"
|
||||
label="邮箱"
|
||||
name="test-email"
|
||||
type="email"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="testForm.message"
|
||||
label="消息"
|
||||
name="test-message"
|
||||
rows="3"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 状态恢复提示 -->
|
||||
<VAlert
|
||||
v-if="isStateRestored"
|
||||
type="success"
|
||||
class="mt-4"
|
||||
closable
|
||||
@click:close="resetStateRestored"
|
||||
>
|
||||
<VIcon icon="mdi-check-circle" class="me-2" />
|
||||
状态已成功恢复!滚动位置和表单数据应该已经恢复到之前的状态。
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { usePWAState, useGlobalPWAState } from '@/composables/usePWAState'
|
||||
|
||||
// 使用PWA状态管理
|
||||
const {
|
||||
isPWAMode,
|
||||
isStateRestored,
|
||||
stateRestoreCount,
|
||||
lastRestoredState,
|
||||
saveCurrentState,
|
||||
checkStateRestore,
|
||||
resetStateRestored,
|
||||
isStateManagerAvailable
|
||||
} = usePWAState()
|
||||
|
||||
// 使用全局PWA状态管理
|
||||
const { clearStoredState } = useGlobalPWAState()
|
||||
|
||||
// 测试表单数据
|
||||
const testForm = ref({
|
||||
name: '',
|
||||
email: '',
|
||||
message: ''
|
||||
})
|
||||
|
||||
// 监听状态恢复事件,恢复表单数据
|
||||
watch(isStateRestored, (restored) => {
|
||||
if (restored && lastRestoredState.value?.appData?.formState) {
|
||||
console.log('检测到状态恢复,尝试恢复表单数据')
|
||||
// 这里可以添加更复杂的表单数据恢复逻辑
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
console.log('PWA状态演示组件已挂载')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user