Private
Public Access
1
0
Files
u-desk/docs/02-架构设计/架构改进方案-状态管理优化.md

11 KiB
Raw Permalink Blame History

架构改进方案:状态管理优化

问题分析

当前遇到的问题属于"响应式状态同步灾难",主要问题:

  1. 状态分散:多个 Composables 各自管理状态,难以追踪数据流
  2. 响应式失效computed/watch 在复杂场景下失效,难以调试
  3. 数据传递复杂props/computed/provide 多层传递,容易丢失
  4. 缺乏状态快照:无法回溯状态变化历史
  5. 调试困难:大量 console.log 散布在代码中,难以系统化

改进方案

1. 引入 Pinia 统一状态管理

1.1 安装 Pinia

npm install pinia

1.2 创建 Store 结构

stores/
  ├── db-cli/
  │   ├── index.ts          # 主 store
  │   ├── connection.ts     # 连接状态
  │   ├── structure.ts      # 表结构状态
  │   ├── result.ts         # 查询结果状态
  │   ├── editor.ts         # 编辑器状态
  │   └── message.ts        # 消息日志状态
  └── devtools.ts           # 开发工具(状态快照/回放)

1.3 核心 Store 设计

stores/db-cli/structure.ts - 表结构状态管理

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export interface StructureInfo {
  connectionId: number
  database: string
  tableName: string
  dbType: 'mysql' | 'mongo' | 'redis'
  nodeType: string
}

export interface StructureData {
  type: string
  columns?: any[]
  database?: string
  table?: string
  // ... 其他字段
}

export const useStructureStore = defineStore('structure', () => {
  // 状态定义
  const loading = ref(false)
  const error = ref<string | null>(null)
  const data = ref<StructureData | null>(null)
  const info = ref<StructureInfo | null>(null)
  
  // 计算属性(自动响应式)
  const hasData = computed(() => data.value !== null && info.value !== null)
  const isReady = computed(() => !loading.value && hasData.value)
  
  // Actions统一的数据变更入口
  async function loadStructure(params: {
    connectionId: number
    database: string
    tableName: string
    dbType: 'mysql' | 'mongo' | 'redis'
    nodeType: string
  }) {
    // 防止重复加载
    if (loading.value) {
      console.warn('结构正在加载中,跳过重复请求')
      return
    }
    
    try {
      loading.value = true
      error.value = null
      
      // 验证参数
      if (params.nodeType === 'connection' || params.nodeType === 'database') {
        info.value = {
          ...params,
          tableName: ''
        }
        data.value = null
        return
      }
      
      if (!params.tableName) {
        info.value = {
          ...params,
          tableName: ''
        }
        data.value = null
        return
      }
      
      // 调用后端
      if (!window.go?.main?.App?.GetTableStructure) {
        throw new Error('Go 后端未就绪')
      }
      
      const result = await window.go.main.App.GetTableStructure(
        params.connectionId,
        params.database,
        params.tableName
      )
      
      // 原子性更新(确保数据一致性)
      data.value = result
      info.value = params
      
      // 状态变更日志(开发环境)
      if (import.meta.env.DEV) {
        console.log('[StructureStore] 数据加载成功', { info: params, data: result })
      }
    } catch (err) {
      const errorMessage = err instanceof Error ? err.message : '加载表结构失败'
      error.value = errorMessage
      data.value = null
      info.value = null
      
      if (import.meta.env.DEV) {
        console.error('[StructureStore] 加载失败', err)
      }
    } finally {
      loading.value = false
    }
  }
  
  function clear() {
    data.value = null
    info.value = null
    error.value = null
  }
  
  function reset() {
    loading.value = false
    error.value = null
    data.value = null
    info.value = null
  }
  
  return {
    // 状态
    loading,
    error,
    data,
    info,
    // 计算属性
    hasData,
    isReady,
    // 方法
    loadStructure,
    clear,
    reset
  }
})

stores/db-cli/index.ts - 主 Store

import { defineStore } from 'pinia'
import { useStructureStore } from './structure'
import { useConnectionStore } from './connection'
// ... 其他 stores

// 组合 Store提供统一访问入口
export const useDbCliStore = () => {
  return {
    structure: useStructureStore(),
    connection: useConnectionStore(),
    // ... 其他 stores
  }
}

2. 组件中使用 Store

views/db-cli/index.vue

<script setup lang="ts">
import { useStructureStore } from '@/stores/db-cli/structure'

// 使用 Store自动响应式无需 computed
const structureStore = useStructureStore()

// 直接使用Vue 会自动追踪
const handleTableStructure = async (data: TableStructureEvent) => {
  // 单一切口,清晰的数据流
  await structureStore.loadStructure({
    connectionId: data.connectionId,
    database: data.database,
    tableName: data.tableName,
    dbType: data.dbType,
    nodeType: data.nodeType
  })
  
  // 切换到结构 Tab
  if (resultPanelRef.value) {
    resultPanelRef.value.switchToStructureTab()
  }
}
</script>

<template>
  <ResultPanel
    :structure-loading="structureStore.loading"
    :structure-error="structureStore.error"
    :structure-data="structureStore.data"
    :structure-info="structureStore.info"
  />
</template>

3. 状态调试工具

stores/devtools.ts - 开发工具

import { watch } from 'vue'

/**
 * 状态变更追踪器(仅开发环境)
 */
export function setupStateDebugger() {
  if (!import.meta.env.DEV) return
  
  // 追踪所有 store 的状态变更
  const stateHistory: Array<{
    timestamp: number
    store: string
    action: string
    oldValue: any
    newValue: any
  }> = []
  
  return {
    log(store: string, action: string, oldValue: any, newValue: any) {
      stateHistory.push({
        timestamp: Date.now(),
        store,
        action,
        oldValue: JSON.parse(JSON.stringify(oldValue)),
        newValue: JSON.parse(JSON.stringify(newValue))
      })
      
      console.group(`[${store}] ${action}`)
      console.log('旧值:', oldValue)
      console.log('新值:', newValue)
      console.log('历史记录:', stateHistory.slice(-10))
      console.groupEnd()
    },
    
    getHistory() {
      return stateHistory
    },
    
    clearHistory() {
      stateHistory.length = 0
    }
  }
}

4. 类型安全增强

types/db-cli.ts

// 统一类型定义
export type DbType = 'mysql' | 'mongo' | 'redis'
export type NodeType = 'connection' | 'database' | 'table' | 'collection' | 'key'

export interface ConnectionInfo {
  id: number
  name: string
  type: DbType
  host: string
  port: number
  database?: string
}

export interface StructureInfo {
  connectionId: number
  database: string
  tableName: string
  dbType: DbType
  nodeType: NodeType
}

// 严格类型检查
export function assertStructureInfo(info: unknown): asserts info is StructureInfo {
  if (!info || typeof info !== 'object') {
    throw new Error('Invalid StructureInfo')
  }
  // ... 类型检查逻辑
}

5. 状态持久化策略

// stores/db-cli/structure.ts
import { defineStore } from 'pinia'
import { useStorage } from '@vueuse/core'

export const useStructureStore = defineStore('structure', () => {
  // 使用 localStorage 持久化(可选)
  const lastStructureInfo = useStorage<StructureInfo | null>(
    'db-cli-last-structure-info',
    null
  )
  
  // 恢复上次查看的结构
  function restoreLastStructure() {
    if (lastStructureInfo.value) {
      loadStructure(lastStructureInfo.value)
    }
  }
  
  // 在 loadStructure 中保存
  async function loadStructure(params: StructureInfo) {
    // ... 加载逻辑
    info.value = params
    lastStructureInfo.value = params // 自动保存到 localStorage
  }
  
  return { /* ... */ }
})

6. 错误边界和恢复机制

// stores/db-cli/structure.ts
export const useStructureStore = defineStore('structure', () => {
  const retryCount = ref(0)
  const maxRetries = 3
  
  async function loadStructure(params: StructureInfo, retry = 0) {
    try {
      // ... 加载逻辑
      retryCount.value = 0 // 成功后重置
    } catch (err) {
      if (retry < maxRetries) {
        console.warn(`[StructureStore] 重试加载 (${retry + 1}/${maxRetries})`)
        await new Promise(resolve => setTimeout(resolve, 1000 * (retry + 1)))
        return loadStructure(params, retry + 1)
      }
      // 超过重试次数,记录错误
      error.value = `加载失败(已重试 ${maxRetries} 次): ${err}`
    }
  }
  
  return { /* ... */ }
})

7. 组件级状态同步检查

// composables/useStateSync.ts
import { watch, nextTick } from 'vue'

/**
 * 状态同步检查器
 * 确保 Store 状态和组件 props 保持同步
 */
export function useStateSync<T>(
  storeValue: () => T,
  propValue: () => T,
  name: string
) {
  if (!import.meta.env.DEV) return
  
  watch(
    () => storeValue(),
    (storeVal) => {
      nextTick(() => {
        const propVal = propValue()
        if (storeVal !== propVal) {
          console.error(
            `[StateSync] ${name} 不同步!`,
            `Store: ${JSON.stringify(storeVal)}`,
            `Prop: ${JSON.stringify(propVal)}`
          )
        }
      })
    },
    { deep: true }
  )
}

8. 测试策略

// stores/db-cli/structure.test.ts
import { setActivePinia, createPinia } from 'pinia'
import { useStructureStore } from './structure'

describe('StructureStore', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })
  
  it('应该正确加载结构数据', async () => {
    const store = useStructureStore()
    
    await store.loadStructure({
      connectionId: 1,
      database: 'test',
      tableName: 'users',
      dbType: 'mysql',
      nodeType: 'table'
    })
    
    expect(store.loading).toBe(false)
    expect(store.data).not.toBeNull()
    expect(store.info).not.toBeNull()
  })
  
  it('应该在加载失败时设置错误', async () => {
    // ... 测试错误处理
  })
})

迁移步骤

  1. 阶段一:引入 Pinia

    • 安装依赖
    • 创建基础 Store 结构
    • 在主应用初始化 Pinia
  2. 阶段二:迁移状态

    • 先迁移 structure store当前问题所在
    • 逐步迁移其他 stores
    • 保持双写一段时间Composable + Store
  3. 阶段三:清理代码

    • 移除旧的 Composables
    • 统一使用 Store
    • 添加类型定义
  4. 阶段四:优化和测试

    • 添加状态调试工具
    • 编写单元测试
    • 性能优化

优势总结

  1. 单一数据源:所有状态集中在 Store避免分散
  2. 自动响应式Pinia 自动处理响应式,无需手动 computed
  3. 开发工具Pinia DevTools 可以可视化状态变化
  4. 类型安全TypeScript 支持更好
  5. 易于测试Store 可以独立测试
  6. 状态持久化:内置支持 localStorage/sessionStorage
  7. 调试友好:可以回放状态变更历史

注意事项

  1. 不要过度使用:简单的局部状态仍可使用 ref/reactive
  2. 避免循环依赖Store 之间不要相互依赖
  3. 性能考虑:大数据量使用 shallowRef
  4. SSR 兼容:如需 SSR注意状态初始化

参考资料