diff --git a/README.md b/README.md index 517f9a03..3cdf217f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,22 @@ [MoviePilot](https://github.com/jxxghp/MoviePilot) 的前端项目,NodeJS版本:>= `v20.12.1`。 +## 特性 + +- 基于 Vue 3 和 Vuetify 3 构建的现代化界面 +- 使用 Vite 作为构建工具,提供快速的开发体验 +- 支持多语言(中文/英文) +- 完整的插件系统支持,包括远程组件动态加载 + +## 模块联邦功能 + +MoviePilot 现已支持模块联邦(Module Federation)功能,允许插件开发者创建可动态加载的远程组件,实现更丰富的插件用户界面。 + +### 相关文档 + +- [模块联邦问题排查指南](docs/federation-troubleshooting.md) - 常见问题和解决方案 +- [插件远程组件示例](examples/plugin-component/) - 开发插件组件的完整示例项目 + ## 推荐的IDE设置 [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (并禁用 Vetur). diff --git a/README_EN.md b/README_EN.md index 3b82d234..265b4ede 100644 --- a/README_EN.md +++ b/README_EN.md @@ -2,39 +2,55 @@ *[中文](README.md) | English* -Frontend project for [MoviePilot](https://github.com/jxxghp/MoviePilot), NodeJS version: >= `v20.12.1`. +Frontend project for [MoviePilot](https://github.com/jxxghp/MoviePilot), NodeJS version required: >= `v20.12.1`. + +## Features + +- Modern interface built with Vue 3 and Vuetify 3 +- Fast development experience with Vite build tool +- Multi-language support (Chinese/English) +- Complete plugin system with dynamic remote component loading + +## Module Federation + +MoviePilot now supports Module Federation, allowing plugin developers to create dynamically loadable remote components for richer plugin user interfaces. + +### Documentation + +- [Module Federation Troubleshooting Guide](docs/federation-troubleshooting.md) - Common issues and solutions +- [Plugin Remote Component Example](examples/plugin-component/) - Complete example project for developing plugin components ## Recommended IDE Setup [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (disable Vetur). -## Vite Configuration +## Configure Vite -Please refer to [Vite Configuration Reference](https://vitejs.dev/config/). +See [Vite Configuration Reference](https://vitejs.dev/config/). -## Installation +## Install Dependencies ```sh yarn ``` -### Development +### Development Server ```sh yarn dev ``` -### Build +### Build for Production ```sh yarn build ``` -### Production Deployment +### Static Deployment -1. Use `nginx` or other web servers to host the `dist` static files. See `public/nginx.conf` for nginx configuration reference. +1. Host the `dist` static files using a web server like `nginx`. Refer to `public/nginx.conf` for nginx configuration. -2. Use `node` command to run `service.js` directly. It listens on port `3000` by default. Set the environment variable `NGINX_PORT` to adjust the port. +2. Alternatively, run the `service.js` directly with the `node` command. It listens on port `3000` by default. Set the `NGINX_PORT` environment variable to adjust the port. ```shell node dist/service.js diff --git a/components.d.ts b/components.d.ts index 703fab76..5d6ff118 100644 --- a/components.d.ts +++ b/components.d.ts @@ -12,13 +12,11 @@ declare module 'vue' { ErrorHeader: typeof import('./src/@core/components/ErrorHeader.vue')['default'] ExistIcon: typeof import('./src/@core/components/ExistIcon.vue')['default'] LoadingBanner: typeof import('./src/@core/components/LoadingBanner.vue')['default'] - LocaleSwitcher: typeof import('./src/@core/components/LocaleSwitcher.vue')['default'] MoreBtn: typeof import('./src/@core/components/MoreBtn.vue')['default'] PageContentTitle: typeof import('./src/@core/components/PageContentTitle.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] ScrollToTopBtn: typeof import('./src/@core/components/ScrollToTopBtn.vue')['default'] StatIcon: typeof import('./src/@core/components/StatIcon.vue')['default'] - ThemeSwitcher: typeof import('./src/@core/components/ThemeSwitcher.vue')['default'] } } diff --git a/docs/federation-troubleshooting.md b/docs/federation-troubleshooting.md new file mode 100644 index 00000000..43e2dd2c --- /dev/null +++ b/docs/federation-troubleshooting.md @@ -0,0 +1,183 @@ +# MoviePilot 模块联邦问题排查指南 + +本文档提供了针对 MoviePilot 项目中使用模块联邦时可能遇到的常见问题及解决方案。 + +关联阅读后端插件开发文档:[第三方插件开发说明](https://github.com/jxxghp/MoviePilot-Plugins/blob/main/README.md) + +## 常见错误 + +### 1. "Module name 'vue' does not resolve to a valid URL" + +**原因**:远程组件无法正确解析共享依赖的 URL,通常是因为共享依赖配置不正确。 + +**解决方案**: + +1. 在 **插件组件项目** 的 `vite.config.js` 中正确配置共享依赖: + +```js +federation({ + // ... + shared: { + vue: { + singleton: true, + requiredVersion: false // 关闭版本检查 + } + } +}) +``` + +2. 在 **主应用** 的 `vite.config.ts` 中确保共享依赖配置正确: + +```ts +federation({ + name: 'host', + remotes: {}, + shared: ['vue', 'vuetify'] +}) +``` + +### 2. "Top-level await is not available in the configured target environment" + +**原因**:模块联邦使用了顶层 await,但目标构建环境不支持此功能。 + +**解决方案**: + +在 **主应用** 和 **插件组件项目** 的构建配置中添加 `target: 'esnext'`: + +```js +build: { + target: 'esnext', // 支持顶层await + // 其他配置... +} +``` + +### 3. "TypeError: Failed to fetch dynamically imported module" + +**原因**:远程组件 JS 文件无法被正确加载,可能是路径错误或网络问题。 + +**解决方案**: + +1. 检查网络请求是否成功(状态码200) +2. 确认组件 URL 是否正确 +3. 确保服务器允许访问该 JS 文件(CORS 配置) +4. 检查插件后端是否正确提供了静态文件服务 + +### 4. 组件加载后渲染为空白或出现错误 + +**原因**:组件内部代码错误或与主应用不兼容。 + +**解决方案**: + +1. 检查浏览器控制台错误信息 +2. 确保组件代码没有语法错误 +3. 避免在组件中使用主应用未提供的依赖 +4. 确保所有路径(如图片、API请求URL等)都是正确的 + +## 调试技巧 + +### 1. 启用详细日志 + +在浏览器控制台中设置: + +```js +localStorage.setItem('debug', 'vite:*') +``` + +### 2. 分析网络请求 + +1. 打开浏览器开发者工具 +2. 转到 Network 标签页 +3. 确认远程组件 JS 文件请求是否成功 +4. 分析响应内容是否为有效的 JavaScript + +### 3. 隔离测试远程组件 + +创建一个独立的简单页面来测试插件组件,排除主应用的干扰因素。 + +## 最佳实践 + +### 插件组件项目配置 + +```js +// vite.config.js +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import federation from '@originjs/vite-plugin-federation' + +export default defineConfig({ + plugins: [ + vue(), + federation({ + name: 'remoteApp', + filename: 'remoteEntry.js', + exposes: { + './PluginComponent': './src/App.vue', + }, + shared: { + vue: { + singleton: true, + requiredVersion: false + } + } + }) + ], + build: { + target: 'esnext', + minify: false, // 开发阶段禁用最小化,方便调试 + cssCodeSplit: false, + rollupOptions: { + output: { + minifyInternalExports: false + } + } + } +}) +``` + +### 插件组件代码 + +```vue + + + +``` + +### 显式依赖声明 + +在插件组件的 `package.json` 中准确声明所有依赖: + +```json +{ + "dependencies": { + "vue": "^3.3.4" + }, + "devDependencies": { + "@originjs/vite-plugin-federation": "^1.3.5", + "@vitejs/plugin-vue": "^4.4.0", + "vite": "^5.0.0" + } +} +``` + +## 其他资源 + +- [Vite 模块联邦插件文档](https://github.com/originjs/vite-plugin-federation) +- [Vite 官方文档](https://vitejs.dev/guide/build.html) +- [Origin.js 模块联邦示例](https://github.com/originjs/vite-plugin-federation/tree/main/packages/examples) +- [MoviePilot 插件组件示例](../examples/plugin-component/) diff --git a/examples/plugin-component/README.md b/examples/plugin-component/README.md new file mode 100644 index 00000000..0675cfae --- /dev/null +++ b/examples/plugin-component/README.md @@ -0,0 +1,141 @@ +# MoviePilot 插件远程组件示例 + +这是 MoviePilot 插件远程组件的示例项目,展示了如何正确配置和开发与主应用兼容的远程组件。 + +## 开发环境准备 + +1. 安装依赖: +```bash +npm install +# 或 +yarn +``` + +2. 开发模式运行: +```bash +npm run dev +# 或 +yarn dev +``` + +## 配置说明 + +### vite.config.js + +```js +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import federation from '@originjs/vite-plugin-federation' + +export default defineConfig({ + plugins: [ + vue(), + federation({ + name: 'remoteApp', + filename: 'remoteEntry.js', + exposes: { + './PluginComponent': './src/App.vue', // 暴露组件 + }, + shared: { + vue: { + singleton: true, + requiredVersion: false + } + } + }) + ], + build: { + target: 'esnext', // 支持顶层await + minify: false, + cssCodeSplit: false, + rollupOptions: { + output: { + minifyInternalExports: false + } + } + } +}) +``` + +### 组件开发 + +主组件 (src/App.vue) 需要遵循以下规则: +- 使用 Vue 3 组合式 API +- 注册 `action` 事件用于与主应用通信 +- 避免直接使用 Vue Router 等全局依赖 + +示例组件结构: + +```vue + + + +``` + +## 构建生产版本 + +```bash +npm run build +# 或 +yarn build +``` + +构建后的 `dist/remoteEntry.js` 是远程组件的入口文件,需要配置到后端让 MoviePilot 能够访问。 + +## 排查常见问题 + +### 顶层 await 报错 + +确保在 `vite.config.js` 中设置 `build.target` 为 `'esnext'`。 + +### 共享依赖加载失败 + +确保正确配置共享依赖: +```js +shared: { + vue: { + singleton: true, + requiredVersion: false // 关闭版本检查 + } +} +``` + +### 组件无法加载 + +1. 检查网络请求是否成功 +2. 确认 JS 文件可以被正确访问 +3. 查看浏览器控制台是否有详细错误信息 diff --git a/examples/plugin-component/index.html b/examples/plugin-component/index.html new file mode 100644 index 00000000..c12faa16 --- /dev/null +++ b/examples/plugin-component/index.html @@ -0,0 +1,12 @@ + + + + + + MoviePilot 插件组件示例 + + +
+ + + diff --git a/examples/plugin-component/package.json b/examples/plugin-component/package.json new file mode 100644 index 00000000..608f9a49 --- /dev/null +++ b/examples/plugin-component/package.json @@ -0,0 +1,19 @@ +{ + "name": "moviepilot-plugin-component", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.3.4" + }, + "devDependencies": { + "@originjs/vite-plugin-federation": "^1.3.5", + "@vitejs/plugin-vue": "^4.4.0", + "vite": "^5.0.0" + } +} diff --git a/examples/plugin-component/src/App.vue b/examples/plugin-component/src/App.vue new file mode 100644 index 00000000..e6e12ad4 --- /dev/null +++ b/examples/plugin-component/src/App.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/examples/plugin-component/src/main.js b/examples/plugin-component/src/main.js new file mode 100644 index 00000000..438f6fe8 --- /dev/null +++ b/examples/plugin-component/src/main.js @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import App from './App.vue' + +const app = createApp(App) +app.mount('#app') diff --git a/examples/plugin-component/vite.config.js b/examples/plugin-component/vite.config.js new file mode 100644 index 00000000..44309206 --- /dev/null +++ b/examples/plugin-component/vite.config.js @@ -0,0 +1,32 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import federation from '@originjs/vite-plugin-federation' + +export default defineConfig({ + plugins: [ + vue(), + federation({ + name: 'remoteApp', + filename: 'remoteEntry.js', + exposes: { + './PluginComponent': './src/App.vue', // 暴露组件 + }, + shared: { + vue: { + singleton: true, + requiredVersion: false + } + } + }) + ], + build: { + target: 'esnext', // 支持顶层await + minify: false, + cssCodeSplit: false, + rollupOptions: { + output: { + minifyInternalExports: false + } + } + } +}) diff --git a/vite.config.ts b/vite.config.ts index c4f2348c..d184156c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -34,9 +34,8 @@ export default defineConfig({ }), federation({ name: 'host', - remotes: { - // 这里我们会动态添加远程组件,所以不预设remotes - }, + remotes: {}, + exposes: {}, shared: ['vue', 'vuetify'], }), VitePWA({ @@ -162,6 +161,7 @@ export default defineConfig({ }, }, build: { + target: 'esnext', minify: 'terser', terserOptions: { compress: {