Refactor PWA state management to simplify and streamline implementation

Co-authored-by: jxxghp <jxxghp@163.com>
This commit is contained in:
Cursor Agent
2025-07-06 23:24:31 +00:00
co-authored by jxxghp
parent 304b990994
commit a81071a50a
4 changed files with 41 additions and 192 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# PWA轻量级状态管理实现总结 # PWA状态管理实现总结
## 问题分析 ## 问题分析
-7
View File
@@ -146,14 +146,9 @@ if (pwaStateController) {
} }
// 6. 初始化后台优化工具 // 6. 初始化后台优化工具
console.log('初始化后台优化工具...')
// 将后台管理器绑定到全局对象(便于调试)
if (import.meta.env.MODE === 'development') { if (import.meta.env.MODE === 'development') {
;(window as any).backgroundManager = backgroundManager ;(window as any).backgroundManager = backgroundManager
;(window as any).sseManagerSingleton = sseManagerSingleton ;(window as any).sseManagerSingleton = sseManagerSingleton
// 添加全局调试函数
;(window as any).debugBackground = () => { ;(window as any).debugBackground = () => {
console.table(backgroundManager.getTimersInfo()) console.table(backgroundManager.getTimersInfo())
console.log('Background Status:', backgroundManager.getStatus()) console.log('Background Status:', backgroundManager.getStatus())
@@ -162,11 +157,9 @@ if (import.meta.env.MODE === 'development') {
// 页面卸载时清理后台管理器 // 页面卸载时清理后台管理器
window.addEventListener('beforeunload', () => { window.addEventListener('beforeunload', () => {
console.log('应用卸载,清理后台资源...')
backgroundManager.destroy() backgroundManager.destroy()
sseManagerSingleton.closeAllManagers() sseManagerSingleton.closeAllManagers()
// 清理PWA状态管理器
if (pwaStateController) { if (pwaStateController) {
pwaStateController.destroy() pwaStateController.destroy()
} }
+5 -5
View File
@@ -1,4 +1,4 @@
# PWA轻量级状态管理使用指南 # PWA状态管理使用指南
## 概述 ## 概述
@@ -254,11 +254,11 @@ if (window.pwaStateController) {
```javascript ```javascript
// 测试状态收集功能 // 测试状态收集功能
const { LightweightStateCollector } = await import('@/utils/pwaStateManager') const { StateCollector } = await import('@/utils/pwaStateManager')
console.log('滚动位置:', LightweightStateCollector.collectScrollPositions()) console.log('滚动位置:', StateCollector.collectScrollPositions())
console.log('弹窗状态:', LightweightStateCollector.collectModalStates()) console.log('弹窗状态:', StateCollector.collectModalStates())
console.log('表单字段:', LightweightStateCollector.collectFormFields()) console.log('表单字段:', StateCollector.collectFormFields())
``` ```
## 性能优势 ## 性能优势
+35 -179
View File
@@ -1,24 +1,21 @@
/** /**
* PWA状态管理器 - 轻量级版本 * PWA状态管理器
* 用于在iOS设备上防止后台被杀时丢失状态,提供状态恢复功能 * 用于在iOS设备上防止后台被杀时丢失状态,提供状态恢复功能
* 只在页面隐藏时收集状态,避免实时监听影响性能 * 只在页面隐藏时收集状态,避免实时监听影响性能
*/ */
// 滚动位置接口
export interface ScrollPosition { export interface ScrollPosition {
x: number x: number
y: number y: number
element?: string // 元素选择器 element?: string
} }
// 弹窗状态接口
export interface ModalState { export interface ModalState {
id: string id: string
isOpen: boolean isOpen: boolean
data?: any data?: any
} }
// 表单字段状态接口
export interface FormFieldState { export interface FormFieldState {
selector: string selector: string
value: string | number | boolean value: string | number | boolean
@@ -27,24 +24,22 @@ export interface FormFieldState {
selectedIndex?: number selectedIndex?: number
} }
// 应用状态接口 - 增强版本
export interface PWAState { export interface PWAState {
url: string url: string
scrollPosition: number scrollPosition: number
scrollPositions: ScrollPosition[] // 多个滚动位置 scrollPositions: ScrollPosition[]
orientation: number orientation: number
timestamp: number timestamp: number
appData?: any appData?: any
formData?: Record<string, any> formData?: Record<string, any>
formFields?: FormFieldState[] // 详细的表单字段状态 formFields?: FormFieldState[]
modalStates?: ModalState[] // 弹窗状态 modalStates?: ModalState[]
userSelections?: { userSelections?: {
selectedItems: string[] selectedItems: string[]
activeTab?: string activeTab?: string
} }
} }
// 当前上下文接口
export interface PWAContext { export interface PWAContext {
url: string url: string
orientation: number orientation: number
@@ -52,28 +47,23 @@ export interface PWAContext {
} }
/** /**
* 轻量级状态收集器 * 状态收集器
* 只在需要时收集状态,不进行实时监听 * 只在需要时收集状态,不进行实时监听
*/ */
export class LightweightStateCollector { export class StateCollector {
/**
* 收集当前页面的滚动位置
*/
static collectScrollPositions(): ScrollPosition[] { static collectScrollPositions(): ScrollPosition[] {
const positions: ScrollPosition[] = [] const positions: ScrollPosition[] = []
// 主窗口滚动
positions.push({ positions.push({
x: window.scrollX, x: window.scrollX,
y: window.scrollY, y: window.scrollY,
element: 'window' element: 'window'
}) })
// 常见的滚动容器
const scrollContainers = [ const scrollContainers = [
'.v-main__wrap', '.v-main__wrap',
'.v-card-text', '.v-card-text',
'.v-sheet', '.v-sheet',
'.perfect-scrollbar', '.perfect-scrollbar',
'[data-simplebar]', '[data-simplebar]',
@@ -97,15 +87,12 @@ export class LightweightStateCollector {
return positions return positions
} }
/**
* 收集当前页面的弹窗状态
*/
static collectModalStates(): ModalState[] { static collectModalStates(): ModalState[] {
const states: ModalState[] = [] const states: ModalState[] = []
const modalSelectors = [ const modalSelectors = [
'.v-dialog', '.v-dialog',
'.v-menu', '.v-menu',
'.v-overlay', '.v-overlay',
'.v-tooltip', '.v-tooltip',
'.v-snackbar', '.v-snackbar',
@@ -121,12 +108,11 @@ export class LightweightStateCollector {
const elements = document.querySelectorAll(selector) const elements = document.querySelectorAll(selector)
elements.forEach(element => { elements.forEach(element => {
if (this.isModalOpen(element)) { if (this.isModalOpen(element)) {
const state: ModalState = { states.push({
id: this.getModalId(element), id: this.getModalId(element),
isOpen: true, isOpen: true,
data: this.extractModalData(element) data: this.extractModalData(element)
} })
states.push(state)
} }
}) })
}) })
@@ -134,9 +120,6 @@ export class LightweightStateCollector {
return states return states
} }
/**
* 收集当前页面的表单字段状态
*/
static collectFormFields(): FormFieldState[] { static collectFormFields(): FormFieldState[] {
const fields: FormFieldState[] = [] const fields: FormFieldState[] = []
@@ -144,31 +127,25 @@ export class LightweightStateCollector {
formElements.forEach(element => { formElements.forEach(element => {
const inputElement = element as HTMLInputElement const inputElement = element as HTMLInputElement
// 跳过密码字段和隐藏字段
if (inputElement.type === 'password' || inputElement.type === 'hidden') { if (inputElement.type === 'password' || inputElement.type === 'hidden') {
return return
} }
// 只收集有值的字段
if (inputElement.value || inputElement.checked) { if (inputElement.value || inputElement.checked) {
const fieldState: FormFieldState = { fields.push({
selector: this.getFieldSelector(inputElement), selector: this.getFieldSelector(inputElement),
value: inputElement.value, value: inputElement.value,
type: inputElement.type, type: inputElement.type,
checked: inputElement.checked, checked: inputElement.checked,
selectedIndex: inputElement.tagName === 'SELECT' ? selectedIndex: inputElement.tagName === 'SELECT' ?
(inputElement as unknown as HTMLSelectElement).selectedIndex : undefined (inputElement as unknown as HTMLSelectElement).selectedIndex : undefined
} })
fields.push(fieldState)
} }
}) })
return fields return fields
} }
/**
* 恢复滚动位置
*/
static restoreScrollPositions(positions: ScrollPosition[]): void { static restoreScrollPositions(positions: ScrollPosition[]): void {
positions.forEach(pos => { positions.forEach(pos => {
if (pos.element === 'window') { if (pos.element === 'window') {
@@ -182,21 +159,14 @@ export class LightweightStateCollector {
}) })
} }
/**
* 恢复弹窗状态
*/
static restoreModalStates(states: ModalState[]): void { static restoreModalStates(states: ModalState[]): void {
states.forEach(state => { states.forEach(state => {
const event = new CustomEvent('restoreModalState', { window.dispatchEvent(new CustomEvent('restoreModalState', {
detail: state detail: state
}) }))
window.dispatchEvent(event)
}) })
} }
/**
* 恢复表单字段
*/
static restoreFormFields(fields: FormFieldState[]): void { static restoreFormFields(fields: FormFieldState[]): void {
fields.forEach(field => { fields.forEach(field => {
const elements = document.querySelectorAll(field.selector) const elements = document.querySelectorAll(field.selector)
@@ -214,17 +184,14 @@ export class LightweightStateCollector {
inputElement.value = field.value as string inputElement.value = field.value as string
} }
// 触发事件以便Vue能够响应
inputElement.dispatchEvent(new Event('input', { bubbles: true })) inputElement.dispatchEvent(new Event('input', { bubbles: true }))
inputElement.dispatchEvent(new Event('change', { bubbles: true })) inputElement.dispatchEvent(new Event('change', { bubbles: true }))
}) })
}) })
} }
// 辅助方法
private static isModalOpen(element: Element): boolean { private static isModalOpen(element: Element): boolean {
const computedStyle = window.getComputedStyle(element) const computedStyle = window.getComputedStyle(element)
return computedStyle.display !== 'none' && return computedStyle.display !== 'none' &&
computedStyle.visibility !== 'hidden' && computedStyle.visibility !== 'hidden' &&
computedStyle.opacity !== '0' && computedStyle.opacity !== '0' &&
@@ -242,7 +209,6 @@ export class LightweightStateCollector {
private static extractModalData(element: Element): any { private static extractModalData(element: Element): any {
const data: any = {} const data: any = {}
// 提取表单数据
const inputs = element.querySelectorAll('input, select, textarea') const inputs = element.querySelectorAll('input, select, textarea')
if (inputs.length > 0) { if (inputs.length > 0) {
data.formData = {} data.formData = {}
@@ -254,7 +220,6 @@ export class LightweightStateCollector {
}) })
} }
// 提取滚动位置
const scrollableElements = element.querySelectorAll('[class*="overflow"], .v-card-text') const scrollableElements = element.querySelectorAll('[class*="overflow"], .v-card-text')
if (scrollableElements.length > 0) { if (scrollableElements.length > 0) {
data.scrollPositions = Array.from(scrollableElements).map((el, index) => ({ data.scrollPositions = Array.from(scrollableElements).map((el, index) => ({
@@ -271,7 +236,6 @@ export class LightweightStateCollector {
if (element.id) return `#${element.id}` if (element.id) return `#${element.id}`
if (element.name) return `[name="${element.name}"]` if (element.name) return `[name="${element.name}"]`
// 简化的选择器生成
const path = [] const path = []
let current = element as Element let current = element as Element
@@ -294,7 +258,6 @@ export class LightweightStateCollector {
path.unshift(selector) path.unshift(selector)
current = current.parentNode as Element current = current.parentNode as Element
// 限制深度,避免选择器过长
if (path.length >= 3) break if (path.length >= 3) break
} }
@@ -729,9 +692,6 @@ export class VisibilityStateManager {
} }
} }
/**
* 完整的PWA状态管理器 - 轻量级版本
*/
export class PWAStateController { export class PWAStateController {
private stateManager: PWAStateManager private stateManager: PWAStateManager
private indexedDBManager: PWAIndexedDBManager private indexedDBManager: PWAIndexedDBManager
@@ -749,7 +709,6 @@ export class PWAStateController {
this.visibilityManager = new VisibilityStateManager(this.stateManager) this.visibilityManager = new VisibilityStateManager(this.stateManager)
this.restoreDecision = new StateRestoreDecision() this.restoreDecision = new StateRestoreDecision()
// 创建状态恢复Promise
this.stateRestorePromise = new Promise((resolve) => { this.stateRestorePromise = new Promise((resolve) => {
this.stateRestoreResolve = resolve this.stateRestoreResolve = resolve
}) })
@@ -757,29 +716,17 @@ export class PWAStateController {
this.init() this.init()
} }
/**
* 等待状态恢复完成
*/
async waitForStateRestore(): Promise<void> { async waitForStateRestore(): Promise<void> {
return this.stateRestorePromise || Promise.resolve() return this.stateRestorePromise || Promise.resolve()
} }
/**
* 获取当前是否正在恢复状态
*/
get isRestoringState(): boolean { get isRestoringState(): boolean {
return this.isRestoring return this.isRestoring
} }
private async init(): Promise<void> { private async init(): Promise<void> {
// 清理过期状态
this.stateManager.clearExpiredState() this.stateManager.clearExpiredState()
// 检查是否需要恢复状态
await this.checkAndRestoreState() await this.checkAndRestoreState()
// 设置定期保存
this.setupPeriodicSave()
} }
private async checkAndRestoreState(): Promise<void> { private async checkAndRestoreState(): Promise<void> {
@@ -822,10 +769,9 @@ export class PWAStateController {
} }
async saveCurrentState(): Promise<void> { async saveCurrentState(): Promise<void> {
// 使用轻量级收集器收集当前状态 const scrollPositions = StateCollector.collectScrollPositions()
const scrollPositions = LightweightStateCollector.collectScrollPositions() const modalStates = StateCollector.collectModalStates()
const modalStates = LightweightStateCollector.collectModalStates() const formFields = StateCollector.collectFormFields()
const formFields = LightweightStateCollector.collectFormFields()
const state: PWAState = { const state: PWAState = {
url: window.location.href, url: window.location.href,
@@ -838,7 +784,6 @@ export class PWAStateController {
formFields: formFields.length > 0 ? formFields : undefined formFields: formFields.length > 0 ? formFields : undefined
} }
// 多重保存策略
await Promise.allSettled([ await Promise.allSettled([
this.stateManager.saveState(state), this.stateManager.saveState(state),
this.indexedDBManager.saveState(state), this.indexedDBManager.saveState(state),
@@ -851,33 +796,27 @@ export class PWAStateController {
const currentUrl = window.location.href const currentUrl = window.location.href
const urlMatches = this.isUrlExactMatch(state.url, currentUrl) const urlMatches = this.isUrlExactMatch(state.url, currentUrl)
// 恢复滚动位置
if (state.scrollPositions && urlMatches) { if (state.scrollPositions && urlMatches) {
LightweightStateCollector.restoreScrollPositions(state.scrollPositions) StateCollector.restoreScrollPositions(state.scrollPositions)
} else if (state.scrollPosition && urlMatches) { } else if (state.scrollPosition && urlMatches) {
// 向后兼容:如果没有新的滚动位置数据,使用旧的方式
window.scrollTo({ window.scrollTo({
top: state.scrollPosition, top: state.scrollPosition,
behavior: 'auto' behavior: 'auto'
}) })
} }
// 恢复弹窗状态
if (state.modalStates) { if (state.modalStates) {
LightweightStateCollector.restoreModalStates(state.modalStates) StateCollector.restoreModalStates(state.modalStates)
} }
// 恢复表单字段
if (state.formFields && urlMatches) { if (state.formFields && urlMatches) {
LightweightStateCollector.restoreFormFields(state.formFields) StateCollector.restoreFormFields(state.formFields)
} }
// 恢复应用特定状态 - 过滤掉不适用的状态
if (state.appData) { if (state.appData) {
this.restoreAppSpecificState(state.appData, urlMatches) this.restoreAppSpecificState(state.appData)
} }
// 触发状态恢复事件
this.dispatchStateRestoreEvent(state) this.dispatchStateRestoreEvent(state)
} }
@@ -891,114 +830,31 @@ export class PWAStateController {
} }
} }
private setupPeriodicSave(): void {
// 轻量级版本不需要定时保存
// 只在页面隐藏时保存状态
}
private getAppSpecificState(): any { private getAppSpecificState(): any {
// 可以在这里添加MoviePilot特定的状态
return { return {
// 路由状态 routerState: {
routerState: this.getRouterState(), currentRoute: window.location.pathname,
// 用户界面状态 query: window.location.search,
uiState: this.getUIState(), hash: window.location.hash
// 表单状态 },
formState: this.getFormState() uiState: {
} sidebarOpen: document.querySelector('.v-navigation-drawer--active') !== null,
} darkMode: document.documentElement.getAttribute('data-theme') === 'dark'
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<string, any> = {}
forms.forEach((form, index) => {
const inputs = form.querySelectorAll('input, select, textarea')
const data: Record<string, any> = {}
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, urlMatches: boolean = true): void {
// 总是恢复UI状态(如主题等)
if (appData.uiState) {
this.restoreUIState(appData.uiState)
}
// 只有在URL匹配时才恢复表单状态
if (appData.formState && urlMatches) {
this.restoreFormState(appData.formState)
} }
} }
private restoreUIState(uiState: any): void { private restoreAppSpecificState(appData: any): void {
// 恢复UI状态 // 基础状态恢复,可根据需要扩展
if (uiState.darkMode !== undefined) {
// 这里可以根据实际的主题切换逻辑来恢复
}
}
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<string, any>).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 { private dispatchStateRestoreEvent(state: PWAState): void {
const event = new CustomEvent('pwaStateRestored', { window.dispatchEvent(new CustomEvent('pwaStateRestored', {
detail: { state } detail: { state }
}) }))
window.dispatchEvent(event)
} }
/**
* 销毁管理器并清理资源
*/
destroy(): void { destroy(): void {
// 轻量级版本无需清理资源 // 无需清理资源
} }
} }