Implement lightweight PWA state management with zero-overhead approach

Co-authored-by: jxxghp <jxxghp@163.com>
This commit is contained in:
Cursor Agent
2025-07-06 23:14:50 +00:00
parent 8824869cd1
commit 304b990994
3 changed files with 327 additions and 720 deletions

View File

@@ -1,31 +1,25 @@
# PWA增强状态管理使用指南
# PWA轻量级状态管理使用指南
## 概述
增强的PWA状态管理器提供了以下功能
- 🔄 **增强的滚动位置管理**自动跟踪和恢复页面及容器的滚动位置
- 📝 **实时表单数据保存**时保存用户输入的表单数据
- 🎛️ **弹窗状态管理**保存和恢复弹窗的打开状态和内容
轻量级PWA状态管理器提供了以下功能
- 🔄 **智能滚动位置管理**退到后台时收集和保存滚动位置
- 📝 **表单数据保存**退到后台时保存用户输入的表单数据
- 🎛️ **弹窗状态管理**退到后台时保存打开的弹窗状态
## 自动功能
## 工作原理
以下功能是**自动启用**的,无需额外配置:
**轻量级设计**:不使用实时监听,只在页面隐藏时收集状态,避免影响性能。
### 1. 滚动位置自动保存和恢复
- 主窗口滚动位置
- 常见容器滚动位置:`.v-main__wrap``.v-card-text``.perfect-scrollbar`
- 自动防抖,避免过度保存
### 状态收集时机
- 页面隐藏(`visibilitychange`
- 页面卸载(`beforeunload`
- 手动调用保存方法
### 2. 表单数据实时保存
- 所有 `input``textarea``select` 元素的值
- 复选框和单选按钮的选中状态
- 下拉框的选中项
- 500ms防抖延迟
### 3. 弹窗状态跟踪
- Vuetify弹窗组件`.v-dialog``.v-menu``.v-overlay`
- 自动检测弹窗的打开和关闭
- 保存弹窗内的表单数据和滚动位置
### 收集的状态
1. **滚动位置**:主窗口和常见容器的滚动位置
2. **表单数据**:所有有值的表单字段(跳过密码字段)
3. **弹窗状态**:当前打开的弹窗及其内容
## 在Vue组件中使用
@@ -230,19 +224,21 @@ onUnmounted(() => {
## 调试和监控
### 检查状态保存
### 手动触发状态保存
```javascript
// 在浏览器控制台中查看保存的状态
console.log('滚动位置:', sessionStorage.getItem('mp-scroll-positions'))
console.log('弹窗状态:', sessionStorage.getItem('mp-modal-states'))
console.log('表单字段:', sessionStorage.getItem('mp-form-fields'))
// 手动保存当前状态
if (window.pwaStateController) {
window.pwaStateController.saveCurrentState()
.then(() => console.log('状态保存成功'))
.catch(err => console.error('状态保存失败:', err))
}
```
### 监控PWA状态
```javascript
// 监听所有PWA状态变化
// 监听状态恢复事件
window.addEventListener('pwaStateRestored', (event) => {
console.log('PWA状态恢复:', event.detail.state)
})
@@ -254,52 +250,70 @@ if (window.pwaStateController) {
}
```
## 性能考虑
### 测试状态收集
### 防抖和节流
- 滚动位置保存100ms防抖
- 表单数据保存500ms防抖
- 弹窗状态检测实时响应DOM变化
```javascript
// 测试状态收集功能
const { LightweightStateCollector } = await import('@/utils/pwaStateManager')
console.log('滚动位置:', LightweightStateCollector.collectScrollPositions())
console.log('弹窗状态:', LightweightStateCollector.collectModalStates())
console.log('表单字段:', LightweightStateCollector.collectFormFields())
```
## 性能优势
### 轻量级设计
- **零实时监听**:不添加事件监听器,不影响页面性能
- **按需收集**:只在页面隐藏时收集状态
- **无防抖需要**:不需要防抖和节流
### 存储策略
- **sessionStorage**用于临时状态(滚动位置、表单数据)
- **localStorage**:用于持久状态(应用设置
- **IndexedDB**:用于复杂状态数据
- **Service Worker**:用于离线状态同步
- **localStorage**持久状态(应用设置、用户数据)
- **IndexedDB**:复杂状态数据(大量数据
- **Service Worker**:离线状态同步
### 内存管理
- 自动清理过期状态24小时
- 组件卸载时自动移除事件监听器
- 限制缓存大小,避免内存泄漏
### 内存效率
- **无常驻对象**:不维护状态缓存
- **静态方法**:使用静态方法,减少内存占用
- **按需加载**状态收集时才执行DOM查询
## 故障排除
### 常见问题
1. **滚动位置没有恢复**
- 确保元素有正确的CSS类名
- 检查元素是否在DOM渲染完成后才滚动
1. **状态没有保存**
- 检查是否在PWA环境中运行
- 确认页面隐藏事件是否触发
- 手动调用 `saveCurrentState()` 测试
2. **表单数据没有保存**
- 确保表单字段有 `name` 属性
- 检查表单是否在PWA控制器初始化后才创建
2. **滚动位置没有恢复**
- 确保滚动容器有支持的CSS类名
- 检查URL是否完全匹配
3. **弹窗状态没有恢复**
- 确保弹窗有唯一标识
- 检查弹窗是否使用了支持的CSS类名
3. **表单数据没有恢复**
- 确保表单字段有 `name``id` 属性
- 检查字段在保存时是否有值
### 启用调试模式
4. **弹窗状态没有恢复**
- 确保弹窗在页面隐藏时是打开状态
- 检查弹窗是否使用了支持的CSS选择器
### 调试方法
```javascript
// 在开发环境中启用详细日志
if (import.meta.env.DEV) {
window.pwaDebug = true
}
// 在页面隐藏前手动测试状态收集
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('页面隐藏,开始收集状态...')
// 在这里添加断点或日志
}
})
```
## 注意事项
- 状态恢复只在PWA环境中工作
- 某些敏感数据(如密码)不会被保存
- 跨域表单数据可能无法正常恢复
- 动态创建的元素可能需要手动处理
- **仅在PWA环境有效**:状态管理只在PWA模式下工作
- **密码字段跳过**:密码和隐藏字段不会被保存
- **URL匹配限制**只有URL完全匹配才恢复滚动位置和表单数据
- **轻量级设计**:不监听实时事件,性能友好但精度相对较低

View File

@@ -1,7 +1,7 @@
/**
* PWA状态管理器 - 增强版本
* PWA状态管理器 - 轻量级版本
* 用于在iOS设备上防止后台被杀时丢失状态提供状态恢复功能
* 新增功能:实时表单保存、弹窗状态管理、增强滚动位置管理
* 只在页面隐藏时收集状态,避免实时监听影响性能
*/
// 滚动位置接口
@@ -16,7 +16,6 @@ export interface ModalState {
id: string
isOpen: boolean
data?: any
position?: { x: number; y: number }
}
// 表单字段状态接口
@@ -53,24 +52,25 @@ export interface PWAContext {
}
/**
* 增强的滚动位置管理
* 轻量级状态收集
* 只在需要时收集状态,不进行实时监听
*/
export class EnhancedScrollManager {
private scrollPositions = new Map<string, ScrollPosition>()
private scrollObservers = new Map<string, MutationObserver>()
private debounceTimer: number | null = null
private saveCallback: (positions: ScrollPosition[]) => void
constructor(saveCallback: (positions: ScrollPosition[]) => void) {
this.saveCallback = saveCallback
this.initScrollTracking()
}
private initScrollTracking(): void {
// 监听主窗口滚动
window.addEventListener('scroll', this.debounceScrollSave.bind(this), { passive: true })
export class LightweightStateCollector {
/**
* 收集当前页面的滚动位置
*/
static collectScrollPositions(): ScrollPosition[] {
const positions: ScrollPosition[] = []
// 监听常见的滚动容器
// 主窗口滚动
positions.push({
x: window.scrollX,
y: window.scrollY,
element: 'window'
})
// 常见的滚动容器
const scrollContainers = [
'.v-main__wrap',
'.v-card-text',
@@ -82,157 +82,27 @@ export class EnhancedScrollManager {
]
scrollContainers.forEach(selector => {
this.observeScrollContainer(selector)
})
}
private observeScrollContainer(selector: string): void {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element
if (element.matches(selector)) {
this.addScrollListener(element, selector)
}
// 也检查子元素
element.querySelectorAll(selector).forEach((child) => {
this.addScrollListener(child, selector)
})
}
const elements = document.querySelectorAll(selector)
elements.forEach((element, index) => {
if (element.scrollTop > 0 || element.scrollLeft > 0) {
positions.push({
x: element.scrollLeft,
y: element.scrollTop,
element: `${selector}:nth-of-type(${index + 1})`
})
}
})
})
observer.observe(document.body, {
childList: true,
subtree: true
})
this.scrollObservers.set(selector, observer)
// 立即处理已存在的元素
document.querySelectorAll(selector).forEach((element) => {
this.addScrollListener(element, selector)
})
}
private addScrollListener(element: Element, selector: string): void {
if (element.getAttribute('data-scroll-tracked')) return
element.setAttribute('data-scroll-tracked', 'true')
element.addEventListener('scroll', () => {
this.updateScrollPosition(element, selector)
}, { passive: true })
return positions
}
private updateScrollPosition(element: Element, selector: string): void {
const scrollPos: ScrollPosition = {
x: element.scrollLeft,
y: element.scrollTop,
element: selector
}
this.scrollPositions.set(selector, scrollPos)
this.debounceScrollSave()
}
private debounceScrollSave(): void {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer)
}
this.debounceTimer = window.setTimeout(() => {
// 主窗口滚动
this.scrollPositions.set('window', {
x: window.scrollX,
y: window.scrollY,
element: 'window'
})
this.saveCallback(Array.from(this.scrollPositions.values()))
}, 100)
}
restoreScrollPositions(positions: ScrollPosition[]): void {
positions.forEach(pos => {
if (pos.element === 'window') {
window.scrollTo({ top: pos.y, left: pos.x, behavior: 'auto' })
} else {
const elements = document.querySelectorAll(pos.element!)
elements.forEach(element => {
element.scrollTo({ top: pos.y, left: pos.x, behavior: 'auto' })
})
}
})
}
destroy(): void {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer)
}
this.scrollObservers.forEach(observer => observer.disconnect())
this.scrollObservers.clear()
this.scrollPositions.clear()
}
}
/**
* 弹窗状态管理器
*/
export class ModalStateManager {
private modalStates = new Map<string, ModalState>()
private mutationObserver: MutationObserver | null = null
private saveCallback: (states: ModalState[]) => void
constructor(saveCallback: (states: ModalState[]) => void) {
this.saveCallback = saveCallback
this.initModalTracking()
}
private initModalTracking(): void {
// 监听DOM变化来检测弹窗的打开和关闭
this.mutationObserver = new MutationObserver((mutations) => {
let hasModalChanges = false
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
// 检查新添加的弹窗
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element
if (this.isModalElement(element)) {
this.trackModal(element)
hasModalChanges = true
}
}
})
} else if (mutation.type === 'attributes') {
const element = mutation.target as Element
if (this.isModalElement(element)) {
this.updateModalState(element)
hasModalChanges = true
}
}
})
if (hasModalChanges) {
this.saveStates()
}
})
this.mutationObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class', 'style', 'aria-hidden', 'data-*']
})
// 立即扫描已存在的弹窗
this.scanExistingModals()
}
private isModalElement(element: Element): boolean {
/**
* 收集当前页面的弹窗状态
*/
static collectModalStates(): ModalState[] {
const states: ModalState[] = []
const modalSelectors = [
'.v-dialog',
'.v-menu',
@@ -244,155 +114,78 @@ export class ModalStateManager {
'.drawer',
'.v-navigation-drawer',
'[role="dialog"]',
'[role="alertdialog"]',
'[role="tooltip"]'
]
return modalSelectors.some(selector =>
element.matches(selector) || element.querySelector(selector)
)
}
private trackModal(element: Element): void {
const id = this.getModalId(element)
const isOpen = this.isModalOpen(element)
if (isOpen) {
const state: ModalState = {
id,
isOpen: true,
data: this.extractModalData(element)
}
this.modalStates.set(id, state)
} else {
this.modalStates.delete(id)
}
}
private updateModalState(element: Element): void {
const id = this.getModalId(element)
const isOpen = this.isModalOpen(element)
if (isOpen) {
const state: ModalState = {
id,
isOpen: true,
data: this.extractModalData(element)
}
this.modalStates.set(id, state)
} else {
this.modalStates.delete(id)
}
}
private getModalId(element: Element): string {
return element.id ||
element.getAttribute('data-modal-id') ||
element.className.replace(/\s+/g, '-') ||
`modal-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
}
private isModalOpen(element: Element): boolean {
const computedStyle = window.getComputedStyle(element)
// 检查多种可能的显示状态
return computedStyle.display !== 'none' &&
computedStyle.visibility !== 'hidden' &&
computedStyle.opacity !== '0' &&
!element.hasAttribute('hidden') &&
element.getAttribute('aria-hidden') !== 'true' &&
!element.classList.contains('v-overlay--active') === false
}
private extractModalData(element: Element): any {
const data: any = {}
// 提取表单数据
const form = element.querySelector('form')
if (form) {
data.formData = this.extractFormData(form)
}
// 提取输入字段
const inputs = element.querySelectorAll('input, select, textarea')
if (inputs.length > 0) {
data.inputData = this.extractInputData(inputs)
}
// 提取滚动位置
const scrollableElements = element.querySelectorAll('[class*="overflow"], .v-card-text')
if (scrollableElements.length > 0) {
data.scrollPositions = Array.from(scrollableElements).map(el => ({
selector: this.getElementSelector(el),
x: el.scrollLeft,
y: el.scrollTop
}))
}
return data
}
private extractFormData(form: Element): Record<string, any> {
const formData: Record<string, any> = {}
const inputs = form.querySelectorAll('input, select, textarea')
inputs.forEach(input => {
const element = input as HTMLInputElement
if (element.name) {
formData[element.name] = element.value
}
})
return formData
}
private extractInputData(inputs: NodeListOf<Element>): Record<string, any> {
const inputData: Record<string, any> = {}
inputs.forEach(input => {
const element = input as HTMLInputElement
const key = element.name || element.id || this.getElementSelector(element)
inputData[key] = element.value
})
return inputData
}
private getElementSelector(element: Element): string {
if (element.id) return `#${element.id}`
if (element.className) return `.${element.className.split(' ')[0]}`
return element.tagName.toLowerCase()
}
private scanExistingModals(): void {
const modalSelectors = [
'.v-dialog',
'.v-menu',
'.v-overlay',
'.modal',
'.popup',
'[role="dialog"]'
'[role="alertdialog"]'
]
modalSelectors.forEach(selector => {
document.querySelectorAll(selector).forEach(element => {
const elements = document.querySelectorAll(selector)
elements.forEach(element => {
if (this.isModalOpen(element)) {
this.trackModal(element)
const state: ModalState = {
id: this.getModalId(element),
isOpen: true,
data: this.extractModalData(element)
}
states.push(state)
}
})
})
this.saveStates()
return states
}
private saveStates(): void {
this.saveCallback(Array.from(this.modalStates.values()))
/**
* 收集当前页面的表单字段状态
*/
static collectFormFields(): FormFieldState[] {
const fields: FormFieldState[] = []
const formElements = document.querySelectorAll('input, textarea, select')
formElements.forEach(element => {
const inputElement = element as HTMLInputElement
// 跳过密码字段和隐藏字段
if (inputElement.type === 'password' || inputElement.type === 'hidden') {
return
}
// 只收集有值的字段
if (inputElement.value || inputElement.checked) {
const fieldState: FormFieldState = {
selector: this.getFieldSelector(inputElement),
value: inputElement.value,
type: inputElement.type,
checked: inputElement.checked,
selectedIndex: inputElement.tagName === 'SELECT' ?
(inputElement as unknown as HTMLSelectElement).selectedIndex : undefined
}
fields.push(fieldState)
}
})
return fields
}
restoreModalStates(states: ModalState[]): void {
// 此方法需要与应用的具体弹窗实现配合
// 可以通过事件系统通知应用恢复弹窗状态
/**
* 恢复滚动位置
*/
static restoreScrollPositions(positions: ScrollPosition[]): void {
positions.forEach(pos => {
if (pos.element === 'window') {
window.scrollTo({ top: pos.y, left: pos.x, behavior: 'auto' })
} else {
const elements = document.querySelectorAll(pos.element!)
elements.forEach(element => {
element.scrollTo({ top: pos.y, left: pos.x, behavior: 'auto' })
})
}
})
}
/**
* 恢复弹窗状态
*/
static restoreModalStates(states: ModalState[]): void {
states.forEach(state => {
const event = new CustomEvent('restoreModalState', {
detail: state
@@ -400,155 +193,11 @@ export class ModalStateManager {
window.dispatchEvent(event)
})
}
destroy(): void {
if (this.mutationObserver) {
this.mutationObserver.disconnect()
}
this.modalStates.clear()
}
}
/**
* 实时表单数据管理器
*/
export class RealTimeFormManager {
private formFields = new Map<string, FormFieldState>()
private debounceTimer: number | null = null
private saveCallback: (fields: FormFieldState[]) => void
private observers = new Set<MutationObserver>()
constructor(saveCallback: (fields: FormFieldState[]) => void) {
this.saveCallback = saveCallback
this.initFormTracking()
}
private initFormTracking(): void {
// 监听表单输入事件
const inputEvents = ['input', 'change', 'blur', 'focus']
inputEvents.forEach(eventType => {
document.addEventListener(eventType, this.handleFormInput.bind(this), true)
})
// 监听DOM变化跟踪新添加的表单元素
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element
this.trackFormElements(element)
}
})
}
})
})
observer.observe(document.body, {
childList: true,
subtree: true
})
this.observers.add(observer)
// 立即扫描已存在的表单元素
this.scanExistingForms()
}
private handleFormInput(event: Event): void {
const target = event.target as HTMLInputElement
if (this.isFormElement(target)) {
this.updateFormField(target)
}
}
private isFormElement(element: Element): boolean {
const formTags = ['INPUT', 'TEXTAREA', 'SELECT']
return formTags.includes(element.tagName)
}
private updateFormField(element: HTMLInputElement): void {
const selector = this.getFieldSelector(element)
const fieldState: FormFieldState = {
selector,
value: element.value,
type: element.type,
checked: element.checked,
selectedIndex: element.tagName === 'SELECT' ? (element as unknown as HTMLSelectElement).selectedIndex : undefined
}
this.formFields.set(selector, fieldState)
this.debounceSave()
}
private getFieldSelector(element: HTMLInputElement): string {
if (element.id) return `#${element.id}`
if (element.name) return `[name="${element.name}"]`
// 构建更复杂的选择器
const path = []
let current = element as Element
while (current && current !== document.body) {
let selector = current.tagName.toLowerCase()
if (current.id) {
selector += `#${current.id}`
path.unshift(selector)
break
}
if (current.className) {
const classes = current.className.split(/\s+/).filter(c => c)
if (classes.length > 0) {
selector += `.${classes[0]}`
}
}
const siblings = Array.from(current.parentNode?.children || [])
.filter(sibling => sibling.tagName === current.tagName)
if (siblings.length > 1) {
const index = siblings.indexOf(current) + 1
selector += `:nth-child(${index})`
}
path.unshift(selector)
current = current.parentNode as Element
}
return path.join(' > ')
}
private trackFormElements(element: Element): void {
const formElements = element.querySelectorAll('input, textarea, select')
formElements.forEach(formElement => {
this.updateFormField(formElement as HTMLInputElement)
})
// 如果元素本身是表单元素
if (this.isFormElement(element)) {
this.updateFormField(element as HTMLInputElement)
}
}
private scanExistingForms(): void {
const formElements = document.querySelectorAll('input, textarea, select')
formElements.forEach(element => {
this.updateFormField(element as HTMLInputElement)
})
}
private debounceSave(): void {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer)
}
this.debounceTimer = window.setTimeout(() => {
this.saveCallback(Array.from(this.formFields.values()))
}, 500) // 500ms防抖
}
restoreFormFields(fields: FormFieldState[]): void {
/**
* 恢复表单字段
*/
static restoreFormFields(fields: FormFieldState[]): void {
fields.forEach(field => {
const elements = document.querySelectorAll(field.selector)
elements.forEach(element => {
@@ -571,14 +220,85 @@ export class RealTimeFormManager {
})
})
}
destroy(): void {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer)
// 辅助方法
private static isModalOpen(element: Element): boolean {
const computedStyle = window.getComputedStyle(element)
return computedStyle.display !== 'none' &&
computedStyle.visibility !== 'hidden' &&
computedStyle.opacity !== '0' &&
!element.hasAttribute('hidden') &&
element.getAttribute('aria-hidden') !== 'true'
}
private static getModalId(element: Element): string {
return element.id ||
element.getAttribute('data-modal-id') ||
element.className.replace(/\s+/g, '-') ||
`modal-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
}
private static extractModalData(element: Element): any {
const data: any = {}
// 提取表单数据
const inputs = element.querySelectorAll('input, select, textarea')
if (inputs.length > 0) {
data.formData = {}
inputs.forEach(input => {
const inputElement = input as HTMLInputElement
if (inputElement.name && inputElement.value) {
data.formData[inputElement.name] = inputElement.value
}
})
}
this.observers.forEach(observer => observer.disconnect())
this.observers.clear()
this.formFields.clear()
// 提取滚动位置
const scrollableElements = element.querySelectorAll('[class*="overflow"], .v-card-text')
if (scrollableElements.length > 0) {
data.scrollPositions = Array.from(scrollableElements).map((el, index) => ({
selector: `${element.className}:nth-of-type(${index + 1}) [class*="overflow"]`,
x: el.scrollLeft,
y: el.scrollTop
}))
}
return data
}
private static getFieldSelector(element: HTMLInputElement): string {
if (element.id) return `#${element.id}`
if (element.name) return `[name="${element.name}"]`
// 简化的选择器生成
const path = []
let current = element as Element
while (current && current !== document.body) {
let selector = current.tagName.toLowerCase()
if (current.id) {
selector += `#${current.id}`
path.unshift(selector)
break
}
if (current.className) {
const classes = current.className.split(/\s+/).filter(c => c)
if (classes.length > 0) {
selector += `.${classes[0]}`
}
}
path.unshift(selector)
current = current.parentNode as Element
// 限制深度,避免选择器过长
if (path.length >= 3) break
}
return path.join(' > ')
}
}
@@ -1010,7 +730,7 @@ export class VisibilityStateManager {
}
/**
* 完整的PWA状态管理器 - 增强版本
* 完整的PWA状态管理器 - 轻量级版本
*/
export class PWAStateController {
private stateManager: PWAStateManager
@@ -1018,9 +738,6 @@ export class PWAStateController {
private swStateSync: ServiceWorkerStateSync
private visibilityManager: VisibilityStateManager
private restoreDecision: StateRestoreDecision
private enhancedScrollManager: EnhancedScrollManager
private modalStateManager: ModalStateManager
private realTimeFormManager: RealTimeFormManager
private stateRestorePromise: Promise<void> | null = null
private stateRestoreResolve: (() => void) | null = null
private isRestoring = false
@@ -1032,19 +749,6 @@ export class PWAStateController {
this.visibilityManager = new VisibilityStateManager(this.stateManager)
this.restoreDecision = new StateRestoreDecision()
// 初始化增强管理器
this.enhancedScrollManager = new EnhancedScrollManager((positions) => {
this.saveScrollPositions(positions)
})
this.modalStateManager = new ModalStateManager((states) => {
this.saveModalStates(states)
})
this.realTimeFormManager = new RealTimeFormManager((fields) => {
this.saveFormFields(fields)
})
// 创建状态恢复Promise
this.stateRestorePromise = new Promise((resolve) => {
this.stateRestoreResolve = resolve
@@ -1118,10 +822,10 @@ export class PWAStateController {
}
async saveCurrentState(): Promise<void> {
// 从sessionStorage获取实时状态
const scrollPositions = this.getScrollPositionsFromStorage()
const modalStates = this.getModalStatesFromStorage()
const formFields = this.getFormFieldsFromStorage()
// 使用轻量级收集器收集当前状态
const scrollPositions = LightweightStateCollector.collectScrollPositions()
const modalStates = LightweightStateCollector.collectModalStates()
const formFields = LightweightStateCollector.collectFormFields()
const state: PWAState = {
url: window.location.href,
@@ -1147,9 +851,9 @@ export class PWAStateController {
const currentUrl = window.location.href
const urlMatches = this.isUrlExactMatch(state.url, currentUrl)
// 使用增强滚动管理器恢复滚动位置
// 恢复滚动位置
if (state.scrollPositions && urlMatches) {
this.enhancedScrollManager.restoreScrollPositions(state.scrollPositions)
LightweightStateCollector.restoreScrollPositions(state.scrollPositions)
} else if (state.scrollPosition && urlMatches) {
// 向后兼容:如果没有新的滚动位置数据,使用旧的方式
window.scrollTo({
@@ -1160,12 +864,12 @@ export class PWAStateController {
// 恢复弹窗状态
if (state.modalStates) {
this.modalStateManager.restoreModalStates(state.modalStates)
LightweightStateCollector.restoreModalStates(state.modalStates)
}
// 恢复表单字段
if (state.formFields && urlMatches) {
this.realTimeFormManager.restoreFormFields(state.formFields)
LightweightStateCollector.restoreFormFields(state.formFields)
}
// 恢复应用特定状态 - 过滤掉不适用的状态
@@ -1173,9 +877,6 @@ export class PWAStateController {
this.restoreAppSpecificState(state.appData, urlMatches)
}
// 从sessionStorage恢复额外的状态
this.restoreFromSessionStorage(urlMatches)
// 触发状态恢复事件
this.dispatchStateRestoreEvent(state)
}
@@ -1191,22 +892,8 @@ export class PWAStateController {
}
private setupPeriodicSave(): void {
// 导入后台管理器
import('@/utils/backgroundManager').then(({ addBackgroundTimer }) => {
// 使用后台管理器,延长间隔
addBackgroundTimer(
'pwa-state-save',
() => {
// 只在前台时保存状态(由后台管理器自动处理)
this.saveCurrentState()
},
60000, // 改为60秒减少频率
{
runInBackground: false, // 后台时不保存
skipInitialRun: true
}
)
})
// 轻量级版本不需要定时保存
// 只在页面隐藏时保存状态
}
private getAppSpecificState(): any {
@@ -1308,118 +995,10 @@ export class PWAStateController {
window.dispatchEvent(event)
}
/**
* 保存滚动位置(被增强滚动管理器调用)
*/
private saveScrollPositions(positions: ScrollPosition[]): void {
// 实时保存滚动位置到sessionStorage
try {
sessionStorage.setItem('mp-scroll-positions', JSON.stringify(positions))
} catch (error) {
console.error('保存滚动位置失败:', error)
}
}
/**
* 保存弹窗状态(被弹窗状态管理器调用)
*/
private saveModalStates(states: ModalState[]): void {
// 实时保存弹窗状态到sessionStorage
try {
sessionStorage.setItem('mp-modal-states', JSON.stringify(states))
} catch (error) {
console.error('保存弹窗状态失败:', error)
}
}
/**
* 保存表单字段(被实时表单管理器调用)
*/
private saveFormFields(fields: FormFieldState[]): void {
// 实时保存表单字段到sessionStorage
try {
sessionStorage.setItem('mp-form-fields', JSON.stringify(fields))
} catch (error) {
console.error('保存表单字段失败:', error)
}
}
/**
* 从sessionStorage恢复额外的状态
*/
private restoreFromSessionStorage(urlMatches: boolean): void {
try {
// 恢复滚动位置
if (urlMatches) {
const scrollPositions = sessionStorage.getItem('mp-scroll-positions')
if (scrollPositions) {
const positions: ScrollPosition[] = JSON.parse(scrollPositions)
this.enhancedScrollManager.restoreScrollPositions(positions)
}
}
// 恢复弹窗状态
const modalStates = sessionStorage.getItem('mp-modal-states')
if (modalStates) {
const states: ModalState[] = JSON.parse(modalStates)
this.modalStateManager.restoreModalStates(states)
}
// 恢复表单字段
if (urlMatches) {
const formFields = sessionStorage.getItem('mp-form-fields')
if (formFields) {
const fields: FormFieldState[] = JSON.parse(formFields)
this.realTimeFormManager.restoreFormFields(fields)
}
}
} catch (error) {
console.error('从sessionStorage恢复状态失败:', error)
}
}
/**
* 从sessionStorage获取滚动位置
*/
private getScrollPositionsFromStorage(): ScrollPosition[] {
try {
const stored = sessionStorage.getItem('mp-scroll-positions')
return stored ? JSON.parse(stored) : []
} catch (error) {
return []
}
}
/**
* 从sessionStorage获取弹窗状态
*/
private getModalStatesFromStorage(): ModalState[] {
try {
const stored = sessionStorage.getItem('mp-modal-states')
return stored ? JSON.parse(stored) : []
} catch (error) {
return []
}
}
/**
* 从sessionStorage获取表单字段
*/
private getFormFieldsFromStorage(): FormFieldState[] {
try {
const stored = sessionStorage.getItem('mp-form-fields')
return stored ? JSON.parse(stored) : []
} catch (error) {
return []
}
}
/**
* 销毁管理器并清理资源
*/
destroy(): void {
this.enhancedScrollManager.destroy()
this.modalStateManager.destroy()
this.realTimeFormManager.destroy()
// 轻量级版本无需清理资源
}
}