docs/archives/126-submode-persistence/experience.md
关键洞察(来自用户):
"基础模式也应该有自己的存储,这个也应该分开...因为这两个功能模式本质上控制的是不同的,只是当前他们的子模式碰巧都叫 系统/用户提示词优化而已。"
经验总结:
反模式:
// ❌ 错误: 共享状态
const selectedOptimizationMode = ref<'system' | 'user'>('system')
// 基础模式和上下文模式都使用同一个变量
// 导致切换功能模式时状态混乱
最佳实践:
// ✅ 正确: 完全独立的状态
const { basicSubMode } = useBasicSubMode(services)
const { proSubMode } = useProSubMode(services)
// 各自独立存储,互不影响
问题背景: Composable可能被多次调用,如何确保状态唯一?
解决方案:
let singleton: {
mode: Ref<SubModeType>
initialized: boolean
initializing: Promise<void> | null
} | null = null
export function useSubMode(services: Ref<AppServices | null>) {
if (!singleton) {
singleton = {
mode: ref<SubModeType>('default'),
initialized: false,
initializing: null
}
}
// ...
}
关键点:
singleton 在模块作用域,确保全局唯一常见陷阱:
// ❌ 错误: 每次调用都创建新状态
export function useSubMode() {
const mode = ref('default') // 每次都是新的!
// ...
}
问题: 如果多个组件同时调用 ensureInitialized(),会导致重复读取存储。
解决方案:
const ensureInitialized = async () => {
// 第一层防护:已初始化
if (singleton!.initialized) return
// 第二层防护:正在初始化(防抖)
if (singleton!.initializing) {
await singleton!.initializing
return
}
// 记录初始化Promise
singleton!.initializing = (async () => {
try {
// 实际初始化逻辑
} finally {
singleton!.initialized = true
singleton!.initializing = null
}
})()
await singleton!.initializing
}
关键机制:
initialized + initializing为什么需要只读?
实现方式:
import { readonly } from 'vue'
return {
// ✅ 只读: 外部不能直接修改
basicSubMode: readonly(singleton.mode) as Ref<BasicSubMode>,
// ✅ 修改器: 通过setter更新并持久化
setBasicSubMode: async (mode: BasicSubMode) => {
singleton!.mode.value = mode
await setPreference(STORAGE_KEY, mode)
}
}
避免的陷阱:
// ❌ 错误: 直接暴露可写状态
return {
basicSubMode: singleton.mode, // 外部可以直接修改!
// ...
}
// 导致问题:
basicSubMode.value = 'user' // 修改了状态但没有持久化!
场景: 导航栏的选择器在 App.vue,但 ImageWorkspace 内部需要知道切换事件。
方案对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Props传递 | 简单直接 | 组件耦合高 | 父子组件 |
| Provide/Inject | 解耦 | 需要共同父组件 | 深层嵌套 |
| 自定义事件 | 完全解耦 | 需要手动管理 | 跨层级通信 |
| Composable共享 | 类型安全 | 需要单例模式 | 全局状态 |
本项目选择:
自定义事件实现:
// 发送端(App.vue)
window.dispatchEvent(new CustomEvent("image-submode-changed", {
detail: { mode }
}))
// 接收端(ImageWorkspace.vue)
const handleImageSubModeChanged = (e: CustomEvent) => {
const { mode } = e.detail
if (mode && mode !== imageMode.value) {
handleImageModeChange(mode)
}
}
onMounted(() => {
window.addEventListener("image-submode-changed", handleImageSubModeChanged as EventListener)
})
onBeforeUnmount(() => {
window.removeEventListener("image-submode-changed", handleImageSubModeChanged as EventListener)
})
问题发现: 图像模式刷新后文件上传按钮不显示
原因分析:
导航栏层 (App.vue + useImageSubMode)
✅ 从 UI_SETTINGS_KEYS.IMAGE_SUB_MODE 恢复
✅ 导航栏显示正确
组件内部层 (ImageWorkspace + useImageWorkspace)
❌ 没有从存储恢复
❌ 始终使用硬编码默认值 'text2image'
❌ v-if="imageMode === 'image2image'" 永远为 false
解决方案: 两层都从同一个存储键恢复
// useImageWorkspace.ts
const restoreSelections = async () => {
// ... 其他恢复 ...
// ✅ 从全局存储恢复
const savedImageMode = await getPreference(
UI_SETTINGS_KEYS.IMAGE_SUB_MODE, // 与导航栏使用同一个键!
"text2image",
)
if (savedImageMode === "text2image" || savedImageMode === "image2image") {
state.imageMode = savedImageMode
}
}
经验教训:
挑战: 现有代码大量使用 selectedOptimizationMode 和 contextMode
策略: 保留旧变量,与新Composable同步
// 新状态
const { basicSubMode, setBasicSubMode } = useBasicSubMode(services)
const { proSubMode, setProSubMode } = useProSubMode(services)
// 旧变量(保留兼容)
const selectedOptimizationMode = ref<OptimizationMode>("system")
// 切换时同步
const handleBasicSubModeChange = async (mode: OptimizationMode) => {
await setBasicSubMode(mode as BasicSubMode)
selectedOptimizationMode.value = mode // ✅ 同步旧变量
}
优点:
长期计划:
用途: 确保全局唯一状态
实现: 模块级变量 + 惰性初始化
用途: 控制状态访问
实现: readonly() 包装 + setter方法
用途: 跨组件通信
实现: 自定义事件 + addEventListener
用途: 根据功能模式选择不同处理
实现: if-else分支 + 独立的Composable
// ❌ 错误
const { basicSubMode, setBasicSubMode } = useBasicSubMode(services)
setBasicSubMode('user') // 可能在初始化前调用!
// ✅ 正确
const { basicSubMode, setBasicSubMode, ensureInitialized } = useBasicSubMode(services)
await ensureInitialized() // 先初始化
await setBasicSubMode('user')
// ❌ 错误
basicSubMode.value = 'user' // TypeScript会报错!
// ✅ 正确
await setBasicSubMode('user')
// ❌ 错误: 只注册不清理
onMounted(() => {
window.addEventListener("event", handler)
})
// ✅ 正确: 清理避免内存泄漏
onMounted(() => {
window.addEventListener("event", handler)
})
onBeforeUnmount(() => {
window.removeEventListener("event", handler)
})
// ❌ 错误: 类型混用
const mode: ProSubMode = basicSubMode.value // 类型不匹配!
// ✅ 正确: 类型转换
const mode = basicSubMode.value as OptimizationMode
本架构适用于以下场景:
添加新功能模式时:
storage-keys.ts 添加存储键types.ts 定义类型useXxxSubMode.tsselectedOptimizationMode 变量contextMode 和 proSubMode文档版本: v1.0
最后更新: 2025-10-22
贡献者: Claude & 用户