Miru 扩展系统是怎么工作的
一句话理解
Miru 的扩展系统让任何会写 JavaScript 的人都能为它添加新的媒体源。你不需要改 Miru 主程序的一行代码——只需写一个符合规范的 JS 文件,Miru 就能加载并运行它。
整体架构
┌───────────────┐ ┌──────────────────────┐ ┌────────────────┐
│ Miru 主程序 │────▶│ QuickJS 引擎 │────▶│ 扩展 JS 文件 │
│ (Flutter UI) │ │ (嵌入式 JS runtime) │ │ (隔离沙箱) │
└───────────────┘ └──────────────────────┘ └────────────────┘
│ │ │
│ │ ▼
│ │ ┌───────────────┐
│ │ │ this.request │
│ │ │ (HTTP 代理) │
│ │ └───────────────┘
▼ ▼ │
UI 渲染 扩展调度 联网请求
为什么选 QuickJS
QuickJS 是一个轻量级嵌入式 JavaScript 引擎,特点是:
- 小而快:整个引擎只有几百 KB,启动几乎零延迟;
- 隔离沙箱:每个扩展在独立上下文里运行,互不影响;
- 跨平台:Android、Windows、Web 三端都能跑;
- 支持现代 JS:async/await、Promise 等现代语法都能用。
Linux 平台暂时不支持——README 里明确说明 Linux 端因为 QuickJS 依赖问题无法启动引擎,以官方最新版为准。
Extension 基类与生命周期
每个 Miru 扩展必须导出一个继承自 Extension 基类的类:
export default class extends Extension {
async load() { /* 扩展加载时调用一次 */ }
async latest(page) { /* 返回"最近更新"列表 */ }
async search(kw, page) { /* 搜索 */ }
async detail(url) { /* 获取详情页 */ }
async watch(url) { /* 获取播放/阅读内容 */ }
async checkUpdate(url) { /* 检查是否有新集/新章节 */ }
async unload() { /* 扩展卸载时调用 */ }
}
这些方法构成了扩展的完整生命周期。其中 latest 和 search 返回 ListItem 列表,detail 返回 Detail 对象,watch 根据扩展类型返回不同的消费数据(视频 / 漫画图片数组 / 小说文本数组)。
三种扩展类型
扩展文件头部的元数据里必须声明 @type,决定了 Miru 用哪个消费者来渲染它的 watch 返回值:
| @type | 消费端 | watch 返回值类型 | |
|---|---|---|---|
| bangumi | 视频播放器 | `{ type: "hls" \ | "mp4", url }` |
| manga | 漫画阅读器 | { urls: string[] }(图片 URL 数组) | |
| fikushon | 小说阅读器 | { content: string[], title, subtitle } |
一个扩展文件同时只能是其中一种类型。想同时提供视频和漫画源?写两个扩展文件。
this.request 代理
扩展里的网络请求必须走 this.request,而不是原生 fetch 或 XMLHttpRequest。原因:
- Web 端跨域问题:浏览器的同源策略阻止直接请求第三方站点;
- 桌面端:
this.request也会通过代理,保证行为一致; - 日志可追溯:所有请求会记录到扩展调试日志里。
示例:
const res = await this.request({
url: "/api/search?kw=" + encodeURIComponent(kw),
method: "GET",
});
this.request 是 Miru 在加载扩展时注入到全局的代理函数,扩展作者不需要自己实现。