486 lines
11 KiB
Markdown
486 lines
11 KiB
Markdown
# 架构改进方案:状态管理优化
|
||
|
||
## 问题分析
|
||
|
||
当前遇到的问题属于"响应式状态同步灾难",主要问题:
|
||
|
||
1. **状态分散**:多个 Composables 各自管理状态,难以追踪数据流
|
||
2. **响应式失效**:computed/watch 在复杂场景下失效,难以调试
|
||
3. **数据传递复杂**:props/computed/provide 多层传递,容易丢失
|
||
4. **缺乏状态快照**:无法回溯状态变化历史
|
||
5. **调试困难**:大量 console.log 散布在代码中,难以系统化
|
||
|
||
## 改进方案
|
||
|
||
### 1. 引入 Pinia 统一状态管理
|
||
|
||
#### 1.1 安装 Pinia
|
||
|
||
```bash
|
||
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** - 表结构状态管理
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
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**
|
||
|
||
```typescript
|
||
<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** - 开发工具
|
||
|
||
```typescript
|
||
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**
|
||
|
||
```typescript
|
||
// 统一类型定义
|
||
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. 状态持久化策略
|
||
|
||
```typescript
|
||
// 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. 错误边界和恢复机制
|
||
|
||
```typescript
|
||
// 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. 组件级状态同步检查
|
||
|
||
```typescript
|
||
// 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. 测试策略
|
||
|
||
```typescript
|
||
// 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,注意状态初始化
|
||
|
||
## 参考资料
|
||
|
||
- [Pinia 官方文档](https://pinia.vuejs.org/)
|
||
- [Vue 3 Composition API 最佳实践](https://vuejs.org/guide/extras/composition-api-faq.html)
|