mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-05 13:27:28 +08:00
添加 @originjs/vite-plugin-federation 依赖,并在多个组件中实现远程组件加载功能
This commit is contained in:
@@ -7,6 +7,7 @@ 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'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -51,32 +52,12 @@ const vueComponentUrl = ref<string | null>(null)
|
||||
// Vue 模式:动态加载的组件
|
||||
const dynamicComponent = computed(() => {
|
||||
if (renderMode.value === 'vue' && vueComponentUrl.value) {
|
||||
const url = vueComponentUrl.value
|
||||
return defineAsyncComponent(() =>
|
||||
api
|
||||
.get(url)
|
||||
.then((response: any) => {
|
||||
if (response) {
|
||||
const blob = new Blob([response.data], { type: 'text/javascript' })
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
return import(/* @vite-ignore */ blobUrl)
|
||||
} else {
|
||||
return { render: () => h('div', '组件加载失败: 未读取到文件数据') }
|
||||
}
|
||||
})
|
||||
.then(module => {
|
||||
if (module.default) {
|
||||
return module.default
|
||||
} else {
|
||||
console.error(`无法从 ${url} 加载默认导出的 Vue 组件`)
|
||||
return { render: () => h('div', '组件加载失败: 无默认导出') }
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(`无法加载插件组件: ${url}`, err)
|
||||
return { render: () => h('div', `组件加载失败:${err}`) }
|
||||
}),
|
||||
)
|
||||
return loadRemoteComponent(vueComponentUrl.value, {
|
||||
onError: error => {
|
||||
console.error(`加载插件组件失败: ${vueComponentUrl.value}`, error)
|
||||
$toast.error(`加载插件组件失败: ${error.message || '未知错误'}`)
|
||||
},
|
||||
})
|
||||
}
|
||||
return null
|
||||
})
|
||||
@@ -88,6 +69,11 @@ async function loadPluginUIData() {
|
||||
pluginFormItems = []
|
||||
pluginConfigForm.value = {}
|
||||
renderMode.value = 'vuetify'
|
||||
|
||||
// 如果存在旧的组件URL,清除其缓存
|
||||
if (vueComponentUrl.value) {
|
||||
clearRemoteComponentCache(vueComponentUrl.value)
|
||||
}
|
||||
vueComponentUrl.value = null
|
||||
|
||||
try {
|
||||
@@ -151,6 +137,13 @@ async function savePluginConf() {
|
||||
onBeforeMount(async () => {
|
||||
await loadPluginUIData()
|
||||
})
|
||||
|
||||
// 组件卸载时清理资源
|
||||
onUnmounted(() => {
|
||||
if (vueComponentUrl.value) {
|
||||
clearRemoteComponentCache(vueComponentUrl.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<VDialog scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
|
||||
|
||||
@@ -4,6 +4,7 @@ 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'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -38,32 +39,12 @@ let pluginPageItems = ref([])
|
||||
// Vue 模式:动态加载的组件
|
||||
const dynamicComponent = computed(() => {
|
||||
if (renderMode.value === 'vue' && vueComponentUrl.value) {
|
||||
const url = vueComponentUrl.value
|
||||
return defineAsyncComponent(() =>
|
||||
api
|
||||
.get(url)
|
||||
.then((response: any) => {
|
||||
if (response) {
|
||||
const blob = new Blob([response.data], { type: 'text/javascript' })
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
return import(/* @vite-ignore */ blobUrl)
|
||||
} else {
|
||||
return { render: () => h('div', '组件加载失败: 未读取到文件数据') }
|
||||
}
|
||||
})
|
||||
.then(module => {
|
||||
if (module.default) {
|
||||
return module.default
|
||||
} else {
|
||||
console.error(`无法从 ${url} 加载默认导出的 Vue 组件`)
|
||||
return { render: () => h('div', '组件加载失败: 无默认导出') }
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(`无法加载插件组件: ${url}`, err)
|
||||
return { render: () => h('div', `组件加载失败:${err}`) }
|
||||
}),
|
||||
)
|
||||
return loadRemoteComponent(vueComponentUrl.value, {
|
||||
onError: error => {
|
||||
console.error(`加载插件组件失败: ${vueComponentUrl.value}`, error)
|
||||
$toast.error(`加载插件组件失败: ${error.message || '未知错误'}`)
|
||||
},
|
||||
})
|
||||
}
|
||||
return null
|
||||
})
|
||||
@@ -73,6 +54,11 @@ async function loadPluginUIData() {
|
||||
isRefreshed.value = false
|
||||
pluginPageItems.value = []
|
||||
renderMode.value = 'vuetify'
|
||||
|
||||
// 如果存在旧的组件URL,清除其缓存
|
||||
if (vueComponentUrl.value) {
|
||||
clearRemoteComponentCache(vueComponentUrl.value)
|
||||
}
|
||||
vueComponentUrl.value = null
|
||||
|
||||
try {
|
||||
@@ -105,6 +91,13 @@ function handleAction() {
|
||||
onMounted(() => {
|
||||
loadPluginUIData()
|
||||
})
|
||||
|
||||
// 组件卸载时清理资源
|
||||
onUnmounted(() => {
|
||||
if (vueComponentUrl.value) {
|
||||
clearRemoteComponentCache(vueComponentUrl.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<VDialog scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
||||
|
||||
@@ -13,6 +13,7 @@ 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'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -36,30 +37,11 @@ const pluginRenderMode = computed(() => props.config?.render_mode || 'vuetify')
|
||||
const dynamicPluginComponent = computed(() => {
|
||||
// 确保 config 存在并且 component_url 也存在
|
||||
if (pluginRenderMode.value === 'vue' && props.config?.component_url) {
|
||||
const url = props.config?.component_url
|
||||
return defineAsyncComponent(() =>
|
||||
api
|
||||
.get(url)
|
||||
.then((response: any) => {
|
||||
if (response) {
|
||||
const blob = new Blob([response.data], { type: 'text/javascript' })
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
return import(/* @vite-ignore */ blobUrl)
|
||||
} else {
|
||||
return { render: () => h('div', '组件加载失败: 未读取到文件数据') }
|
||||
}
|
||||
})
|
||||
.then(module => {
|
||||
if (module.default) {
|
||||
return module.default
|
||||
} else {
|
||||
return { render: () => h('div', '组件加载失败: 无默认导出') }
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
return { render: () => h('div', '组件加载失败') }
|
||||
}),
|
||||
)
|
||||
return loadRemoteComponent(props.config.component_url, {
|
||||
onError: error => {
|
||||
console.error(`加载插件组件失败: ${props.config?.component_url}`, error)
|
||||
},
|
||||
})
|
||||
}
|
||||
return null
|
||||
})
|
||||
@@ -67,6 +49,11 @@ const dynamicPluginComponent = computed(() => {
|
||||
onUnmounted(() => {
|
||||
// 组件卸载时禁用刷新状态
|
||||
emit('update:refreshStatus', false)
|
||||
|
||||
// 清理远程组件缓存
|
||||
if (pluginRenderMode.value === 'vue' && props.config?.component_url) {
|
||||
clearRemoteComponentCache(props.config.component_url)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
|
||||
129
src/utils/remoteFederationLoader.ts
Normal file
129
src/utils/remoteFederationLoader.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 模块联邦动态加载器
|
||||
* 用于动态加载远程组件
|
||||
*/
|
||||
import { defineAsyncComponent, type Component } from 'vue'
|
||||
import api from '@/api'
|
||||
|
||||
// 远程组件加载状态
|
||||
interface RemoteModuleState {
|
||||
loading: boolean
|
||||
loaded: boolean
|
||||
module: any
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
// 已加载组件的缓存
|
||||
const loadedRemotes: Record<string, RemoteModuleState> = {}
|
||||
|
||||
/**
|
||||
* 获取和初始化远程组件
|
||||
* @param remoteUrl 远程组件URL
|
||||
* @param options 组件选项
|
||||
* @returns 异步组件
|
||||
*/
|
||||
export function loadRemoteComponent(
|
||||
remoteUrl: string,
|
||||
options: {
|
||||
timeout?: number
|
||||
onError?: (error: Error) => void
|
||||
} = {},
|
||||
): Component {
|
||||
const { timeout = 30000, onError } = options
|
||||
|
||||
// 创建异步组件
|
||||
return defineAsyncComponent({
|
||||
loader: async () => {
|
||||
try {
|
||||
// 检查缓存
|
||||
if (loadedRemotes[remoteUrl]?.loaded) {
|
||||
return loadedRemotes[remoteUrl].module
|
||||
}
|
||||
|
||||
// 标记加载状态
|
||||
if (!loadedRemotes[remoteUrl]) {
|
||||
loadedRemotes[remoteUrl] = {
|
||||
loading: false,
|
||||
loaded: false,
|
||||
module: null,
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
|
||||
// 如果正在加载,等待加载完成
|
||||
if (loadedRemotes[remoteUrl].loading) {
|
||||
while (loadedRemotes[remoteUrl].loading && !loadedRemotes[remoteUrl].loaded) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
}
|
||||
if (loadedRemotes[remoteUrl].error) {
|
||||
throw loadedRemotes[remoteUrl].error
|
||||
}
|
||||
return loadedRemotes[remoteUrl].module
|
||||
}
|
||||
|
||||
loadedRemotes[remoteUrl].loading = true
|
||||
|
||||
// 获取远程组件JS
|
||||
const response = await api.get(remoteUrl)
|
||||
if (!response) {
|
||||
throw new Error('无法加载远程组件:请求无响应或响应无数据')
|
||||
}
|
||||
|
||||
// 创建Blob URL并动态导入
|
||||
const blob = new Blob([response as any], { type: 'text/javascript' })
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
|
||||
// 动态导入模块
|
||||
const moduleExports = await import(/* @vite-ignore */ blobUrl)
|
||||
|
||||
// 清理Blob URL
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
|
||||
// 获取默认导出
|
||||
if (!moduleExports.default) {
|
||||
throw new Error('远程组件没有默认导出')
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
loadedRemotes[remoteUrl].module = moduleExports.default
|
||||
loadedRemotes[remoteUrl].loaded = true
|
||||
loadedRemotes[remoteUrl].loading = false
|
||||
|
||||
return moduleExports.default
|
||||
} catch (error: any) {
|
||||
console.error(`加载远程组件失败: ${remoteUrl}`, error)
|
||||
loadedRemotes[remoteUrl].error = error
|
||||
loadedRemotes[remoteUrl].loading = false
|
||||
|
||||
// 调用错误处理回调
|
||||
if (onError) onError(error)
|
||||
|
||||
// 返回一个简单的错误组件
|
||||
return {
|
||||
render: () =>
|
||||
h('div', { class: 'text-error pa-4 text-center' }, `组件加载失败: ${error.message || '未知错误'}`),
|
||||
}
|
||||
}
|
||||
},
|
||||
timeout,
|
||||
// 可以定义加载中和错误状态的组件
|
||||
loadingComponent: {
|
||||
render: () => h('div', { class: 'text-center pa-4' }, '加载组件中...'),
|
||||
},
|
||||
errorComponent: {
|
||||
render: () => h('div', { class: 'text-error pa-4 text-center' }, '组件加载失败'),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 清除缓存的组件
|
||||
export function clearRemoteComponentCache(remoteUrl?: string) {
|
||||
if (remoteUrl) {
|
||||
delete loadedRemotes[remoteUrl]
|
||||
} else {
|
||||
// 清除所有缓存
|
||||
Object.keys(loadedRemotes).forEach(key => {
|
||||
delete loadedRemotes[key]
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user