在做插件市场之前,先有的是 MoeKoe 的普通插件系统。
当时我想解决的问题不是某一个具体功能,而是一个更通用的问题:播放器里总会冒出很多小需求,有些人想改界面,有些人想加一个工具弹窗,有些人想接自己的服务。如果每个想法都往主程序里塞,主程序会越来越重,也越来越难维护。
所以我一开始给插件系统定的方向很明确:主程序只提供插件运行环境和管理入口,具体功能交给插件自己做。插件能独立安装、独立卸载,坏了也尽量不要影响播放器本体。
上一篇我写的是 MoeKoeMusic-Plugins 这个插件登记仓库。那边解决的是“插件市场的数据怎么产生”:用户用 Issue 提交,Action 校验,维护者审核,通过后写进 plugins.json。
但光有登记仓库还不够。真正对用户来说,插件市场不是 GitHub 上的一份 JSON,而是客户端里一个能看、能搜、能安装、能更新的入口。
所以这篇就换到 MoeKoe Music 主项目里,专门聊客户端的插件市场部分。
为什么选 Chrome Extension 这一套
MoeKoe 是 Electron 应用,Electron 本身就有加载扩展的能力。既然底层已经能跑 Chrome Extension,那就没必要再从零设计一套插件格式。
这样做有几个好处:
- 第一,插件作者不用学一套完全陌生的格式。
manifest.json、popup.html、background.js都是浏览器扩展里很常见的概念。 - 第二,插件天然有清单文件。主程序可以先读
manifest.json,判断插件名称、版本、描述、权限、弹窗入口、最低支持版本这些信息。 - 第三,Electron 可以直接加载插件目录,不需要我自己写一个脚本沙箱。
展示、搜索、分页和状态
插件市场不是只要能安装就行,还要能快速判断:
- 这个插件是干什么的
- 我有没有装过
- 我装的是不是旧版本
- 这个插件有没有更高版本要求
- 它有没有联网、文件访问、本地二进制这些风险点
- 分页,搜索,更新,状态
所以市场卡片里展示的不只是“安装”按钮。
- 确保插件目录存在
- 调用Electron下载安装、卸旧版、装新版
- 注册插件相关 IPC
- 加载 Chrome 扩展
- 加载 Native Host 索引和授权系统
已安装插件
插件管理页里的“已安装插件”不是只读 Electron 当前加载的扩展。它会把两类信息合并:
session.defaultSession.getAllExtensions()拿到已加载扩展scanExtensions()扫描磁盘插件目录,读取 manifest、图标、popup、Native Host 声明
前端拿到这些字段后,就能显示:
- 插件名称、描述、版本、作者
- 图标
- 是否是 MoeKoe 适配插件
- 当前客户端版本是否低于插件
minversion - 是否有 popup 设置页
- 是否声明了本地二进制程序
Native Host
这是2.0版本新增的插件功能.
插件系统里最需要小心的是 Native Host,也就是插件附带本地可执行程序的能力。
普通前端插件再怎么折腾,大多还是在浏览器扩展模型里。但本地程序不一样。它一旦启动,就具备普通桌面程序的系统访问能力。
所以 MoeKoe Music 没有让插件声明了就直接运行,而是设计了一层授权层。
manifest 里必须声明权限:'moekoe:nativeHost'
本地程序通信
Native Host 真正启动时,用的是 spawn:
const child = spawn(host.executablePath, host.args, {
cwd: path.dirname(host.executablePath),
stdio: ['pipe', 'pipe', 'pipe']
});
这里没有用 shell,这个选择很重要。它避免插件通过拼接参数获得额外命令执行能力。
插件向本地程序发消息时,会写入一行 JSON:
child.stdin.write(`${JSON.stringify(payload)}\n`, error => {
// ...
});
也就是 JSON Lines 协议:一行一个 JSON。
本地程序的 stdout 也按行解析,消息大小限制是 64 KB
这套协议不复杂,但足够明确。插件和本地程序之间不需要共享一堆状态,只要约定好输入输出的 JSON 就行。
bridge 窗口
Native Host 里还有一个挺有意思的设计:bridge, 给插件后台一个通往 preload 的通道
代码注释写得很清楚:
// bridge 是一个隐藏扩展页,既能访问 chrome.runtime,又能通过 preload 访问 electronAPI。
// background/service worker 不能直接拿到 Electron preload,因此需要这个中转页。
实现上是创建一个 1×1 的隐藏 BrowserWindow
为什么要这么做?
因为 Chrome Extension 的 background/service worker 不一定能直接拿到 Electron preload 暴露的 API。而 bridge 是一个扩展页面,它既属于 chrome-extension://,又运行在 Electron 窗口里,所以可以作为中转层。
最后一问
看到这里可能会有一个问题:既然 MoeKoe Music 已经用了 Chrome Extension 模型,为什么 Native Host 通信不直接照搬 Chrome 浏览器自带的 Native Messaging?
Chrome 那套机制本身是成熟的。浏览器扩展可以通过 chrome.runtime.connectNative 或 chrome.runtime.sendNativeMessage 连接本地程序,本地程序和浏览器之间用 stdin/stdout 通信,消息格式是“4 字节长度头 + JSON 内容”。
但 MoeKoe Music 这里要解决的问题不完全一样。
Chrome Native Messaging 面向的是“浏览器扩展连接系统里已经注册好的 native host”。它要求本地程序先在操作系统特定位置登记 host manifest,比如 macOS/Linux 的固定目录,或者 Windows 的注册表。这个 manifest 里还要写 allowed_origins,把允许访问它的 Chrome 扩展 id 列出来。
第一,MoeKoe 的插件是从客户端市场安装到应用自己的插件目录里的。用户点击安装插件,本质上应该只是往 MoeKoe Music 的用户数据目录写入扩展文件。如果为了一个插件里的本地程序,还要去改系统级 native host 注册信息,那安装、更新、卸载都会变复杂。卸载时,客户端还必须保证系统注册信息也被清掉,否则就会留下应用之外的残留状态。
第二,Chrome 那套授权是围绕浏览器扩展 id 和 host manifest 做的,而 MoeKoe Music 更需要围绕 plugin_id 做管理。前面已经提到过,Electron 生成的 extension id 更像运行时身份,插件市场、审核记录、更新判断和 Native Host 授权都需要一个更稳定的身份。如果直接走 Chrome Native Messaging,权限链路会被拆到系统 host manifest 和浏览器扩展 id 上,和 MoeKoe 自己的插件市场信任链不太好对齐。
第三,MoeKoe Music 需要主进程掌握完整生命周期。谁能启动本地程序、启动前是否已经授权、可执行文件是不是插件目录内的相对路径、退出应用时要不要关闭子进程、插件更新或卸载时如何清理,这些判断都应该集中在 Electron 主进程里。自己 spawn 本地程序,就可以把这些规则全部收在客户端内部,而不是把一部分状态交给操作系统注册机制。
第四,Chrome Native Messaging 的长度头协议很通用,但对这个场景并不是必须的。MoeKoe 的 Native Host 更像插件和本地小工具之间的命令通道,消息量不大,也不需要传二进制流。用 JSON Lines,一行一个 JSON,本地程序作者用 Node、Python、Go、Rust 都很好实现,调试时直接看 stdout 也更直观。
第五,Electron 不支持 chrome.runtime.connectNative 或 chrome.runtime.sendNativeMessage, 这两个 API 是 Chrome 浏览器扩展(Extension)的 Native Messaging API,依赖浏览器安装的 Native Messaging Host,而不是 Chromium 内核本身,所以 Electron 并没有实现这一套机制. 说了那么多,这才是最主要的原因
结尾
如果只看界面,MoeKoe Music 的插件市场就是一个列表:图标、名字、描述、版本、按钮。
但往里看,它其实是一条信任链:
插件作者提交到登记仓库,维护者审核快照,Action 写入 plugins.json,客户端读取市场索引,用户看到权限提示,点击安装,主进程下载并校验 zip,最后 Electron 加载扩展。
- 插件来源从哪里来
- 审核通过的是哪一版
- 客户端如何知道可更新
- 安装包如何落到本地
- 本地二进制能力如何授权
- 插件卸载和应用退出时如何清理
最后用户看到的只是一个“安装”按钮。
但这个按钮背后,其实已经走过了一整套很具体、很工程化的判断。






最新评论