Cherry Studio Preference 系统实战指南:usePreference 钩子与 PreferenceService 的完整用法

发布时间:2026/9/20 11:58:52
Cherry Studio Preference 系统实战指南:usePreference 钩子与 PreferenceService 的完整用法 Cherry Studio Preference 系统实战指南usePreference 钩子与 PreferenceService 的完整用法【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio本文以 Cherry Studio 仓库中的 Preference Usage Guide 为核心骨架系统讲解 React 侧usePreference/useMultiplePreferences两个 Hook、渲染进程单例preferenceService与主进程生命周期服务PreferenceService的完整调用方式并结合 源码、渲染进程服务 与 主进程服务 深入剖析乐观更新、回滚、跨窗口同步等底层机制。读完本文你将能够在 Cherry Studio 的任何功能模块中正确读写主题、语言、字号、功能开关等用户设置并理解何时选择乐观或悲观更新策略。一、Preference 是什么定位与边界Cherry Studio 的数据体系分为四套系统详见 Data System Reference 的决策表BootConfigService进程级早期配置、CacheService可再生临时数据、PreferenceService用户设置与DataApiService业务数据。Preference 的定位是存储小而固定键位的用户设置必须在多个窗口间持久化并保持一致适用于主题、语言、字号、功能开关、快捷键等场景不适用于用户创建的记录、大型集合或可再生的 UI 状态。从架构看参见 Preference Overviewpreference表以(scope, key)为联合主键值以 JSON 存储当前运行时 scope 恒为default其表结构定义在 db/schemas/preference.tsexport const preferenceTable sqliteTable( preference, { scope: text().notNull().default(default), // scope 预留扩展当前仅支持 default key: text().notNull(), value: text({ mode: json }), ...createUpdateTimestamps }, (t) [primaryKey({ columns: [t.scope, t.key] })] )src/shared/data/preference/preferenceTypes.ts定义了完整的类型体系PreferenceKeyType仅覆盖 SQLite 后端键UnifiedPreferenceKeyType额外包含带BootConfig.前缀的公开 BootConfig 键PreferenceUpdateOptions则是唯一的更新策略选项export type PreferenceUpdateOptions { optimistic: boolean }每个键都有生成好的默认值因此调用方即使在渲染进程缓存尚未加载完成时也能立刻观察到一个合理值而不是undefined。二、React HooksusePreference 与 useMultiplePreferencesReact 组件中推荐始终使用 Hook由 Hook 自动管理订阅生命周期。实现位于 src/renderer/data/hooks/usePreference.ts底层基于 React 18 的useSyncExternalStore能获得实时的跨窗口同步。2.1 usePreference单个键usePreference接收一个合法键返回[value, setValue]。value 会套用生成的默认值绝不会是undefinedsetter 返回 Promiseimport { usePreference } from data/hooks/usePreference const [theme, setTheme] usePreference(ui.theme_mode) await setTheme(dark)从源码看usePreference.tsHook 内部用useSyncExternalStore订阅该键的变更preferenceService.subscribeChange(key)首次渲染时若缓存未命中rawValue undefined通过useEffect异步发起preferenceService.get(key)拉取对外暴露的值rawValue ! undefined ? rawValue : getDefaultValue(key)即默认值兜底绝不返回undefinedsetter 内部调用preferenceService.set(key, newValue, options)失败时记录日志并重新抛出由调用方决定如何提示用户。2.2 悲观更新等待持久化确认更新默认是乐观的DEFAULT_PREFERENCE_OPTIONS { optimistic: true }。当 UI 必须等到持久化确认后才能呈现已保存状态时传入{ optimistic: false }const [developerMode, setDeveloperMode] usePreference(app.developer_mode.enabled, { optimistic: false }) await setDeveloperMode(true)典型的悲观场景包括密码、WebDAV 凭据等关键设置例如源码注释中给出的data.backup.webdav.pass而主题、字号等纯 UI 偏好适合乐观更新以换取即时反馈。2.3 useMultiplePreferences批量读取与批量更新当需要一组相关设置时用useMultiplePreferences。它接收一个「本地名 → 偏好键」的映射对象返回[values, updateValues]import { useMultiplePreferences } from data/hooks/usePreference const [settings, updateSettings] useMultiplePreferences({ theme: ui.theme_mode, language: app.language, fontSize: chat.message.font_size }) await updateSettings({ theme: system, language: en-US })源码实现要点usePreference.ts单次订阅聚合内部将映射值转为 keyList为每个键调用一次subscribeChange并聚合退订函数相比多个usePreference更高效快照去重通过lastSnapshotRef缓存最近快照仅当任一值真正变化时才产生新对象避免useSyncExternalStore无限循环初始加载对未缓存键调用getMultipleRaw一次性批量拉取局部更新updateSettings只更新传入的键未传的键保持原值默认值兜底exposedValues同样对每个键应用getDefaultValue。2.4 键映射对象的稳定性要求key-map 对象必须保持引用稳定模块常量或useMemo因为它既是 Hook 的依赖也是订阅定义。若由动态输入构造务必用useMemo包裹否则每次渲染都会重建订阅。三、渲染进程 Service非 React 代码的直接访问非 React 的渲染进程代码如事件监听、工具函数、服务层可以直接使用单例preferenceServicesrc/renderer/data/PreferenceService.ts 导出。3.1 基本读写import { preferenceService } from data/PreferenceService const theme await preferenceService.get(ui.theme_mode) const settings await preferenceService.getMultiple({ language: app.language, fontSize: chat.message.font_size }) await preferenceService.set(ui.theme_mode, dark) await preferenceService.setMultiple({ app.language: en-US, chat.message.font_size: 16 })注意两处命名细节getMultiple()接收本地名到键的对象返回以本地名命名的结果getMultipleRaw(keys)接收键数组返回以 Preference 键本身命名的对象——仅在结果必须按键索引时使用。getMultipleRaw的缓存逻辑PreferenceService.ts先筛出已缓存键对未缓存键批量走一次 IPCgetMultipleRaw拉取失败时用getDefaultValue填充默认值兜底随后对全部请求键执行一次批量子订阅内部自动去重。3.2 同步缓存读取与订阅渲染进程服务内部维护cache对象提供同步读取接口getCachedValue(key)与isCached(key)。subscribeChange(key)是柯里化的先传键、再传回调返回退订函数const unsubscribe preferenceService.subscribeChange(ui.theme_mode)(() { const theme preferenceService.getCachedValue(ui.theme_mode) logger.info(Theme changed, { theme }) })务必在持有者销毁时调用返回的退订函数React 代码应使用 HookHook 会自动管理该生命周期useSyncExternalStore会在组件卸载时调用退订。3.3 乐观更新的底层实现渲染进程服务是乐观更新机制的真正执行者其核心状态包括optimisticValues跟踪乐观值、原始值、时间戳与 requestId与requestQueues同键并发更新队列。整体流程PreferenceService.tssetOptimistic生成唯一requestId并入队同键并发请求串行处理防止竞态executeOptimisticUpdate立即更新本地 cache 并通知监听者UI 瞬间响应调用window.api.preference.set(key, value)持久化到主进程成功则confirmOptimistic清除乐观状态并处理下一个排队请求失败则rollbackOptimistic恢复首次请求记录的原始值并重新通知然后抛出异常。批量的setMultipleOptimistic采用同一策略PreferenceService.ts为每个键生成batchRequestId_key形式的独立 requestId任何一个键失败都会回滚整个批次中所有受影响键的原始值。此外onChanged监听器用isEqual深比较过滤掉自己写入产生的 IPC 回显避免无谓重渲染。四、主进程 Service生命周期托管与同步读主进程代码通过application容器获取生命周期托管的服务实例实现见 src/main/data/PreferenceService.tsimport { application } from application const preferences application.get(PreferenceService) const theme preferences.get(ui.theme_mode) const { language, fontSize } preferences.getMultiple({ language: app.language, fontSize: chat.message.font_size }) await preferences.set(ui.theme_mode, dark)4.1 同步读、异步写的设计主进程get()/getMultiple()是同步的内存缓存读取服务初始化时一次性从 SQLite 加载全部scope default的键到内存见onInitPreferenceService.ts。写入返回 Promise 的原因在于better-sqlite3 的写入本身是同步的但写入后需要跨进程广播变更通知给所有已订阅的渲染窗口notifyChange见 PreferenceService.ts这部分是异步语义。服务声明PreferenceService.ts展示了其在生命周期中的位置Injectable(PreferenceService) ServicePhase(Phase.BeforeReady) DependsOn([DbService]) export class PreferenceService extends BaseService {Phase.BeforeReady在应用 Ready 之前完成初始化保证窗口创建后立即可读DependsOn([DbService])依赖数据库服务先行就绪。4.2 统一键路由resolveKey主进程是统一偏好 API 的唯一入口闸门。resolveKey()PreferenceService.ts把每个键路由到对应存储键存储普通生成键如ui.theme_modeSQLite preference 行公开BootConfig.app.*键文件后端bootConfigService内部BootConfig.temp.*键在统一 Preference 边界直接拒绝路由规则实现在 src/shared/data/preference/preferenceUtils.tsisBootConfigKey检测BootConfig.前缀isPublicBootConfigKey依据DefaultBootConfig自动派生的白名单过滤掉temp.*内部状态与未知键getDefaultValue则同时覆盖 DB 键与 BootConfig 键的默认值查询。setMultiple会先解析并校验所有键再执行任何写入PreferenceService.ts批次中若混有内部键会被原子性拒绝随后 BootConfig 写入与 SQLite 事务是两套独立存储因此并非一次跨存储的原子提交。另外主进程对未变化的键会跳过数据库写入isEqual比较并只在成功后发布通知。4.3 主进程订阅与资源释放主进程订阅签名与渲染进程不同subscribeChange(key, callback)。生命周期服务必须把返回的 disposable 注册到自身生命周期以便服务停止时自动释放this.registerDisposable( preferences.subscribeChange(ui.theme_mode, (theme) { logger.info(Theme changed, { theme }) }) )主进程还维护了窗口级订阅表windowSubscriptions: MapwindowId, SetkeysnotifyChange会精确推送给订阅了该键的窗口对已销毁的窗口自动清理订阅setupWindowCleanup每 5 分钟巡检一次见 PreferenceService.ts。广播刻意不排除写入方窗口——写入方通过渲染侧onChanged的深比较去重从而在多窗口并发写竞争下保证各窗口缓存与数据库最终一致。五、失败语义Failure Semantics综合两个进程的实现Preference 的失败行为可归纳为四条明确规则乐观写入渲染进程立即更新本地缓存并通知 React若主进程拒绝写入则回滚到受保护的原始值悲观写入主进程确认持久化之前旧缓存值保持可见UI 不会呈现未确认的已保存状态批量乐观回滚批次内任一键失败恢复该批次中每一个受影响键的原始值Hook setter 重新抛出异常usePreference与useMultiplePreferences的 setter 捕获错误并throw由调用方决定如何向用户提示。六、新增一个 Preference 键的正确姿势绝不直接编辑生成文件src/shared/data/preference/preferenceSchemas.tsPreferenceSchemas接口与DefaultPreferences对象均为自动生成或DefaultPreferences。正确流程是修改生成器输入并重新生成详见 Preference Schema Guide在v2-refactor-temp/tools/data-classify/data/target-key-definitions.json新 v2 设置或classification.json简单 v1→v2 映射中添加条目status必须为classified键名遵循namespace.category.key_name规范至少两段小写点分、下划线分词由data-schema-key/valid-keylint 规则强制共享的联合类型、枚举、品牌类型放入preferenceTypes.ts生成器输入中以PreferenceTypes.X引用在v2-refactor-temp/tools/data-classify目录运行生成管线cd v2-refactor-temp/tools/data-classify npm run generate该命令会同步重新生成四个耦合产物preferenceSchemas.ts、bootConfigSchemas.ts、PreferencesMappings.ts与BootConfigMappings.ts。之后即可通过正常 API 消费新键import { usePreference } from data/hooks/usePreference const [enabled, setEnabled] usePreference(feature.my_feature.enabled)修改键后运行pnpm lint它会检查生成类型、键命名、格式以及所有 Preference 调用点。七、调试与安全统计接口主进程getStats(details?)报告键数与订阅数见 PreferenceService.ts。摘要形式含总键数、主进程订阅数、窗口订阅数与活动窗口数details: true的详细形式包含逐键订阅数据适合诊断场景且源码注明该接口资源开销较大、建议仅在开发环境使用IPC 安全所有Preference_*IPC 入口Preference_Get、Preference_Set、Preference_GetMultipleRaw、Preference_SetMultiple、Preference_GetAll、Preference_Subscribe都经assertTrustedSender的validateSender源信任校验PreferenceService.ts拒绝不受信任发送者渲染进程代码应始终使用服务与 Hook而不是直接调用这些通道渲染侧调试服务暴露getPendingOptimisticUpdates()查看所有未确认的乐观更新以及preloadAll()/isFullyCached()用于启动预热与状态查询。八、小结Cherry Studio 的 Preference 系统在三个层次提供了自洽的访问方式React 组件用 Hook自动管理订阅与默认值、非 React 渲染代码用单例 service支持乐观/悲观策略与柯里化订阅、主进程用生命周期服务同步读、异步写、跨窗口广播。理解resolveKey的 BootConfig 路由与乐观更新的回滚机制能帮助你在接入主题、语言、字号、功能开关等设置时写出既响应迅速又行为正确的代码。进一步阅读可参考 Preference Overview、Preference Schema Guide 与 Data System Reference。【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询