11 KiB
11 KiB
架构改进方案:状态管理优化
问题分析
当前遇到的问题属于"响应式状态同步灾难",主要问题:
- 状态分散:多个 Composables 各自管理状态,难以追踪数据流
- 响应式失效:computed/watch 在复杂场景下失效,难以调试
- 数据传递复杂:props/computed/provide 多层传递,容易丢失
- 缺乏状态快照:无法回溯状态变化历史
- 调试困难:大量 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 () => {
// ... 测试错误处理
})
})
迁移步骤
-
阶段一:引入 Pinia
- 安装依赖
- 创建基础 Store 结构
- 在主应用初始化 Pinia
-
阶段二:迁移状态
- 先迁移 structure store(当前问题所在)
- 逐步迁移其他 stores
- 保持双写一段时间(Composable + Store)
-
阶段三:清理代码
- 移除旧的 Composables
- 统一使用 Store
- 添加类型定义
-
阶段四:优化和测试
- 添加状态调试工具
- 编写单元测试
- 性能优化
优势总结
- 单一数据源:所有状态集中在 Store,避免分散
- 自动响应式:Pinia 自动处理响应式,无需手动 computed
- 开发工具:Pinia DevTools 可以可视化状态变化
- 类型安全:TypeScript 支持更好
- 易于测试:Store 可以独立测试
- 状态持久化:内置支持 localStorage/sessionStorage
- 调试友好:可以回放状态变更历史
注意事项
- 不要过度使用:简单的局部状态仍可使用 ref/reactive
- 避免循环依赖:Store 之间不要相互依赖
- 性能考虑:大数据量使用 shallowRef
- SSR 兼容:如需 SSR,注意状态初始化