恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Nuclear 插件设置系统详解:用 api.Settings 定义、读写与订阅持久化配置
首页
资讯中心
/
Nuclear 插件设置系统详解:用 api.Settings 定义、读写与订阅持久化配置
Nuclear 插件设置系统详解:用 api.Settings 定义、读写与订阅持久化配置
发布时间:2026/9/13 23:12:43
Nuclear 插件设置系统详解用 api.Settings 定义、读写与订阅持久化配置【免费下载链接】nuclearStreaming music player that finds free music for you项目地址: https://gitcode.com/GitHub_Trending/nu/nuclearNuclear 的插件设置SettingsAPI 让插件能够以统一的方式定义用户偏好、读写配置值并订阅变更底层由 Tauri Store 持久化到磁盘。本文基于仓库中的 设置文档 与 plugin-sdk 源码、宿主实现完整讲解设置命名空间、五种定义类型、自定义 Widget、默认值/持久化语义以及端到端的插件示例读完即可在 Nuclear 插件中落地一套可持久化、可热更新的配置体系。1. 核心概念命名空间、类型、默认值与持久化设置系统的第一原则是命名空间隔离。应用会自动为设置 ID 加前缀插件代码里只需要传裸 id来源完全限定 ID存储键插件中传入的 id核心设置core.idcore.前缀由宿主自动添加插件设置plugin.pluginId.id仅传裸id如theme这个前缀逻辑在宿主端 settingsHost.ts 的normalizeId函数中实现const normalizeId (source: SettingSource, id: string): string { if (source.type plugin) { return plugin.${source.pluginId}.${id}; } return core.${id}; };其余核心概念类型内置类型为boolean | number | stringcustom类型可以存储任意 JSON 可序列化值对象、数组、null。默认值Defaults在用户未设置之前生效只有用户主动选择的值会被持久化。分类Categories任意字符串用于在设置 UI 中分组展示建议使用句子式大写如General、Appearance、Integrations。隐藏Hiddenhidden: true的设置仍会被存储但不会出现在标准设置 UI 中。典型场景是由其他界面控制的设置例如音量滑块——核心设置中的playback.volume就是hidden: true其真实操作入口是播放器上的音量滑杆见 coreSettings.ts。持久化值通过 Tauri 的 Store 插件写入磁盘。具体实现见 settingsStore.tsimport { LazyStore } from tauri-apps/plugin-store; const SETTINGS_FILE settings.json; const store new LazyStore(SETTINGS_FILE);应用启动时loadFromDisk()一次性把settings.json中所有条目读入内存Zustand store此后每次写入都是更新内存 写盘 save三步。2. 注册设置api.Settings.register插件在onLoad中批量声明自己的设置定义。register返回完全限定 ID 列表可用于确认哪些键已注册import type { NuclearPluginAPI } from nuclearplayer/plugin-sdk; export default { async onLoad(api: NuclearPluginAPI) { await api.Settings.register([ { id: theme, title: Theme, description: Choose your preferred theme, category: Appearance, kind: enum, options: [ { value: system, label: System }, { value: light, label: Light }, { value: dark, label: Dark }, ], default: system, }, { id: scrobbleEnabled, title: Enable scrobbling, category: Integrations, kind: boolean, default: false, widget: { type: toggle }, }, ]); }, };SDK 侧的register只是一个转发器真正的工作在宿主完成api/settings.tsregister(defs: SettingDefinition[]) { return this.#withHost((h) h.register(defs)); }宿主端 settingsStore.ts 的register会做三件事为每个定义补上命名空间前缀得到完全限定 ID、把sourcecore或plugin回填进定义、把定义存入definitions注册表最后返回fullyQualifiedIds。SDK 测试 settings.test.ts 明确验证了这一点const res await api.Settings.register(definitions); expect(res.registered).toContain(plugin.p1.feature.enabled);即id 为feature.enabled的设置在插件p1下存储键为plugin.p1.feature.enabled。3. 读取与写入订阅变更三个方法覆盖运行时最常用的操作// 读取值string | number | boolean | undefined const theme await api.Settings.getstring(theme); // 更新值 await api.Settings.set(theme, dark); // 订阅变更 const unsubscribe api.Settings.subscribestring(theme, (value) { console.log(Theme changed to, value); }); // 之后取消订阅 unsubscribe();默认值语义由 settingsStore.ts 的getValue实现getValue: (fullyQualifiedId) { const { values, definitions } get(); const currentValue values[fullyQualifiedId]; if (currentValue ! undefined) { return currentValue; // 1. 用户设置过的值优先 } return definitions[fullyQualifiedId]?.default; // 2. 否则回落到 default }因此规则是用户未设置时get(id)返回定义中的default若未声明 default 则为undefined用户一旦设置该值写入磁盘并在下次启动时优先于default生效两者都没有时返回undefined。写入路径setValue先更新内存状态再调用store.setstore.save()落盘所以订阅方内存中的其他监听者是同步感知到的而磁盘写入是异步完成的。订阅实现细节subscribe基于 Zustand 的 store 订阅并带值相等则不触发的去抖逻辑settingsHost.tssubscribe: (id, listener) { const fullyQualifiedId normalizeId(pluginSource, id); let previousValue useSettingsStore.getState().getValue(fullyQualifiedId); const unsubscribe useSettingsStore.subscribe((state) { const nextValue state.getValue(fullyQualifiedId); if (nextValue ! previousValue) { previousValue nextValue; listener(nextValue); } }); return unsubscribe; },注意这里用!比较原始值直接比对对象值则按引用比对监听器只在值实际变化时才会被回调。4. 全局设置跨插件与核心边界的读写普通get/set被锁定在本插件命名空间内而getGlobal/setGlobal使用完全限定 ID直接访问任意设置不加前缀// 读取核心设置完整 ID const shuffle await api.Settings.getGlobalboolean(core.playback.shuffle); // 读取其他插件的设置 const otherValue await api.Settings.getGlobalstring(plugin.other-plugin.apiKey); // 写入全局设置 await api.Settings.setGlobalboolean(core.playback.shuffle, true);SDK 测试明确验证了这个隔离边界同一个插件用get(core.theme.dark)读不到该值被自己的plugin.p1.前缀挡住但getGlobal(core.theme.dark)可以读到——const inaccessible await api.Settings.get(core.theme.dark); expect(inaccessible).toBeUndefined(); const accessible await api.Settings.getGlobal(core.theme.dark); expect(accessible).toBe(true);适用前提全局读取拿到的是另一个命名空间的值其语义类型、枚举取值由对方定义决定调用方需要自行保证 ID 拼写与类型假设正确。核心设置里可用的 ID 可以在 coreSettings.ts 中查到例如core.playback.shuffle、core.general.language、core.integrations.mcp.enabled等它们都带default因此getGlobal在未写入磁盘时也能取到默认值。5. SettingDefinition 全类型参考内置五种定义类型联合类型SettingDefinition定义于 types/settings.ts除custom外的四种内置类型如下type SettingCategory string; type BooleanSettingDefinition { id: string; title: string; description?: string; category: SettingCategory; kind: boolean; default?: boolean; hidden?: boolean; widget?: { type: toggle }; }; type NumberSettingDefinition { id: string; title: string; description?: string; category: SettingCategory; kind: number; default?: number; hidden?: boolean; widget?: | { type: slider; min?: number; max?: number; step?: number; unit?: string; startLabel?: string; endLabel?: string; } | { type: number-input; min?: number; max?: number; step?: number; unit?: string }; min?: number; max?: number; step?: number; unit?: string; }; type StringSettingDefinition { id: string; title: string; description?: string; category: SettingCategory; kind: string; default?: string; hidden?: boolean; widget?: | { type: text; placeholder?: string } | { type: password; placeholder?: string } | { type: textarea; placeholder?: string; rows?: number } | { type: info }; format?: text | url | path | token | language; pattern?: string; // 正则 minLength?: number; maxLength?: number; }; type EnumSettingDefinition { id: string; title: string; description?: string; category: SettingCategory; kind: enum; options: { value: string; label: string }[]; default?: string; hidden?: boolean; widget?: { type: select } | { type: radio }; };要点速查kindwidget 可选值额外约束字段booleantoggle—numberslider支持 min/max/step/unit/startLabel/endLabel、number-inputmin/max/step/unit顶层min/max/step/unit可单独声明stringtext、password、textarearows、info只读信息展示formattext/url/path/token/language、pattern正则、minLength、maxLengthenumselect、radiooptions必填{ value, label }数组string类型的format用于给 UI 提示输入形态widget: { type: info }表示该条目只用于展示例如核心设置中 Jam 会话的remoteUrl/apiUrl就是只读信息行。passwordwidget 适合 API Key 一类敏感输入配合format: token使用。6. 自定义设置与 Widgetkind: custom当内置 widget 不够用OAuth 流程、多字段表单、实时预览等用kind: custom引用一个已注册的 React 组件type CustomSettingDefinition { id: string; title: string; description?: string; category: SettingCategory; kind: custom; widgetId: string; default?: SettingValue; hidden?: boolean; };widgetId指向通过api.Settings.registerWidget()注册的 React 组件。组件通过 props 接收当前值、setter 与定义本身import type { NuclearPluginAPI, CustomWidgetProps } from nuclearplayer/plugin-sdk; import { FC } from react; const AuthWidget: FCCustomWidgetProps ({ value, setValue }) { const session value as { username: string } | undefined; if (session) { return spanConnected as {session.username}/span; } return ( button onClick{() setValue({ username: testuser })} Connect /button ); }; export default { async onEnable(api: NuclearPluginAPI) { api.Settings.registerWidget(auth, AuthWidget); await api.Settings.register([{ id: session, title: Account, category: Integrations, kind: custom, widgetId: auth, }]); }, async onDisable(api: NuclearPluginAPI) { api.Settings.unregisterWidget(auth); }, };CustomWidgetProps类型types/settings.tstype CustomWidgetPropsAPI unknown { value: SettingValue | undefined; setValue: (value: SettingValue) void; definition: CustomSettingDefinition; api: API; };关键机制与注意事项Widget ID 自动按插件 ID 命名空间隔离。从源码结构看宿主侧 widgetRegistry.ts 用plugin.${pluginId}.${widgetId}作为 Map 键因此两个插件各自注册名为auth的 widget 不会冲突const toKey (pluginId: string, widgetId: string) plugin.${pluginId}.${widgetId};SDK 的registerWidget会把自己的pluginId注入注册调用api/settings.tsregisterWidget(widgetId: string, component: CustomWidgetComponent) { if (!this.#widgetRegistry || !this.#pluginId) { throw new Error(Widget registry not available); } this.#widgetRegistry.register(this.#pluginId, widgetId, component); }结构化数据SettingValue接受任意 JSON 可序列化值自定义 widget 可以存储{ sessionKey: string, username: string }这样的对象。必须在onDisable中反注册如果某个 custom 设置引用的 widget 没有注册设置 UI 会抛错。宿主缺失时快速失败SDK 测试验证了所有Settings方法在 host 未注入时抛出Settings host not availablewidget 注册同理抛出Widget registry not available——这是插件宿主环境配置错误的清晰信号而不是静默失败。7. React 端快捷方式useSetting 钩子除了命令式 APISDK 还提供 React 钩子useSettingreact/useSetting.ts把订阅 初始读取 写入封装为一个状态 hook适合在插件 UI 组件中使用const [currentValue, setValue] useSetting(host, language);它的实现要点挂载时先subscribe再异步get取初始值若异步读取返回前订阅已推送过更新则丢弃初始值以避免竞态hasReceivedUpdate标记组件卸载时清理订阅并标记isMounted false防止卸载后 setState。8. 端到端示例一个完整的插件设置流程下面的示例综合了注册、读取默认行为、实时订阅与生命周期钩子展示设置系统在插件各阶段的典型用法对应 settings.md 的 End-to-end exampleimport type { NuclearPluginAPI } from nuclearplayer/plugin-sdk; export default { async onLoad(api: NuclearPluginAPI) { await api.Settings.register([ { id: apiKey, title: API Key, category: Account, kind: string, widget: { type: password }, format: token }, { id: language, title: Language, category: General, kind: enum, options: [ { value: en, label: English }, { value: fr, label: Français }, ], default: en }, { id: debug, title: Enable debug logs, category: Advanced, kind: boolean, default: false, hidden: true }, ]); const lang await api.Settings.getstring(language); if (lang fr) { // 初始化法语资源…… } api.Settings.subscribestring(language, (next) { // 实时切换翻译 }); }, async onEnable(api: NuclearPluginAPI) { const scrobbling await api.Settings.getboolean(scrobbleEnabled); if (scrobbling) { // 启动 scrobbling 服务 } }, };注意onLoad与onEnable的分工onLoad负责注册定义并做一次性初始化读取language决定加载哪套资源onEnable在插件启用时读取开关类设置scrobbleEnabled决定是否启动后台服务。9. API 参考速查表以下为 settings.md 中给出的完整参考类型与 types/settings.ts 保持一致// 命名空间内设置自动加 core. 或 plugin.pluginId. 前缀 api.Settings.register(defs: SettingDefinition[]): Promise{ registered: string[] } api.Settings.getT extends SettingValue(id: string): PromiseT | undefined api.Settings.setT extends SettingValue(id: string, value: T): Promisevoid api.Settings.subscribeT extends SettingValue(id: string, cb: (v: T | undefined) void): () void // 全局设置完全限定 ID不加前缀 api.Settings.getGlobalT extends SettingValue(id: string): PromiseT | undefined api.Settings.setGlobalT extends SettingValue(id: string, value: T): Promisevoid // 自定义 widget api.Settings.registerWidget(widgetId: string, component: CustomWidgetComponent): void api.Settings.unregisterWidget(widgetId: string): void // 类型 type SettingValue JsonSerializable | undefined; type JsonSerializable string | number | boolean | null | JsonSerializable[] | { [key: string]: JsonSerializable }; type SettingDefinition BooleanSettingDefinition | NumberSettingDefinition | StringSettingDefinition | EnumSettingDefinition | CustomSettingDefinition; type CustomWidgetComponentAPI unknown FCCustomWidgetPropsAPI;10. 小结与延伸Nuclear 的设置系统本质上是注册表 命名空间 磁盘持久化三层结构定义注册到内存注册表含默认值读写经过命名空间归一化后落到 Zustand 状态与 Tauri Store 的settings.json变更通过订阅机制在应用内广播。对插件作者而言实践要点可以归纳为四条插件内始终使用裸 id让宿主处理plugin.pluginId.前缀用户可见的开关用boolean toggle、数值用number slider/number-input、敏感串用string password format: token跨插件/核心边界一律走getGlobal/setGlobal并使用完全限定 IDcustom widget 在onEnable注册、onDisable反注册避免设置 UI 因缺失组件而报错。如需了解插件生命周期onLoad/onEnable/onDisable的整体机制可继续阅读 插件系统文档 与 插件开发入门核心设置项的完整清单如core.playback.volume的 slider 参数min: 0, max: 1, step: 0.01见 coreSettings.ts它是使用同一套SettingDefinition结构的最佳参照实现。【免费下载链接】nuclearStreaming music player that finds free music for you项目地址: https://gitcode.com/GitHub_Trending/nu/nuclear创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考