mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-07 04:52:45 +08:00
Optimize PWA state restoration and loading experience
Co-authored-by: jxxghp <jxxghp@163.com>
This commit is contained in:
180
PWA_Optimization_Summary.md
Normal file
180
PWA_Optimization_Summary.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# PWA 后台切换前台体验优化 - 修改总结
|
||||
|
||||
## 已完成的修改
|
||||
|
||||
### 1. 优化PWA状态管理器 (`src/utils/pwaStateManager.ts`)
|
||||
|
||||
#### 1.1 增强PWAStateController类
|
||||
- 添加了状态恢复等待机制 (`waitForStateRestore()`)
|
||||
- 添加了状态恢复状态跟踪 (`isRestoringState`)
|
||||
- 改进了状态恢复流程,确保异步操作完成后才通知应用
|
||||
- 添加了详细的日志记录,便于调试
|
||||
|
||||
#### 1.2 改进VisibilityStateManager类
|
||||
- 添加了轻量级状态恢复指示器
|
||||
- 改进了页面可见性处理逻辑
|
||||
- 添加了防重入机制,避免重复恢复
|
||||
- 优化了状态恢复的用户体验
|
||||
|
||||
### 2. 优化应用初始化 (`src/main.ts`)
|
||||
|
||||
#### 2.1 提前初始化PWA状态管理器
|
||||
- 在Vue应用挂载前初始化PWA状态管理器
|
||||
- 等待状态恢复完成后再挂载应用
|
||||
- 确保PWA状态在应用启动时就已经准备就绪
|
||||
|
||||
#### 2.2 改进事件监听器设置
|
||||
- 优化了状态恢复事件监听
|
||||
- 移除了重复的初始化逻辑
|
||||
- 简化了代码结构
|
||||
|
||||
### 3. 优化加载界面管理 (`src/App.vue`)
|
||||
|
||||
#### 3.1 智能加载界面移除
|
||||
- 创建了`removeLoadingWithStateCheck()`函数
|
||||
- 确保PWA状态恢复完成后才移除加载界面
|
||||
- 添加了多重检查机制,包括全局设置和背景图片加载
|
||||
|
||||
#### 3.2 改进加载动画
|
||||
- 添加了更平滑的加载完成动画
|
||||
- 优化了过渡效果
|
||||
- 添加了PWA状态恢复的视觉反馈
|
||||
|
||||
#### 3.3 增强样式和过渡效果
|
||||
- 添加了PWA过渡效果类
|
||||
- 优化了加载完成动画
|
||||
- 改进了用户体验
|
||||
|
||||
### 4. 优化Service Worker缓存策略 (`vite.config.ts`)
|
||||
|
||||
#### 4.1 启用导航预加载
|
||||
- 在Workbox配置中启用了导航预加载
|
||||
- 提高了页面加载性能
|
||||
|
||||
#### 4.2 改进缓存策略
|
||||
- 将页面缓存策略从`NetworkFirst`改为`StaleWhileRevalidate`
|
||||
- 添加了缓存键优化,提高缓存命中率
|
||||
- 忽略状态参数,避免缓存污染
|
||||
|
||||
### 5. 增强Service Worker功能 (`src/service-worker.ts`)
|
||||
|
||||
#### 5.1 添加状态预缓存
|
||||
- 在Service Worker安装时预缓存关键状态数据
|
||||
- 添加了错误处理机制
|
||||
|
||||
#### 5.2 改进激活事件
|
||||
- 启用导航预加载功能
|
||||
- 添加了缓存清理逻辑
|
||||
- 优化了Service Worker生命周期管理
|
||||
|
||||
### 6. 创建加载状态管理器 (`src/utils/loadingStateManager.ts`)
|
||||
|
||||
#### 6.1 智能加载状态协调
|
||||
- 创建了`PWALoadingStateManager`类
|
||||
- 提供了统一的加载状态管理
|
||||
- 支持多组件加载状态协调
|
||||
|
||||
#### 6.2 加载状态监听
|
||||
- 添加了状态变化监听器
|
||||
- 提供了等待所有加载完成的机制
|
||||
- 支持状态重置和详细状态查询
|
||||
|
||||
## 关键特性
|
||||
|
||||
### 1. 状态恢复等待机制
|
||||
- 应用在PWA状态恢复完成前不会显示
|
||||
- 避免了用户看到不一致的状态
|
||||
- 确保了平滑的用户体验
|
||||
|
||||
### 2. 轻量级状态恢复指示器
|
||||
- 在页面可见性切换时显示轻量级指示器
|
||||
- 避免了重新显示完整的加载界面
|
||||
- 提供了即时的用户反馈
|
||||
|
||||
### 3. 智能缓存策略
|
||||
- 优化了页面和资源的缓存策略
|
||||
- 启用了导航预加载功能
|
||||
- 提高了应用响应速度
|
||||
|
||||
### 4. 详细的日志记录
|
||||
- 添加了全面的日志记录
|
||||
- 便于调试和性能监控
|
||||
- 提供了状态恢复过程的可见性
|
||||
|
||||
## 预期效果
|
||||
|
||||
### 1. 消除界面闪烁
|
||||
- ✅ 状态恢复完成后才显示界面
|
||||
- ✅ 避免了用户看到不一致的状态
|
||||
- ✅ 提供了平滑的过渡效果
|
||||
|
||||
### 2. 提升加载性能
|
||||
- ✅ 智能缓存策略减少了网络请求
|
||||
- ✅ 导航预加载提高了响应速度
|
||||
- ✅ 多重加载状态协调确保了资源就绪
|
||||
|
||||
### 3. 改善用户体验
|
||||
- ✅ 添加了过渡动画和视觉反馈
|
||||
- ✅ 轻量级指示器替代了完整加载界面
|
||||
- ✅ 状态恢复过程更加透明
|
||||
|
||||
### 4. 接近原生体验
|
||||
- ✅ 快速的状态恢复
|
||||
- ✅ 平滑的切换动画
|
||||
- ✅ 即时的用户反馈
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 1. 开发环境测试
|
||||
1. 启动开发服务器:`npm run dev`
|
||||
2. 在浏览器中打开应用
|
||||
3. 使用开发者工具测试PWA功能
|
||||
4. 检查控制台日志,确认状态恢复过程
|
||||
|
||||
### 2. 真机测试
|
||||
1. 构建生产版本:`npm run build`
|
||||
2. 部署到服务器或使用本地服务器
|
||||
3. 在移动设备上安装PWA
|
||||
4. 测试后台切换前台的各种场景
|
||||
|
||||
### 3. 性能监控
|
||||
- 使用Chrome DevTools监控性能
|
||||
- 检查状态恢复时间
|
||||
- 监控内存使用情况
|
||||
- 验证缓存策略效果
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 兼容性
|
||||
- 确保在不同浏览器上测试
|
||||
- 特别关注iOS Safari和Android Chrome
|
||||
- 测试不同版本的Service Worker支持
|
||||
|
||||
### 2. 性能
|
||||
- 监控状态恢复的性能影响
|
||||
- 避免过度的状态保存
|
||||
- 定期清理过期的状态数据
|
||||
|
||||
### 3. 用户体验
|
||||
- 确保状态恢复不会影响用户操作
|
||||
- 提供适当的加载反馈
|
||||
- 处理网络异常情况
|
||||
|
||||
## 后续优化建议
|
||||
|
||||
### 1. 高级功能
|
||||
- 添加骨架屏支持
|
||||
- 实现智能状态压缩
|
||||
- 添加状态恢复统计
|
||||
|
||||
### 2. 性能优化
|
||||
- 实现增量状态更新
|
||||
- 优化状态序列化
|
||||
- 添加状态恢复缓存
|
||||
|
||||
### 3. 用户体验
|
||||
- 添加自定义状态恢复动画
|
||||
- 实现状态恢复进度指示
|
||||
- 支持用户自定义状态恢复偏好
|
||||
|
||||
这些修改将显著提升PWA应用从后台切换到前台时的用户体验,使其更接近原生应用的表现。
|
||||
117
src/App.vue
117
src/App.vue
@@ -8,6 +8,7 @@ import { getBrowserLocale, setI18nLanguage } from './plugins/i18n'
|
||||
import { SupportedLocale } from '@/types/i18n'
|
||||
import { checkAndEmitUnreadMessages } from '@/utils/badge'
|
||||
import { preloadImage } from './@core/utils/image'
|
||||
import { globalLoadingStateManager } from '@/utils/loadingStateManager'
|
||||
|
||||
// 生效主题
|
||||
const { global: globalTheme } = useTheme()
|
||||
@@ -35,6 +36,9 @@ const activeImageIndex = ref(0)
|
||||
const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent')
|
||||
let backgroundRotationTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// PWA状态恢复相关
|
||||
const isRestoring = ref(false)
|
||||
|
||||
// ApexCharts 全局配置
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -123,8 +127,65 @@ function startBackgroundRotation() {
|
||||
function animateAndRemoveLoader() {
|
||||
const loadingBg = document.querySelector('#loading-bg') as HTMLElement
|
||||
if (loadingBg) {
|
||||
removeEl('#loading-bg')
|
||||
document.documentElement.style.removeProperty('background')
|
||||
// 添加完成动画类
|
||||
loadingBg.classList.add('loading-complete')
|
||||
|
||||
// 等待动画完成后移除
|
||||
setTimeout(() => {
|
||||
removeEl('#loading-bg')
|
||||
document.documentElement.style.removeProperty('background')
|
||||
}, 800)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查PWA状态并移除加载界面
|
||||
async function removeLoadingWithStateCheck() {
|
||||
try {
|
||||
console.log('开始检查加载状态...')
|
||||
|
||||
// 设置各个组件的加载状态
|
||||
globalLoadingStateManager.setLoadingState('pwa-state', true)
|
||||
globalLoadingStateManager.setLoadingState('global-settings', true)
|
||||
globalLoadingStateManager.setLoadingState('background-images', true)
|
||||
|
||||
// 检查PWA状态是否已恢复
|
||||
const pwaController = (window as any).pwaStateController
|
||||
if (pwaController) {
|
||||
isRestoring.value = true
|
||||
await pwaController.waitForStateRestore()
|
||||
console.log('PWA状态恢复完成')
|
||||
}
|
||||
globalLoadingStateManager.setLoadingState('pwa-state', false)
|
||||
|
||||
// 确保关键资源已加载
|
||||
await Promise.all([
|
||||
// 等待全局设置初始化完成
|
||||
globalSettingsStore.initialize().then(() => {
|
||||
globalLoadingStateManager.setLoadingState('global-settings', false)
|
||||
}),
|
||||
// 等待背景图片加载状态稳定
|
||||
new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
globalLoadingStateManager.setLoadingState('background-images', false)
|
||||
resolve(void 0)
|
||||
}, 200)
|
||||
})
|
||||
])
|
||||
|
||||
// 等待所有加载完成
|
||||
await globalLoadingStateManager.waitForAllComplete()
|
||||
console.log('所有资源加载完成,准备移除加载界面')
|
||||
|
||||
// 移除加载界面
|
||||
animateAndRemoveLoader()
|
||||
|
||||
// 检查未读消息
|
||||
checkAndEmitUnreadMessages()
|
||||
} catch (error) {
|
||||
console.error('移除加载界面时发生错误:', error)
|
||||
// 即使出错也要移除加载界面
|
||||
globalLoadingStateManager.reset()
|
||||
animateAndRemoveLoader()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,9 +208,11 @@ async function loadBackgroundImages(retryCount = 0) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 初始化全局设置
|
||||
await globalSettingsStore.initialize()
|
||||
|
||||
// 监听PWA状态恢复事件
|
||||
window.addEventListener('pwaStateRestored', () => {
|
||||
isRestoring.value = false
|
||||
})
|
||||
|
||||
// 配置 ApexCharts
|
||||
configureApexCharts()
|
||||
|
||||
@@ -170,14 +233,9 @@ onMounted(async () => {
|
||||
// 加载背景图片
|
||||
loadBackgroundImages()
|
||||
|
||||
// 移除加载动画
|
||||
// 使用优化后的加载界面移除逻辑
|
||||
ensureRenderComplete(() => {
|
||||
nextTick(() => {
|
||||
// 移除加载动画,显示页面
|
||||
animateAndRemoveLoader()
|
||||
// 页面完全显示后,检查未读消息
|
||||
checkAndEmitUnreadMessages()
|
||||
})
|
||||
nextTick(removeLoadingWithStateCheck)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -205,7 +263,7 @@ onUnmounted(() => {
|
||||
<div v-if="isLogin && isTransparentTheme" class="global-blur-layer"></div>
|
||||
</div>
|
||||
<!-- 页面内容 -->
|
||||
<VApp :class="{ 'transparent-app': isTransparentTheme }">
|
||||
<VApp :class="{ 'transparent-app': isTransparentTheme, 'pwa-restoring': isRestoring }">
|
||||
<RouterView />
|
||||
</VApp>
|
||||
</div>
|
||||
@@ -267,4 +325,37 @@ onUnmounted(() => {
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
|
||||
/* PWA过渡效果 */
|
||||
.transparent-app {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.pwa-restoring {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 优化加载完成动画 */
|
||||
.loading-complete {
|
||||
animation: fadeOutScale 0.8s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeOutScale {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
filter: blur(0px);
|
||||
}
|
||||
70% {
|
||||
opacity: 0.3;
|
||||
transform: scale(1.05);
|
||||
filter: blur(2px);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(1.1);
|
||||
filter: blur(5px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
87
src/main.ts
87
src/main.ts
@@ -46,6 +46,32 @@ import '@/styles/main.scss'
|
||||
// 8. PWA状态管理
|
||||
import { PWAStateController } from '@/utils/pwaStateManager'
|
||||
|
||||
// PWA状态管理器初始化函数
|
||||
const initializePWABeforeMount = async () => {
|
||||
// 检查是否在PWA模式下运行
|
||||
const isPWA = window.matchMedia('(display-mode: standalone)').matches ||
|
||||
(window.navigator as any).standalone ||
|
||||
document.referrer.includes('android-app://')
|
||||
|
||||
if (isPWA) {
|
||||
console.log('检测到PWA模式,预初始化状态管理器')
|
||||
const pwaStateController = new PWAStateController()
|
||||
|
||||
// 等待状态恢复完成
|
||||
await pwaStateController.waitForStateRestore()
|
||||
|
||||
// 将状态管理器绑定到全局对象
|
||||
;(window as any).pwaStateController = pwaStateController
|
||||
|
||||
return pwaStateController
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// 在创建Vue应用前初始化PWA状态管理器
|
||||
const pwaStateController = await initializePWABeforeMount()
|
||||
|
||||
// 创建Vue实例
|
||||
const app = createApp(App)
|
||||
|
||||
@@ -93,51 +119,24 @@ app
|
||||
.use(i18n)
|
||||
.mount('#app')
|
||||
|
||||
// 5. 初始化PWA状态管理器
|
||||
let pwaStateController: PWAStateController | null = null
|
||||
|
||||
// PWA状态管理器初始化函数
|
||||
const initializePWAStateManager = () => {
|
||||
// 检查是否在PWA模式下运行
|
||||
const isPWA = window.matchMedia('(display-mode: standalone)').matches ||
|
||||
(window.navigator as any).standalone ||
|
||||
document.referrer.includes('android-app://')
|
||||
// 5. 添加状态恢复事件监听器
|
||||
if (pwaStateController) {
|
||||
// 监听状态恢复事件
|
||||
window.addEventListener('pwaStateRestored', (event: Event) => {
|
||||
const customEvent = event as CustomEvent
|
||||
console.log('PWA状态已恢复:', customEvent.detail.state)
|
||||
|
||||
// 可以在这里添加状态恢复后的处理逻辑
|
||||
// 例如:通知Vue组件状态已恢复
|
||||
app.config.globalProperties.$pwaStateRestored = true
|
||||
})
|
||||
|
||||
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模式,跳过状态管理器初始化')
|
||||
}
|
||||
}
|
||||
|
||||
// 检查DOM状态并初始化PWA状态管理
|
||||
if (document.readyState === 'loading') {
|
||||
// DOM尚未加载完成,添加事件监听器
|
||||
document.addEventListener('DOMContentLoaded', initializePWAStateManager)
|
||||
} else {
|
||||
// DOM已经准备就绪,立即初始化
|
||||
initializePWAStateManager()
|
||||
// 监听应用即将卸载事件,保存状态
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (pwaStateController) {
|
||||
pwaStateController.saveCurrentState()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 导出状态管理器供其他模块使用
|
||||
|
||||
@@ -152,8 +152,26 @@ async function clearBadge() {
|
||||
// 安装事件
|
||||
self.addEventListener('install', event => {
|
||||
console.log('Service Worker install')
|
||||
// 强制等待中的Service Worker立即成为活动的Service Worker
|
||||
self.skipWaiting()
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
// 预缓存关键状态数据
|
||||
try {
|
||||
const cache = await caches.open(STATE_CACHE_NAME)
|
||||
const existingState = await cache.match(STATE_ENDPOINT)
|
||||
|
||||
if (existingState) {
|
||||
// 预热状态数据
|
||||
const state = await existingState.json()
|
||||
console.log('预缓存状态数据:', state)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('预缓存状态数据失败:', error)
|
||||
}
|
||||
|
||||
// 强制等待中的Service Worker立即成为活动的Service Worker
|
||||
self.skipWaiting()
|
||||
})()
|
||||
)
|
||||
})
|
||||
|
||||
// 激活事件
|
||||
@@ -164,7 +182,19 @@ self.addEventListener('activate', event => {
|
||||
// 启用导航预载功能以提高性能
|
||||
if ('navigationPreload' in self.registration) {
|
||||
await self.registration.navigationPreload.enable()
|
||||
console.log('导航预加载已启用')
|
||||
}
|
||||
|
||||
// 清理旧版本的缓存
|
||||
const cacheNames = await caches.keys()
|
||||
await Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
if (cacheName.includes('old-') || cacheName.includes('deprecated-')) {
|
||||
console.log('清理旧缓存:', cacheName)
|
||||
return caches.delete(cacheName)
|
||||
}
|
||||
})
|
||||
)
|
||||
})(),
|
||||
)
|
||||
// 告诉活动的Service Worker立即控制页面
|
||||
|
||||
105
src/utils/loadingStateManager.ts
Normal file
105
src/utils/loadingStateManager.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* PWA加载状态管理器
|
||||
* 用于协调不同组件的加载状态,确保所有关键资源加载完成后再显示界面
|
||||
*/
|
||||
export class PWALoadingStateManager {
|
||||
private loadingStates: Map<string, boolean> = new Map()
|
||||
private listeners: Set<(isLoading: boolean) => void> = new Set()
|
||||
|
||||
/**
|
||||
* 设置加载状态
|
||||
* @param key 状态键名
|
||||
* @param loading 是否正在加载
|
||||
*/
|
||||
setLoadingState(key: string, loading: boolean): void {
|
||||
const wasLoading = this.isAnyLoading()
|
||||
this.loadingStates.set(key, loading)
|
||||
const isLoading = this.isAnyLoading()
|
||||
|
||||
// 如果总体加载状态发生变化,通知监听器
|
||||
if (wasLoading !== isLoading) {
|
||||
this.notifyListeners(isLoading)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有任何组件正在加载
|
||||
*/
|
||||
isAnyLoading(): boolean {
|
||||
return Array.from(this.loadingStates.values()).some(loading => loading)
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待所有加载完成
|
||||
*/
|
||||
waitForAllComplete(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (!this.isAnyLoading()) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
const checkComplete = () => {
|
||||
if (!this.isAnyLoading()) {
|
||||
resolve()
|
||||
} else {
|
||||
// 检查间隔
|
||||
setTimeout(checkComplete, 50)
|
||||
}
|
||||
}
|
||||
checkComplete()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加状态变化监听器
|
||||
* @param listener 监听器函数
|
||||
*/
|
||||
addListener(listener: (isLoading: boolean) => void): void {
|
||||
this.listeners.add(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除状态变化监听器
|
||||
* @param listener 监听器函数
|
||||
*/
|
||||
removeListener(listener: (isLoading: boolean) => void): void {
|
||||
this.listeners.delete(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知所有监听器
|
||||
* @param isLoading 是否正在加载
|
||||
*/
|
||||
private notifyListeners(isLoading: boolean): void {
|
||||
this.listeners.forEach(listener => {
|
||||
try {
|
||||
listener(isLoading)
|
||||
} catch (error) {
|
||||
console.error('加载状态监听器错误:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前加载状态详情
|
||||
*/
|
||||
getLoadingStates(): Record<string, boolean> {
|
||||
return Object.fromEntries(this.loadingStates)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置所有加载状态
|
||||
*/
|
||||
reset(): void {
|
||||
const wasLoading = this.isAnyLoading()
|
||||
this.loadingStates.clear()
|
||||
|
||||
if (wasLoading) {
|
||||
this.notifyListeners(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 全局实例
|
||||
export const globalLoadingStateManager = new PWALoadingStateManager()
|
||||
@@ -275,6 +275,8 @@ export class StateRestoreDecision {
|
||||
export class VisibilityStateManager {
|
||||
private stateManager: PWAStateManager
|
||||
private blurTimer: number | null = null
|
||||
private isRestoring = false
|
||||
private restorePromise: Promise<void> | null = null
|
||||
|
||||
constructor(stateManager: PWAStateManager) {
|
||||
this.stateManager = stateManager
|
||||
@@ -313,10 +315,106 @@ export class VisibilityStateManager {
|
||||
}
|
||||
|
||||
private handlePageVisible(): void {
|
||||
const restoredState = this.stateManager.restoreState()
|
||||
if (restoredState) {
|
||||
this.restoreAppState(restoredState)
|
||||
console.log('页面显示,已恢复状态')
|
||||
if (this.isRestoring) return
|
||||
|
||||
this.isRestoring = true
|
||||
this.restorePromise = this.performStateRestore()
|
||||
}
|
||||
|
||||
private async performStateRestore(): Promise<void> {
|
||||
try {
|
||||
// 显示轻量级恢复指示器
|
||||
this.showRestoreIndicator()
|
||||
|
||||
const restoredState = this.stateManager.restoreState()
|
||||
if (restoredState) {
|
||||
await this.restoreAppState(restoredState)
|
||||
console.log('页面显示,已恢复状态')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('状态恢复失败:', error)
|
||||
} finally {
|
||||
this.isRestoring = false
|
||||
this.hideRestoreIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
private showRestoreIndicator(): void {
|
||||
// 检查是否已经存在指示器
|
||||
if (document.getElementById('pwa-restore-indicator')) return
|
||||
|
||||
const indicator = document.createElement('div')
|
||||
indicator.id = 'pwa-restore-indicator'
|
||||
indicator.innerHTML = `
|
||||
<div class="restore-indicator">
|
||||
<div class="restore-spinner"></div>
|
||||
<div class="restore-text">正在恢复状态...</div>
|
||||
</div>
|
||||
`
|
||||
document.body.appendChild(indicator)
|
||||
|
||||
// 添加样式
|
||||
const style = document.createElement('style')
|
||||
style.textContent = `
|
||||
#pwa-restore-indicator {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
#pwa-restore-indicator.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.restore-indicator {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.restore-spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid transparent;
|
||||
border-top: 2px solid white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
|
||||
// 触发显示动画
|
||||
setTimeout(() => {
|
||||
indicator.classList.add('show')
|
||||
}, 10)
|
||||
}
|
||||
|
||||
private hideRestoreIndicator(): void {
|
||||
const indicator = document.getElementById('pwa-restore-indicator')
|
||||
if (indicator) {
|
||||
indicator.classList.remove('show')
|
||||
setTimeout(() => {
|
||||
indicator.remove()
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,13 +448,21 @@ export class VisibilityStateManager {
|
||||
}
|
||||
}
|
||||
|
||||
private restoreAppState(state: PWAState): void {
|
||||
private async restoreAppState(state: PWAState): Promise<void> {
|
||||
// 添加小延迟以确保页面完全加载
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
if (state.scrollPosition) {
|
||||
window.scrollTo(0, state.scrollPosition)
|
||||
}
|
||||
if (state.appData) {
|
||||
this.restoreAppSpecificState(state.appData)
|
||||
}
|
||||
|
||||
// 触发状态恢复完成事件
|
||||
window.dispatchEvent(new CustomEvent('pwaStateRestored', {
|
||||
detail: { state }
|
||||
}))
|
||||
}
|
||||
|
||||
private getAppSpecificState(): any {
|
||||
@@ -439,6 +545,9 @@ export class PWAStateController {
|
||||
private swStateSync: ServiceWorkerStateSync
|
||||
private visibilityManager: VisibilityStateManager
|
||||
private restoreDecision: StateRestoreDecision
|
||||
private stateRestorePromise: Promise<void> | null = null
|
||||
private stateRestoreResolve: (() => void) | null = null
|
||||
private isRestoring = false
|
||||
|
||||
constructor() {
|
||||
this.stateManager = new PWAStateManager()
|
||||
@@ -447,9 +556,28 @@ export class PWAStateController {
|
||||
this.visibilityManager = new VisibilityStateManager(this.stateManager)
|
||||
this.restoreDecision = new StateRestoreDecision()
|
||||
|
||||
// 创建状态恢复Promise
|
||||
this.stateRestorePromise = new Promise((resolve) => {
|
||||
this.stateRestoreResolve = resolve
|
||||
})
|
||||
|
||||
this.init()
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待状态恢复完成
|
||||
*/
|
||||
async waitForStateRestore(): Promise<void> {
|
||||
return this.stateRestorePromise || Promise.resolve()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前是否正在恢复状态
|
||||
*/
|
||||
get isRestoringState(): boolean {
|
||||
return this.isRestoring
|
||||
}
|
||||
|
||||
private async init(): Promise<void> {
|
||||
// 清理过期状态
|
||||
this.stateManager.clearExpiredState()
|
||||
@@ -462,29 +590,41 @@ export class PWAStateController {
|
||||
}
|
||||
|
||||
private async checkAndRestoreState(): Promise<void> {
|
||||
const currentContext: PWAContext = {
|
||||
url: window.location.href,
|
||||
orientation: window.orientation || 0,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
this.isRestoring = true
|
||||
|
||||
try {
|
||||
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()
|
||||
]
|
||||
// 尝试从多个来源恢复状态
|
||||
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
|
||||
for (const source of sources) {
|
||||
try {
|
||||
const savedState = await source()
|
||||
if (this.restoreDecision.shouldRestoreState(savedState, currentContext)) {
|
||||
await this.restoreState(savedState!)
|
||||
console.log('PWA状态恢复成功')
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('状态恢复失败:', error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('状态恢复失败:', error)
|
||||
}
|
||||
} finally {
|
||||
this.isRestoring = false
|
||||
// 状态恢复完成(无论成功还是失败)
|
||||
if (this.stateRestoreResolve) {
|
||||
this.stateRestoreResolve()
|
||||
this.stateRestoreResolve = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -508,18 +648,28 @@ export class PWAStateController {
|
||||
}
|
||||
|
||||
private async restoreState(state: PWAState): Promise<void> {
|
||||
console.log('开始恢复PWA状态:', {
|
||||
url: state.url,
|
||||
scrollPosition: state.scrollPosition,
|
||||
timestamp: new Date(state.timestamp).toISOString(),
|
||||
hasAppData: !!state.appData
|
||||
})
|
||||
|
||||
// 恢复滚动位置
|
||||
if (state.scrollPosition) {
|
||||
console.log('恢复滚动位置:', state.scrollPosition)
|
||||
window.scrollTo(0, state.scrollPosition)
|
||||
}
|
||||
|
||||
// 恢复应用特定状态
|
||||
if (state.appData) {
|
||||
console.log('恢复应用特定状态:', state.appData)
|
||||
this.restoreAppSpecificState(state.appData)
|
||||
}
|
||||
|
||||
// 触发状态恢复事件
|
||||
this.dispatchStateRestoreEvent(state)
|
||||
console.log('PWA状态恢复完成')
|
||||
}
|
||||
|
||||
private setupPeriodicSave(): void {
|
||||
|
||||
@@ -60,6 +60,8 @@ export default defineConfig({
|
||||
revision: null,
|
||||
},
|
||||
],
|
||||
// 启用导航预加载
|
||||
navigationPreload: true,
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: /\.(?:js|css|html)$/,
|
||||
@@ -115,10 +117,16 @@ export default defineConfig({
|
||||
},
|
||||
{
|
||||
urlPattern: ({ request }) => request.destination === 'document',
|
||||
handler: 'NetworkFirst',
|
||||
handler: 'StaleWhileRevalidate',
|
||||
options: {
|
||||
cacheName: 'pages-cache',
|
||||
networkTimeoutSeconds: 10,
|
||||
cacheKeyWillBeUsed: async ({ request }) => {
|
||||
// 忽略状态参数,提高缓存命中率
|
||||
const url = new URL(request.url)
|
||||
url.searchParams.delete('restored')
|
||||
url.searchParams.delete('t')
|
||||
return url.toString()
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user