- 添加模板管理故障排除清单,以帮助用户解决模板管理中遇到的常见问题。 - 统一服务注入逻辑,移除不必要的props定义,增强错误处理机制。 - 优化模板管理,统一服务注入与存储键管理。 - 更新 `TemplateSelect.vue`,移除 `services` prop,改用 `inject` 注入服务。 - 整合 `useTemplateManager`,统一模板选择保存逻辑及存储键管理。 - 新增 `storage-keys.ts`,集中管理存储键常量,避免重复定义,便于维护与遍历。 - 更新相关组件以适配新的模板管理方式,确保模板选择状态正确保存和恢复。 - 修正了模板类型错误的问题,确保在管理界面切换分类后添加的模板类型与当前显示的分类一致。 - 修复了模板管理器打开位置错误的问题,确保从不同入口打开模板管理器时,定位到正确的分类。 - 优化了模板保存和导入逻辑,增加了错误处理和提示。 - 确保所有异步模板操作都使用了 `await` 关键字,避免潜在的时序问题。 - 移除了 `usePromptOptimizer` 中 `selectedOptimizationMode` 的默认值,强制传入该参数。 - 优化了 `TemplateSelect` 组件中 `optimizationMode` prop 的处理,设为 `required`。
12 KiB
服务单例模式重构计划 (Singleton Refactor Plan)
1. 问题背景
经过深入排查,我们发现当前架构存在一个核心缺陷:服务实例在模块导入时被过早创建(Eager Instantiation),并作为单例(Singleton)在多个包之间导出和传递。
这导致了以下严重问题:
- "幽灵"服务:在Electron的渲染进程中,意外地创建了一套基于
Dexie(IndexedDB) 的Web端服务。这些服务虽然未被最终使用,但占用了资源并造成了数据混乱的假象。 - 状态不一致:由于服务实例的创建不感知运行环境,导致UI进程(看到的是Web版实例状态)和主进程(实际执行逻辑)之间存在状态不一致。
- 架构耦合:
@prompt-optimizer/ui包不必要地导出了核心服务实例,使其职责不清,更像一个服务中转站而非纯UI库。 - 测试困难:单例模式使得在测试中隔离和模拟服务变得非常困难。
2. 重构目标
本次重构的核心目标是实现服务的延迟初始化(Lazy Initialization)和依赖注入(Dependency Injection),确保只在需要时、在正确的环境中、创建唯一正确的服务实例。
- 移除单例导出:任何包(
core,ui)都不应再导出预先创建好的服务实例。 - 统一初始化入口:创建一个唯一的、环境感知的应用初始化器。
- 清晰的职责划分:
core只提供服务类和工厂函数,ui只提供UI组件和Hooks,应用入口(App.vue)负责编排。
3. 实施计划与成果
本次重构已圆满完成。所有核心服务均已从单例模式迁移至工厂函数和依赖注入模式,实现了按需、按环境创建服务实例的目标。
阶段一:改造 Core 包,移除单例导出 (已完成) ✅
目标:将所有服务的单例导出模式(export const service = new Service()) 改为工厂函数模式 (export function createService())。
步骤:
services/storage/factory.ts: 移除storageProvider单例导出。services/model/manager.ts: 移除modelManager单例导出,并使其工厂函数接收依赖。services/template/manager.ts: 移除templateManager单例导出,并使其工厂函数接收依赖。services/history/manager.ts: 移除historyManager单例导出,并使其工厂函数接收依赖。index.ts: 更新入口文件,确保只导出模块和工厂函数。
期间发现的偏差及处理:
-
TemplateManager的深层依赖:- 发现:
TemplateManager依赖另一个未被发现的单例templateLanguageService。 - 措施:对
services/template/languageService.ts进行了相同的重构,移除了单例并创建了createTemplateLanguageService工厂函数。相应地,createTemplateManager现在接收storageProvider和languageService两个实例作为参数。
- 发现:
-
index.ts的导出清理:- 发现:
index.ts导出了属于应用层的electron-proxy.ts文件。 - 措施:清理了
index.ts,移除了这些不应由core包暴露的导出项,使 API 更纯净。
- 发现:
阶段二:净化 UI 包,停止导出服务 (已完成) ✅
目标:让 @prompt-optimizer/ui 回归其纯粹的UI库职责。
packages/ui/src/index.ts- 移除所有从
@prompt-optimizer/core重新导出的服务实例。UI包已回归纯UI库职责。
- 移除所有从
阶段三:创建统一的应用初始化器 (已完成) ✅
目标:将所有初始化逻辑收敛到一个可复用的 composable 中。
- 文件:
packages/ui/src/composables/useAppInitializer.ts(新建)- 创建文件并实现以下逻辑:
- 导入所有
create...工厂函数和 Electron 代理类。 - 定义
services和isInitializingrefs。 - 在
onMounted中,通过isRunningInElectron()判断环境:- 如果为 Electron:创建所有服务的 代理 实例。
- 如果为 Web:创建所有 真实 服务实例(包括
storageProvider)。 - 将所有服务实例聚合到
servicesref 中。 - 更新
isInitializing状态。
- 导入所有
- 创建文件并实现以下逻辑:
阶段四:重构应用入口 (App.vue) (已完成) ✅
目标:让应用入口变得简洁,只负责消费初始化器返回的服务。
- 修改
packages/web/src/App.vue&packages/extension/src/App.vue- 完成: Web端和插件端的应用入口已重构,消费
useAppInitializer返回的服务,实现了清晰的初始化流程。 - 深化: 进一步重构了
App.vue下的所有UI子组件(如ModelSelect,TemplateSelect等),使其不再直接导入服务单例,而是通过props或inject接收服务实例,彻底完成了UI层的架构统一。
- 完成: Web端和插件端的应用入口已重构,消费
4. 预期成果 (已达成)
- 无"幽灵"服务:
Dexie将只在Web环境下被创建一次。 - 清晰的数据流:依赖关系变为
useAppInitializer->App.vue->Components,单向且清晰。 - 健壮的初始化:所有服务都在正确的时机、以正确的配置被创建。
- 彻底解决状态不一致问题:因为服务实例的创建逻辑是统一且唯一的。
这个计划将从根本上解决我们发现的架构问题,为项目未来的可维护性和可扩展性奠定坚实的基础。
5. 重构反思与后续决策
本次重构成功地将核心服务从单例模式转换为了工厂函数模式,解决了环境隔离和状态不一致的根本问题。然而,在修复因此产生的大量测试失败的过程中,我们也总结出了一些宝贵的经验和需要进一步完善的设计决策:
5.1 关于强制调用 ensureInitialized()
- 现状反思: 当前设计要求调用者在获取
Manager实例后,必须手动调用await manager.ensureInitialized()来完成异步初始化。这虽然将实例的创建和初始化过程解耦,但也暴露了内部实现细节,增加了调用者的负担。 - 优化方向: 更理想的设计是让工厂函数(如
createTemplateManager)本身成为一个异步函数,内部处理完所有初始化逻辑后,直接返回一个完全可用的实例Promise<Manager>。这样调用者只需await一次,接口更简洁、封装性更好。 - 决策: 暂时接受当前的设计,但将其标记为未来可优化的点。当前的核心任务是稳定重构后的代码。
5.2 关于错误处理:坚持"快速失败"原则
- 问题发现: 重构后的
TemplateManager在初始化时若遇到存储错误,会静默地降级使用内置模板,而不是抛出错误。 - 决策: 这掩盖了底层的严重问题,违反了"快速失败"(Fail-fast)原则。我们决定修正此行为。
TemplateManager在初始化遇到存储访问等关键错误时,必须向上抛出异常。由应用的顶层逻辑来捕获并决定如何处理(如向用户报错、进入安全模式等)。
5.3 关于测试代码的严谨性
- 问题发现: 部分旧的单元测试不够严谨。
- 决策与成果: 已修复。在本次重构的测试修复阶段,重写了大量断言,使用
expect.objectContaining等方式增强了测试的稳定性和可靠性。所有核心测试已通过。
5.4 UI 层的连锁反应与应对
- 发现: 核心服务的"去单例化"重构,对上层 UI 和 Composable 的冲击比预期更大。原先直接导入单例的模式被破坏后,引发了包括
属性类型检查失败、响应式状态丢失和服务未初始化在内的一系列连锁问题。 - 应对: 我们为此制定了专门的
composables-refactor-plan.md和web-refactor-plan.md。核心对策是:1) 将返回多个ref的 Composable 重构为返回单个reactive对象,以解决属性传递问题。2) 在组件层级,通过provide/inject机制注入服务,减少了属性钻孔 (props drilling)。这次经历表明,底层架构的重大变更,必须伴随对上层应用影响的充分评估和细致的改造计划。
6. 详细修改清单
此清单中的所有项目均已在最近的提交中完成。
阶段一:改造 Core 包
-
文件:
packages/core/src/services/storage/factory.ts- 删除 (约 L125):
export const storageProvider = StorageFactory.createDefault();
- 删除 (约 L125):
-
文件:
packages/core/src/services/model/manager.ts- 删除 (约 L427):
export const modelManager = ... - 修改 (约 L428):
export function createModelManager(storageProvider?: IStorageProvider): ModelManager- 改为:
export function createModelManager(storageProvider: IStorageProvider): ModelManager - 移除:
storageProvider = storageProvider || StorageFactory.createDefault();
- 改为:
- 删除 (约 L427):
-
文件:
packages/core/src/services/template/manager.ts- 删除 (约 L300):
export const templateManager = ...
- 删除 (约 L300):
-
文件:
packages/core/src/services/history/manager.ts- 删除 (约 L230):
export const historyManager = ...
- 删除 (约 L230):
-
文件:
packages/core/src/services/data/manager.ts- 删除 (约 L80):
export const dataManager = ... - 修改 (构造函数):
constructor()->constructor(modelManager: IModelManager, templateManager: ITemplateManager, historyManager: IHistoryManager) - 修改 (工厂函数):
createDataManager()->createDataManager(modelManager: IModelManager, templateManager: ITemplateManager, historyManager: IHistoryManager)
- 删除 (约 L80):
阶段二:净化 UI 包
- 文件:
packages/ui/src/index.ts- 删除 (约 L45-53):
export { templateManager, modelManager, historyManager, dataManager, storageProvider, createLLMService, createPromptService } from '@prompt-optimizer/core' - 新增: 导出
createDataManager等其他必要的工厂函数。
- 删除 (约 L45-53):
阶段三:创建统一的应用初始化器
- 文件:
packages/ui/src/composables/useAppInitializer.ts(新建)- 创建文件并实现以下逻辑:
- 导入所有
create...工厂函数和 Electron 代理类。 - 定义
services和isInitializingrefs。 - 在
onMounted中,通过isRunningInElectron()判断环境:- 如果为 Electron:创建所有服务的 代理 实例。
- 如果为 Web:创建所有 真实 服务实例(包括
storageProvider)。 - 将所有服务实例聚合到
servicesref 中。 - 更新
isInitializing状态。
- 导入所有
- 创建文件并实现以下逻辑:
阶段四:重构应用入口
- 文件:
packages/web/src/App.vue&packages/extension/src/App.vue- 移除: 所有对
modelManager,templateManager,historyManager等服务单例的导入。 - 替换:
- 旧:
import { modelManager, ... } from '@prompt-optimizer/ui' - 新:
import { useAppInitializer } from '@prompt-optimizer/ui'
- 旧:
- 调用:
const { services, isInitializing } = useAppInitializer(); - 包裹: 在模板的根元素上使用
v-if="!isInitializing",并添加一个v-else的加载状态。 - 传递: 将
services.value作为 props 传递给需要的子组件,或在composable中使用services.value.modelManager等。 - 清理: 删除
onMounted中手动的初始化逻辑。
- 移除: 所有对