# MoviePilot前端远程模块开发指南 ## 1. 概述 MoviePilot前端采用模块联邦(Module Federation)技术实现插件的动态加载和集成。本文档详细说明如何开发符合要求的远程模块,以便在MoviePilot中作为插件使用。 关联阅读后端插件开发文档:[第三方插件开发说明](https://github.com/jxxghp/MoviePilot-Plugins/blob/main/README.md) ## 2. 技术要求 - Node.js 20+ - Vue 3 - Vite 4+ - TypeScript 5+ ## 3. 核心概念 每个 Vue 联邦插件需要提供下列标准组件(`AppPage` 为可选,用于主界面侧栏全页入口): | 组件名称 | 暴露名 | 文件名 | 用途 | | --------- | ---------------- | ---------------------- | --------------------------------------------- | | Page | `./Page` | Page.vue | 插件管理中的详情弹窗 | | Config | `./Config` | Config.vue | 插件配置页面 | | Dashboard | `./Dashboard` | Dashboard.vue | 仪表盘小组件 | | AppPage | `./AppPage` | AppPage.vue | 主界面侧栏独立全页(主内容区由插件完全绘制) | | (可选) | `./AppPage{Xxx}` | 如 AppPageSettings.vue | 多 `nav_key` 时按名优先加载,见下文「多界面」 | 主应用在侧栏全页路由中按 `nav_key` 解析暴露名(如 `AppPageSettings`),再回退 `AppPage` → `Page`;`nav_key` 为 `main` 时仅尝试 `AppPage` → `Page`。 ## 4. 快速开始 ### 创建项目 ```bash # 创建项目 npm create vite@latest my-plugin -- --template vue-ts # 进入项目目录 cd my-plugin # 安装依赖 yarn ``` ### 配置vite.config.ts ```typescript import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import federation from '@originjs/vite-plugin-federation' export default defineConfig({ plugins: [ vue(), federation({ name: 'MyPlugin', filename: 'remoteEntry.js', exposes: { './Page': './src/components/Page.vue', './Config': './src/components/Config.vue', './Dashboard': './src/components/Dashboard.vue', './AppPage': './src/components/AppPage.vue', './AppPageSettings': './src/components/AppPageSettings.vue', }, shared: { vue: { requiredVersion: false, generate: false, }, vuetify: { requiredVersion: false, generate: false, singleton: true, }, 'vuetify/styles': { requiredVersion: false, generate: false, singleton: true, }, }, format: 'esm', }), ], build: { target: 'esnext', // 必须设置为esnext以支持顶层await minify: false, // 开发阶段建议关闭混淆 cssCodeSplit: true, // 改为true以便能分离样式文件 }, css: { preprocessorOptions: { scss: { additionalData: '/* 覆盖vuetify样式 */', }, }, postcss: { plugins: [ { postcssPlugin: 'internal:charset-removal', AtRule: { charset: atRule => { if (atRule.name === 'charset') { atRule.remove() } }, }, }, { postcssPlugin: 'vuetify-filter', Root(root) { // 过滤掉所有vuetify相关的CSS root.walkRules(rule => { if (rule.selector && (rule.selector.includes('.v-') || rule.selector.includes('.mdi-'))) { rule.remove() } }) }, }, ], }, }, server: { port: 5001, // 使用不同于主应用的端口 cors: true, // 启用CORS origin: 'http://localhost:5001', }, }) ``` ## 5. 组件开发规范 ### 5.1 Page组件(详情页面) ```vue ``` ### 5.2 Config组件(配置页面) ```vue ``` ### 5.3 Dashboard组件(仪表板) ```vue ``` ### 5.4 AppPage 组件(侧栏全页) 用于主应用左侧导航中的独立页面(路由 `#/plugin-app/:pluginId/:navKey?`),占据默认布局下的主内容区;与 `Page` 不同,不嵌在插件管理弹窗中。 主应用传入的 props: | 属性 | 说明 | | ----------------- | ----------------------------------------------------- | | `api` | 与 `Page` 相同,用于 `bear` 认证的插件 HTTP 调用 | | `nativeSubscribe` | 打开主应用原生订阅交互 | | `navKey` | 与侧栏声明的 `nav_key` 一致,同一插件多入口时用于区分 | | `pluginId` | 当前插件 ID | ```vue ``` ### 5.5 主应用宿主能力 登录后的联邦组件宿主会向插件开放以下能力: | 能力 | Page | Config | Dashboard | AppPage | 调用方式 | | ---------------- | ---- | ------ | --------- | ------- | ---------------------------------------------------------------- | | 认证 API | ✓ | ✓ | ✓ | ✓ | `api` prop | | 原生订阅交互 | ✓ | ✓ | ✓ | ✓ | `nativeSubscribe` prop 或 `inject('moviepilot:nativeSubscribe')` | | 主应用统一 Toast | ✓ | ✓ | ✓ | ✓ | `inject('moviepilot:toast')` | `nativeSubscribe` 和 Toast 都由主应用宿主提供。插件不应复制主程序订阅弹窗,也不应自行创建另一套 Toast 容器。插件在旧版主程序或能力不存在的环境中运行时,应保留空值判断和必要的页面内 fallback。 ### 5.6 玻璃光学表面 主应用的 `Page`、`Config` 与 `AppPage` 宿主在玻璃主题下默认采用 `static-material` 光学模式:保留壁纸透射、材质色调和方向反射,但不响应指针流场、局部折射、拖尾或动态焦散。插件列表与 `Dashboard` 继续使用完整动态光学。视觉型插件可以在自己控制的 DOM 区域显式恢复完整动态光学: ```html
``` 使用时需同时声明 `data-glass-optical-surface` 和 `data-glass-optical-mode="dynamic"`。模式会从最近的祖先容器继承,因此显式声明的动态子表面不会沿用宿主的静态模式。该合同适用于插件在 `Page`、`Config` 或 `AppPage` 中自行渲染并控制的区域;主应用生成的插件列表、插件市场卡片、`Dashboard` 及其他宿主 DOM 不属于插件的修改边界。 动态模式只在主应用启用玻璃主题和实时光学能力时生效。其他主题、降低动态效果或光学能力不可用时,插件必须保持内容与交互正常,不应依赖动态光学表达业务状态或必要反馈。 ### 5.7 调用主应用原生订阅 `Page`、`Config`、`Dashboard` 与 `AppPage` 都会收到 `nativeSubscribe(mediaInfo)` prop。插件传入媒体信息后,电视剧会打开主应用的选季抽屉,电影会进入现有电影订阅流程。宿主也会用 `moviepilot:nativeSubscribe` 键提供同一个方法,深层子组件可以使用 `inject`,无需逐层传递 prop。 媒体信息必须包含: - `type`:`电影` / `电视剧`,也兼容 `movie` / `tv`; - `title`; - 至少一个有效媒体标识:`tmdb_id` / `tmdbid`、`douban_id` / `doubanid`、`bangumi_id` / `bangumiid`、`anilist_id` / `anilistid`,或者 `media_id` 与 `mediaid_prefix` / `source` / `media_source` 的组合。 ```vue ``` `success: true` 表示主应用已接受调用并启动原生交互,不表示用户已经完成订阅。字段无效或当前用户没有订阅权限时返回 `success: false`,插件可以依据 `code` 执行 fallback。 ### 5.8 调用主应用 Toast `Page`、`Config`、`Dashboard` 与 `AppPage` 的宿主容器会通过固定键提供主应用 Toast。远程组件应复用该实例,不要自行渲染 `VSnackbar` 或创建另一套 Toast 容器: ```vue ``` 可用方法与主项目 `vue-toastification` 一致,包括 `success`、`info`、`warning` 和 `error`。注入不存在时应静默降级,关键错误仍需保留页面内状态提示。 #### 后端:注册侧栏入口 插件需为 **Vue** 渲染模式(`get_render_mode` 返回 `vue`),并实现 `get_sidebar_nav`,返回列表项字段与主应用 `GET /api/v1/plugin/sidebar_nav` 一致: | 字段 | 说明 | | ------------ | ------------------------------------------------------------------------------------- | | `nav_key` | URL 路径段,唯一标识本入口(同一插件可多入口) | | `title` | 侧栏显示标题 | | `icon` | MDI 图标名,如 `mdi-rss` | | `section` | 分组:`start` / `discovery` / `subscribe` / `organize` / `system` | | `permission` | 可选:`subscribe` / `discovery` / `search` / `manage` / `admin`,与主应用菜单权限一致 | | `order` | 可选:同组内排序,数值越小越靠前 | ```python def get_sidebar_nav(self) -> List[Dict[str, Any]]: return [ { "nav_key": "main", "title": "示例订阅页", "icon": "mdi-rss", "section": "subscribe", "permission": "subscribe", "order": 10, } ] ``` #### 同一插件多个全页界面(多 `nav_key`) 在 `get_sidebar_nav` 中**返回多条**记录,每条使用不同的 `nav_key` / `title` / `section` 等,侧栏与「更多」中会出现多个入口,路由形如 `#/plugin-app/<插件ID>/`。 前端加载远程组件的顺序为: | `nav_key` | 依次尝试的联邦暴露名 | | -------------------------------- | ------------------------------------------------ | | `main` 或省略 | `./AppPage` → `./Page` | | 其它(如 `settings`、`my_tool`) | `./AppPage{PascalCase}` → `./AppPage` → `./Page` | `PascalCase` 规则:按 `-`、`_`、空格分段后首字母大写并拼接。例如 `nav_key=settings` → 先试 `./AppPageSettings`;`my_tool` → `./AppPageMyTool`。 **两种实现方式(二选一或混用):** 1. **单文件分支**:只暴露 `./AppPage`,在组件内根据 `navKey` prop 用 `v-if` / `` 切换子界面。 2. **多文件**:为某个入口单独暴露 `./AppPageSettings.vue` 等,主应用会优先加载对应模块,失败再回退到 `AppPage`。 `vite.config` 多暴露示例: ```typescript exposes: { './AppPage': './src/components/AppPage.vue', './AppPageSettings': './src/components/AppPageSettings.vue', // ... } ``` ## 6. 构建和部署 ### 构建项目 ```bash yarn build ``` - 将生成的dist文件夹上传到插件后端目录下(默认为`dist/assets`) **注意: `__federation_shared_vuetify` 目录以及 `index-`、`date-`、`runtime-` 开头的文件不需要上传**,只需要上传以下命名格式文件:`__federation_*`、`_plugin-vue_export-helper-*`、`remoteEntry.js` - 在插件的后端python代码中,实现以下方法来集成远程组件: ```python def get_render_mode() -> Tuple[str, str]: """ 获取插件渲染模式 :return: 1、渲染模式,支持:vue/vuetify,默认vuetify :return: 2、组件路径,默认 dist/assets """ return "vue", "dist/assets" ``` - 需要在插件前端页面调用后端接口时,通过传入的api模块发起调用,后端api接口声明认证类型为:`bear` ```typescript // 演示使用api模块调用插件接口 recentItems.value = await props.api.get(`plugin/MyPlugin/history`) ``` ```python def get_api(self) -> List[Dict[str, Any]]: """ 注册插件API """ return [ { "path": "/history", "endpoint": self.get_history, "methods": ["GET"], "auth": "bear", # 认证类型设为bear "summary": "查询历史记录" } ] ``` ## 7. 调试与排错 ### 常见问题 1. **模块无法加载** - 检查网络请求是否成功(状态码200) - 确认文件路径是否正确 - 检查CORS跨域设置 2. **模块加载但组件不显示** - 检查控制台错误信息 - 确认组件是否正确导出 - 验证共享依赖配置 3. **"Module name 'vue' does not resolve to a valid URL"** - 检查`shared`配置是否正确 - 设置`requiredVersion: false`尝试解决 4. **"Top-level await is not available"** - 确保`build.target`设置为`esnext` ## 8. 高级配置 ### 8.1 CSS隔离 为防止样式冲突,建议使用CSS Modules或scoped样式: ```vue ``` ### 8.2 共享更多依赖 如果您的插件需要共享更多依赖,可以扩展shared配置: ```js shared: { vue: { requiredVersion: false }, vuetify: { requiredVersion: false }, '@vueuse/core': { requiredVersion: false }, pinia: { requiredVersion: false } } ``` ### 8.3 本地监听构建 插件前端可使用 Vite 的监听构建模式: ```bash yarn dev ``` 将 `dev` 脚本配置为 `vite build --watch` 后,源码变化会自动重新构建。使用本地插件仓并启用 `DEV` 或 `PLUGIN_AUTO_RELOAD` 时,MoviePilot 会同步新的构建产物;刷新页面即可看到修改。 ## 9. 示例代码 - [插件远程组件示例](../examples/plugin-component/) - 开发插件组件的完整示例项目 - [模块联邦问题排查指南](./federation-troubleshooting.md) - 常见问题排查 ## 10. 参考资料 - [Vite Plugin Federation](https://github.com/originjs/vite-plugin-federation) - [Vue 3官方文档](https://vuejs.org/) --- 如有问题,请提交Issue。