增强远程组件注册机制,添加动态注册和获取功能,更新相关文档以指导插件开发者。调整多个组件以支持新注册逻辑。

This commit is contained in:
jxxghp
2025-05-05 22:13:07 +08:00
parent 36ef7ba589
commit 643ca35aed
6 changed files with 136 additions and 4 deletions

View File

@@ -4,6 +4,16 @@
关联阅读后端插件开发文档:[第三方插件开发说明](https://github.com/jxxghp/MoviePilot-Plugins/blob/main/README.md)
## 远程组件注册机制
MoviePilot 使用自动注册机制来加载远程组件:
1. 对于使用 Vue 渲染模式的插件,自动注册其远程组件
2. 每个远程组件根据插件 ID 唯一标识,确保不会冲突
3. 在需要加载组件时,会优先检查已注册的组件信息
这种设计使得插件开发者只需专注于组件开发,而不需要担心加载机制的复杂性。
## 常见错误
### 1. "Module name 'vue' does not resolve to a valid URL"

View File

@@ -116,6 +116,33 @@ yarn build
构建后的 `dist/remoteEntry.js` 是远程组件的入口文件,需要配置到后端让 MoviePilot 能够访问。
## 插件后端配置
在插件的后端代码中,需要实现以下方法来提供组件信息:
```python
def get_render_mode() -> str:
"""
获取插件渲染模式
:return: 渲染模式支持vue/vuetify默认vuetify
"""
return "vue"
def get_form_file() -> Tuple[str, Dict[str, Any]]:
"""
获取插件配置页面JS代码源文件与get_from二选一使用
:return: 1、编译后的JS代码插件目录下相对路径2、默认数据结构
"""
return "/dist/page.js", {}
def get_page_file() -> Optional[str]:
"""
获取插件数据页面JS代码源文件与get_page二选一使用
:return: 编译后的JS代码插件目录下相对路径
"""
return "/dist/config.js", {}
```
## 排查常见问题
### 顶层 await 报错

View File

@@ -7,7 +7,12 @@ import { useToast } from 'vue-toast-notification'
import FormRender from '../render/FormRender.vue'
import ProgressDialog from '../dialog/ProgressDialog.vue'
import { useI18n } from 'vue-i18n'
import { loadRemoteComponent, clearRemoteComponentCache } from '@/utils/remoteFederationLoader'
import {
loadRemoteComponent,
clearRemoteComponentCache,
registerRemoteComponent,
getRemoteComponent,
} from '@/utils/remoteFederationLoader'
// 国际化
const { t } = useI18n()
@@ -52,6 +57,14 @@ const vueComponentUrl = ref<string | null>(null)
// Vue 模式:动态加载的组件
const dynamicComponent = computed(() => {
if (renderMode.value === 'vue' && vueComponentUrl.value) {
// 检查是否已经注册,如果没有则进行注册
const remoteInfo = props.plugin?.id ? getRemoteComponent(props.plugin.id) : null
if (!remoteInfo && props.plugin?.id) {
// 动态注册远程组件
registerRemoteComponent(props.plugin.id, vueComponentUrl.value)
}
// 加载远程组件
return loadRemoteComponent(vueComponentUrl.value, {
onError: error => {
console.error(`加载插件组件失败: ${vueComponentUrl.value}`, error)

View File

@@ -4,7 +4,12 @@ import type { Plugin } from '@/api/types'
import PageRender from '@/components/render/PageRender.vue'
import api from '@/api'
import { useToast } from 'vue-toast-notification'
import { loadRemoteComponent, clearRemoteComponentCache } from '@/utils/remoteFederationLoader'
import {
loadRemoteComponent,
clearRemoteComponentCache,
registerRemoteComponent,
getRemoteComponent,
} from '@/utils/remoteFederationLoader'
// 输入参数
const props = defineProps({
@@ -39,6 +44,14 @@ let pluginPageItems = ref([])
// Vue 模式:动态加载的组件
const dynamicComponent = computed(() => {
if (renderMode.value === 'vue' && vueComponentUrl.value) {
// 检查是否已经注册,如果没有则进行注册
const remoteInfo = props.plugin?.id ? getRemoteComponent(props.plugin.id) : null
if (!remoteInfo && props.plugin?.id) {
// 动态注册远程组件
registerRemoteComponent(props.plugin.id, vueComponentUrl.value)
}
// 加载远程组件
return loadRemoteComponent(vueComponentUrl.value, {
onError: error => {
console.error(`加载插件组件失败: ${vueComponentUrl.value}`, error)

View File

@@ -12,8 +12,12 @@ import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue'
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
import DashboardRender from '@/components/render/DashboardRender.vue'
import { isNullOrEmptyObject } from '@/@core/utils'
import api from '@/api'
import { loadRemoteComponent, clearRemoteComponentCache } from '@/utils/remoteFederationLoader'
import {
loadRemoteComponent,
clearRemoteComponentCache,
registerRemoteComponent,
getRemoteComponent,
} from '@/utils/remoteFederationLoader'
// 输入参数
const props = defineProps({
@@ -37,6 +41,16 @@ const pluginRenderMode = computed(() => props.config?.render_mode || 'vuetify')
const dynamicPluginComponent = computed(() => {
// 确保 config 存在并且 component_url 也存在
if (pluginRenderMode.value === 'vue' && props.config?.component_url) {
// 如果有插件ID尝试注册远程组件
if (props.config.id) {
const remoteInfo = getRemoteComponent(props.config.id)
if (!remoteInfo) {
// 动态注册远程组件
registerRemoteComponent(props.config.id, props.config.component_url)
}
}
// 加载远程组件
return loadRemoteComponent(props.config.component_url, {
onError: error => {
console.error(`加载插件组件失败: ${props.config?.component_url}`, error)

View File

@@ -16,6 +16,51 @@ interface RemoteModuleState {
// 已加载组件的缓存
const loadedRemotes: Record<string, RemoteModuleState> = {}
// 动态注册的远程组件映射
interface RemotePluginInfo {
url: string
pluginId: string
moduleName: string
}
// 已注册的远程模块
const registeredRemotes: Record<string, RemotePluginInfo> = {}
/**
* 动态注册远程组件
* @param pluginId 插件ID
* @param remoteUrl 远程组件URL
* @returns 注册成功返回true否则返回false
*/
export async function registerRemoteComponent(pluginId: string, remoteUrl: string): Promise<boolean> {
try {
// 生成远程模块名称使用插件ID作为标识
const moduleName = `plugin_${pluginId.replace(/[^a-zA-Z0-9_]/g, '_')}`
// 注册到映射表中
registeredRemotes[pluginId] = {
url: remoteUrl,
pluginId,
moduleName,
}
console.log(`已注册远程组件: ${pluginId} -> ${moduleName} (${remoteUrl})`)
return true
} catch (error) {
console.error(`注册远程组件失败: ${pluginId}`, error)
return false
}
}
/**
* 获取远程组件信息
* @param pluginId 插件ID
* @returns 远程组件信息
*/
export function getRemoteComponent(pluginId: string): RemotePluginInfo | null {
return registeredRemotes[pluginId] || null
}
/**
* 获取和初始化远程组件
* @param remoteUrl 远程组件URL
@@ -127,3 +172,13 @@ export function clearRemoteComponentCache(remoteUrl?: string) {
})
}
}
// 取消注册远程组件
export function unregisterRemoteComponent(pluginId: string) {
if (registeredRemotes[pluginId]) {
delete registeredRemotes[pluginId]
console.log(`已取消注册远程组件: ${pluginId}`)
return true
}
return false
}