Nuclear 插件开发实战:从零创建、加载并验证你的第一个插件

Nuclear 插件开发实战:从零创建、加载并验证你的第一个插件 Nuclear 插件开发实战从零创建、加载并验证你的第一个插件【免费下载链接】nuclearStreaming music player that finds free music for you项目地址: https://gitcode.com/GitHub_Trending/nu/nuclear本篇以 Nuclear一个面向免费流媒体音乐的播放器官方文档 packages/docs/plugins/getting-started.md 为核心骨架完整走通创建一个裸插件 → 在应用内加载 → 验证 SDK 全链路的全过程。读完后你将掌握 Nuclear 插件的目录约定、package.json清单规范、生命周期钩子语义、设置项自动命名空间机制并能看懂 PluginLoader 背后的清单校验、TS 即时编译与模块沙箱实现。插件的本质磁盘上的一个文件夹官方文档开篇就给出了定义插件是磁盘上的文件夹包含一个package.json和入口文件应用会在运行时加载它并向你的代码提供nuclearplayer/plugin-sdk。这句看似简单但仓库源码印证了完整的运行时链路加载入口是 PluginLoader。构造函数接收插件目录路径load()依次执行读取并校验package.json清单 → 解析入口文件路径 → 读取必要时即时编译插件代码 → 在受控环境中执行代码 → 调用onLoad(api)启动阶段由 pluginBootstrap.ts 中的hydratePluginsFromRegistry()驱动它从注册表按installedAt排序逐个插件创建 loader 并加载若该插件处于启用状态则再调用enablePlugin每个插件拿到的api对象由 createPluginAPI.ts 组装——把Settings、Queue、Playback、Providers、Streaming、Metadata、Dashboard、Events、Shell、Http、Ytdlp、Logger等各域 API 挂到同一个NuclearPluginAPI实例上且settingsHost和loggerHost是按插件 ID 独立创建的createPluginSettingsHost(pluginId, displayName)。这意味着写插件不需要在应用工程内改任何代码只需按约定组织好文件夹Nuclear 自己会处理编译、执行和 API 注入。第一步创建插件目录与 package.json 清单官方文档建议在工作区外创建插件目录例如~/nuclear-plugins/hello-plugin然后在目录内执行npm init。最终package.json的最小可用形态如下直接继承自官方文档{ name: hello-plugin, version: 0.1.0, description: Minimal Nuclear plugin, author: Your Name, main: index.ts, nuclear: { displayName: Hello Plugin, categories: [other] } }清单的运行时校验规则这些字段不是建议而是被 Zod schema 强校验的。pluginManifest.ts 中定义了PackageJsonSchema必填name、version、description、author且都必须是长度 ≥ 1 的字符串。任何一个缺失或为空readManifest()会抛错Invalid package.json: ...插件加载直接失败可选main入口文件相对路径可选nuclear对象其内合法键为displayName、category旧版单值分类源码注释标注将在注册表迁移到categories后移除、categories、icon、permissions顶层和nuclear都采用passthrough()不会因额外键而报错但collectUnknownNuclearKeys会把nuclear下的未知键收集成警告nuclear contains unknown keys: ...permissions会被去重并排序出现重复项时追加警告Duplicate permissions removed.。校验通过后PluginLoader.buildMetadata() 会生成PluginMetadatadisplayName缺省回落到namecategories缺省为空数组permissions缺省为空数组。这些元数据会原样出现在应用的 Plugins 列表中。第二步编写入口文件 index.ts 与生命周期钩子官方文档给出的最小入口文件如下注意它演示了设置项注册、读写与订阅这三个 SDK 最基本的动作const CATEGORY Examples; module.exports { async onLoad(api) { await api.Settings.register([ { id: hello, title: Hello world, category: CATEGORY, kind: boolean, default: true } ]); const v await api.Settings.get(hello); await api.Settings.set(hello, !v); }, async onEnable(api) { api.Settings.subscribe(hello, () {}); } };官方文档特别强调应用会对 TS 即时编译不需要额外构建步骤。这一点在 pluginCompiler.ts 中有完整的工程实现值得了解因为它直接决定了插件作者能写什么、不能写什么编译只在入口文件后缀为.ts/.tsx时发生isTs判断纯 JS 入口会被 PluginLoader.readPluginCode() 直接以文本读取跳过编译编译器是esbuild-wasm运行在 Tauri webview 内通过globalThis上的单例状态__NUCLEAR_ESBUILD_WASM__保证initialize()每个 JS 上下文只执行一次以兼容 Vite HMR 反复重执行模块的场景入口源码通过stdin喂给 esbuild所有相对导入由名为tauri-fs的虚拟文件系统插件解析——文件内容全部经 Tauri 的readTextFile读取不触碰 Node 的 fs构建参数为bundle: true、format: cjs、jsx: automatic、target: [es2022]且external: [nuclearplayer/plugin-sdk]——SDK 不打包进插件 bundle运行时由应用注入每个入口的编译产物有缓存但缓存新鲜度判定会重新哈希上一次构建参与过的所有文件任何一个被改动都会触发重编译避免改了utils.ts却拿到旧 bundle 的问题。插件对象的形状Plugin shape官方文档给出了插件导出对象的类型定义这与 SDK 中 types.ts 的NuclearPlugin类型一致type Plugin { onLoad?(api: NuclearPluginAPI): void | Promisevoid; onEnable?(api: NuclearPluginAPI): void | Promisevoid; onDisable?(api: NuclearPluginAPI): void | Promisevoid; onUnload?(api: NuclearPluginAPI): void | Promisevoid; };四个钩子全部可选。结合 pluginBootstrap.ts 与 PluginLoader.load() 的调用时序可以确认插件被加载registry hydrate 或手动 Add Plugin时onLoad在代码求值后立即执行用户把开关拨到启用时执行onEnable用户禁用时执行onDisable从内存移除前执行onUnload。所以官方文档的加载说明onLoadruns at import time;onEnableruns when you enable是有源码依据的load()中if (instance.onLoad api) { await instance.onLoad(api); }而onEnable由应用 store 的enablePlugin流程触发。第三步在 Nuclear 中加载插件官方文档的加载步骤为打开 Nuclear → Preference → Plugins左侧边栏点击Add Plugin选择你的插件文件夹打开开关启用。从源码看Add Plugin 之后插件信息会写入注册表。pluginRegistry.ts 使用 Tauri 的LazyStore持久化到plugins.jsonREGISTRY_FILE plugins.json每条记录以plugins.id为键字段包括type PluginRegistryEntry { id: string; version: string; path: string; // 插件实际所在目录 installationMethod: dev | store; originalPath?: string; enabled: boolean; installedAt: string; lastUpdatedAt: string; warnings?: string[]; };启动时hydratePluginsFromRegistry()会按installedAt升序遍历即按安装日期顺序加载与 packages/docs/plugins/plugin-system.md 中loaded in the order of installation dates一致。另外注意 plugin-system.md 描述的托管安装流程应用读取插件清单后会把内容复制到 appdata 下的plugins/pluginName/pluginVersion目录并从那里加载pluginBootstrap中的isManagedPath()校验表明启动时只会加载位于受管插件目录getPluginsDir()下的条目注册表里指向其他路径如 dev 插件的条目目前会被跳过源码中留有TODO: Support non-managed paths (dev plugins)。加载失败时错误信息会合并写入注册表的warnings字段而不是让整个启动崩溃。验证 SDK设置项如何落地与持久化官方文档的验证方法打开 Settings找到 Examples 分组应能看到 Hello world 开关拨动开关。值会被持久化到磁盘并更新所有订阅者。这里的机制在 settingsHost.ts 中可以看得很清楚也是官方文档那条 warning 提示的实现来源const normalizeId (source: SettingSource, id: string): string { if (source.type plugin) { return plugin.${source.pluginId}.${id}; } return core.${id}; };即设置 ID 会被自动加命名空间。插件里用裸 IDhello实际存储键为plugin.hello-plugin.hellopluginId取自package.json的name。这保证了不同插件的同名设置互不冲突。get/set/subscribe三个操作内部都会先经过normalizeId所以插件代码中始终只写裸 ID无需也不能手动拼前缀。subscribe的实现基于 zustand store 的订阅每次 store 变化时比较新旧值值真正发生变化时才调用监听器并返回一个unsubscribe函数供onDisable/onUnload时清理。而Settings类的公开接口register/get/set/getGlobal/setGlobal/subscribe/registerWidget定义在 plugin-sdk 的 api/settings.ts其中getGlobal/setGlobal面向应用核心设置core.前缀域插件一般用不到。package.json 键位速查loader 实际读取的字段汇总官方文档 Plugin shape 小节与源码loader 实际使用的package.json键如下键必填说明name是插件唯一 ID同时是设置命名空间前缀与注册表键version是语义化版本用于安装目录plugins/name/versiondescription是一句话描述展示在插件列表author是作者名main否入口文件路径缺失时按顺序尝试index.js→index.ts→index.tsx→dist/index.js→dist/index.ts→dist/index.tsx见 PluginLoader.resolveEntryPath()nuclear.displayName否UI 名称缺省回落为namenuclear.categories否展示在 Plugins 列表的分类字符串数组nuclear.icon否图标当前仅支持{ type: link, link: ... }见 types.ts 的PluginIconnuclear.permissions否能力声明数组当前为信息性字段未知权限只会产生警告入口文件解析的完整逻辑在resolveEntryPath()中有main就直接用没有则按上表候选列表依次尝试readTextFile全部失败时抛错并明确列出所有尝试过的文件名。测试用例 PluginLoader.test.ts 覆盖了清单校验失败、入口解析失败等分支可作为行为依据。插件代码能 require 什么模块白名单官方文档没有明说但源码里非常关键的一点插件不是在全局 require 环境里跑的。PluginLoader.evaluatePlugin() 用new Function(exports, module, require, code)(...)执行编译产物并注入一个白名单 requireconst ALLOWED_MODULES: Recordstring, unknown { nuclearplayer/plugin-sdk: { NuclearPluginAPI }, nuclearplayer/ui: nuclearUI, react: React, react/jsx-runtime: jsxRuntime, };含义有三插件只被允许引用 SDK、UI 组件库和 React 运行时这四个模块其余一律抛Module id not found这解释了为什么入口文件里可以直接module.exports {...}CJS 形态被保留也解释了 plugin-sdk README 里bundle needs to work in a CommonJS environment (module.exportsorexports.default)的要求求值后会取module.exports.default || module.exports因此export default经 esbuild 转成exports.default和module.exports 两种写法都合法——官方示例用module.exportsSDK 文档示例用export default两者等价。开发循环与注意事项结合官方文档与 plugin-sdk README 的 Development 一节日常循环是修改插件文件夹 → 在 Nuclear 中重新加载插件改动后需要 reload 才能生效→ 观察行为。编译缓存的失效是文件哈希级的重新加载即触发对参与文件的重检。几个容易踩的坑均有源码依据name/version/description/author任一缺失插件加载抛Invalid package.json错误注册表会记录 warning忘写main不会报错但会产生package.json missing main; will attempt fallback resolution...警告且只有入口恰好命中候选列表index.*/dist/index.*才能加载成功nuclear下写了拼错的键如display-name得到nuclear contains unknown keys: display-name警告键本身被忽略设置 ID 手动加前缀如get(plugin.hello-plugin.hello)会二次命名空间化导致读取不到值始终使用裸 ID。延伸SDK 的完整能力面入门示例只碰了api.Settings但 createPluginAPI.ts 组装的 API 对象覆盖面远超入门场景。按 plugin-sdk README 的域 API 表API能力api.Settings定义、读取、持久化插件设置本篇验证过api.Queue读取和操作播放队列api.Playback控制播放、音量、随机与循环api.Events订阅播放器生命周期事件如曲目结束api.Favorites管理用户收藏曲目api.Playlists创建、更新、删除播放列表api.Providers注册/注销音频源 providerapi.Streaming解析曲目音频流地址api.Metadata检索艺术家/专辑/曲目元数据api.Dashboard提供 Dashboard 内容热门曲目、新发行等api.Discovery从 provider 获取推荐曲目api.Shell在系统浏览器中打开 URLapi.Http从插件发起 HTTP 请求并绕过 CORSapi.Ytdlpyt-dlp 集成每个域 API 在仓库内都有对应的宿主实现packages/player/src/services/下的playbackHost.ts、queueHost.ts、providersHost.ts等与文档如 playback.md、queue.md。当入门插件验证通过Settings 中出现 Examples 分组、开关值可持久化并可被订阅即说明从文件夹约定、清单校验、TS 编译、模块沙箱到 API 注入的整条链路已端到端跑通可以在此基础上按域文档逐步扩展功能。相关深入文档插件体系总览、插件市场与发布、publishing.md。【免费下载链接】nuclearStreaming music player that finds free music for you项目地址: https://gitcode.com/GitHub_Trending/nu/nuclear创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考