增强模块联邦支持,添加动态导入远程模块的声明,更新示例项目以展示新组件结构和配置,调整 Vite 配置以支持更灵活的远程组件加载。

This commit is contained in:
jxxghp
2025-05-06 08:53:33 +08:00
parent 643ca35aed
commit d349d2b500
19 changed files with 1834 additions and 459 deletions

View File

@@ -7,12 +7,14 @@ import { useToast } from 'vue-toast-notification'
import FormRender from '../render/FormRender.vue'
import ProgressDialog from '../dialog/ProgressDialog.vue'
import { useI18n } from 'vue-i18n'
import { defineAsyncComponent } from 'vue'
import {
loadRemoteComponent,
clearRemoteComponentCache,
registerRemoteComponent,
getRemoteComponent,
} from '@/utils/remoteFederationLoader'
registerRemotePlugin,
isRemoteComponentLoaded,
ComponentType,
} from '@/utils/federationLoader'
// 国际化
const { t } = useI18n()
@@ -51,28 +53,54 @@ const isRefreshed = ref(false)
// 渲染模式: 'vuetify' 或 'vue'
const renderMode = ref('vuetify')
//Vue 模式:组件 URL
const vueComponentUrl = ref<string | null>(null)
// 挂载状态
const componentMounted = ref(false)
// 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)
// Vue 模式:动态加载的组件
const dynamicComponent = defineAsyncComponent({
loader: async () => {
if (renderMode.value !== 'vue' || !props.plugin?.id) {
return { render: () => null }
}
// 加载远程组件
return loadRemoteComponent(vueComponentUrl.value, {
onError: error => {
console.error(`加载插件组件失败: ${vueComponentUrl.value}`, error)
$toast.error(`加载插件组件失败: ${error.message || '未知错误'}`)
},
})
}
return null
try {
componentMounted.value = false
// 确保插件已注册
if (!isRemoteComponentLoaded(props.plugin.id, ComponentType.CONFIG)) {
await registerRemotePlugin(props.plugin.id)
}
// 加载配置组件
const component = await loadRemoteComponent(props.plugin.id, ComponentType.CONFIG)
componentMounted.value = true
if (!component) {
throw new Error('组件加载失败')
}
return component
} catch (error: any) {
console.error(`加载插件配置组件失败: ${props.plugin.id}`, error)
$toast.error(`加载插件组件失败: ${error.message || '未知错误'}`)
return {
render: () => h('div', { class: 'text-error pa-4' }, `加载失败: ${error.message || '未知错误'}`),
}
}
},
loadingComponent: {
render: () =>
h('div', { class: 'text-center pa-4' }, [
h('v-progress-circular', { indeterminate: true, class: 'mr-2' }),
'加载组件中...',
]),
},
errorComponent: {
render: () => h('div', { class: 'text-error pa-4 text-center' }, '组件加载失败'),
},
onError: error => {
console.error('加载插件组件出错', error)
},
})
//调用API读取UI和配置数据
@@ -82,12 +110,12 @@ async function loadPluginUIData() {
pluginFormItems = []
pluginConfigForm.value = {}
renderMode.value = 'vuetify'
componentMounted.value = false
// 如果存在旧的组件URL清除其缓存
if (vueComponentUrl.value) {
clearRemoteComponentCache(vueComponentUrl.value)
// 清除组件缓存
if (props.plugin?.id) {
clearRemoteComponentCache(props.plugin.id)
}
vueComponentUrl.value = null
try {
// 获取UI定义
@@ -98,15 +126,15 @@ async function loadPluginUIData() {
}
renderMode.value = result.render_mode
if (renderMode.value === 'vue') {
// 使用 component_url
vueComponentUrl.value = result.component_url
// 注册远程插件 (如果提供了组件URL则使用它)
if (props.plugin?.id) {
registerRemotePlugin(props.plugin.id, result.component_url)
}
// Vue模式下初始配置在同一个API返回
if (!isNullOrEmptyObject(result.model)) {
pluginConfigForm.value = result.model
}
if (!vueComponentUrl.value) {
console.error(`插件 ${props.plugin?.plugin_name} 配置错误未提供Vue组件URL`)
}
} else {
// Vuetify模式
pluginFormItems = result.conf || []
@@ -153,8 +181,8 @@ onBeforeMount(async () => {
// 组件卸载时清理资源
onUnmounted(() => {
if (vueComponentUrl.value) {
clearRemoteComponentCache(vueComponentUrl.value)
if (props.plugin?.id) {
clearRemoteComponentCache(props.plugin.id, ComponentType.CONFIG)
}
})
</script>
@@ -170,7 +198,7 @@ onUnmounted(() => {
<div v-if="!pluginFormItems || pluginFormItems.length === 0">此插件没有可配置项</div>
</div>
<!-- Vue 渲染模式 -->
<div v-else-if="renderMode === 'vue' && dynamicComponent">
<div v-else-if="renderMode === 'vue'">
<component :is="dynamicComponent" :initial-config="pluginConfigForm" @save="handleVueComponentSave" />
</div>
<!-- 加载中或错误 -->

View File

@@ -4,12 +4,14 @@ import type { Plugin } from '@/api/types'
import PageRender from '@/components/render/PageRender.vue'
import api from '@/api'
import { useToast } from 'vue-toast-notification'
import { defineAsyncComponent } from 'vue'
import {
loadRemoteComponent,
clearRemoteComponentCache,
registerRemoteComponent,
getRemoteComponent,
} from '@/utils/remoteFederationLoader'
registerRemotePlugin,
isRemoteComponentLoaded,
ComponentType,
} from '@/utils/federationLoader'
// 输入参数
const props = defineProps({
@@ -35,31 +37,57 @@ const isRefreshed = ref(false)
// 渲染模式: 'vuetify' 或 'vue'
const renderMode = ref('vuetify')
// Vue 模式:组件 URL
const vueComponentUrl = ref<string | null>(null)
// 挂载状态
const componentMounted = ref(false)
// 插件数据页面配置项
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)
// Vue 模式:动态加载的组件
const dynamicComponent = defineAsyncComponent({
loader: async () => {
if (renderMode.value !== 'vue' || !props.plugin?.id) {
return { render: () => null }
}
// 加载远程组件
return loadRemoteComponent(vueComponentUrl.value, {
onError: error => {
console.error(`加载插件组件失败: ${vueComponentUrl.value}`, error)
$toast.error(`加载插件组件失败: ${error.message || '未知错误'}`)
},
})
}
return null
try {
componentMounted.value = false
// 确保插件已注册
if (!isRemoteComponentLoaded(props.plugin.id, ComponentType.PAGE)) {
await registerRemotePlugin(props.plugin.id)
}
// 加载页面组件
const component = await loadRemoteComponent(props.plugin.id, ComponentType.PAGE)
componentMounted.value = true
if (!component) {
throw new Error('组件加载失败')
}
return component
} catch (error: any) {
console.error(`加载插件页面组件失败: ${props.plugin.id}`, error)
$toast.error(`加载插件组件失败: ${error.message || '未知错误'}`)
return {
render: () => h('div', { class: 'text-error pa-4' }, `加载失败: ${error.message || '未知错误'}`),
}
}
},
loadingComponent: {
render: () =>
h('div', { class: 'text-center pa-4' }, [
h('v-progress-circular', { indeterminate: true, class: 'mr-2' }),
'加载组件中...',
]),
},
errorComponent: {
render: () => h('div', { class: 'text-error pa-4 text-center' }, '组件加载失败'),
},
onError: error => {
console.error('加载插件组件出错', error)
},
})
// 调用API读取数据页面UI
@@ -67,12 +95,12 @@ async function loadPluginUIData() {
isRefreshed.value = false
pluginPageItems.value = []
renderMode.value = 'vuetify'
componentMounted.value = false
// 如果存在旧的组件URL清除其缓存
if (vueComponentUrl.value) {
clearRemoteComponentCache(vueComponentUrl.value)
// 清除组件缓存
if (props.plugin?.id) {
clearRemoteComponentCache(props.plugin.id)
}
vueComponentUrl.value = null
try {
const result: { [key: string]: any } = await api.get(`plugin/page/${props.plugin?.id}`)
@@ -80,14 +108,15 @@ async function loadPluginUIData() {
console.error(`插件 ${props.plugin?.plugin_name} UI数据加载失败无效的响应`)
return
}
renderMode.value = result.render_mode
if (renderMode.value === 'vue') {
vueComponentUrl.value = result.component_url
if (!vueComponentUrl.value) {
console.error(`插件 ${props.plugin?.plugin_name} 配置错误未提供Vue组件URL`)
renderMode.value = 'vuetify'
// 注册远程插件 (如果提供了组件URL则使用它)
if (props.plugin?.id) {
registerRemotePlugin(props.plugin.id, result.component_url)
}
} else {
// Vuetify模式
pluginPageItems.value = result.page || []
}
} catch (error: any) {
@@ -96,6 +125,7 @@ async function loadPluginUIData() {
isRefreshed.value = true
}
}
// 重新加载数据(可由 PageRender 或 Vue component 触发)
function handleAction() {
loadPluginUIData()
@@ -107,8 +137,8 @@ onMounted(() => {
// 组件卸载时清理资源
onUnmounted(() => {
if (vueComponentUrl.value) {
clearRemoteComponentCache(vueComponentUrl.value)
if (props.plugin?.id) {
clearRemoteComponentCache(props.plugin.id, ComponentType.PAGE)
}
})
</script>
@@ -124,7 +154,7 @@ onUnmounted(() => {
<div v-if="!pluginPageItems || pluginPageItems.length === 0">此插件没有详情页面</div>
</div>
<!-- Vue 渲染模式 -->
<div v-else-if="renderMode === 'vue' && dynamicComponent">
<div v-else-if="renderMode === 'vue'">
<component :is="dynamicComponent" @action="handleAction" />
</div>
</VCardText>

View File

@@ -12,12 +12,14 @@ 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 { defineAsyncComponent } from 'vue'
import {
loadRemoteComponent,
clearRemoteComponentCache,
registerRemoteComponent,
getRemoteComponent,
} from '@/utils/remoteFederationLoader'
registerRemotePlugin,
isRemoteComponentLoaded,
ComponentType,
} from '@/utils/federationLoader'
// 输入参数
const props = defineProps({
@@ -37,27 +39,53 @@ const emit = defineEmits(['update:refreshStatus'])
// 插件UI渲染模式 ('vuetify' 或 'vue')
const pluginRenderMode = computed(() => props.config?.render_mode || 'vuetify')
// 挂载状态
const componentMounted = ref(false)
// Vue 模式:动态加载的组件
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)
}
const dynamicPluginComponent = defineAsyncComponent({
loader: async () => {
if (pluginRenderMode.value !== 'vue' || !props.config?.id) {
return { render: () => null }
}
// 加载远程组件
return loadRemoteComponent(props.config.component_url, {
onError: error => {
console.error(`加载插件组件失败: ${props.config?.component_url}`, error)
},
})
}
return null
try {
componentMounted.value = false
// 确保插件已注册
if (!isRemoteComponentLoaded(props.config.id, ComponentType.DASHBOARD)) {
await registerRemotePlugin(props.config.id, props.config.component_url)
}
// 加载仪表板组件
const component = await loadRemoteComponent(props.config.id, ComponentType.DASHBOARD)
componentMounted.value = true
if (!component) {
throw new Error('组件加载失败')
}
return component
} catch (error: any) {
console.error(`加载插件仪表板组件失败: ${props.config.id}`, error)
return {
render: () => h('div', { class: 'text-error pa-4' }, `加载失败: ${error.message || '未知错误'}`),
}
}
},
loadingComponent: {
render: () =>
h('div', { class: 'text-center pa-4' }, [
h('v-progress-circular', { indeterminate: true, class: 'mr-2' }),
'加载组件中...',
]),
},
errorComponent: {
render: () => h('div', { class: 'text-error pa-4 text-center' }, '组件加载失败'),
},
onError: error => {
console.error('加载插件组件出错', error)
},
})
onUnmounted(() => {
@@ -65,8 +93,8 @@ onUnmounted(() => {
emit('update:refreshStatus', false)
// 清理远程组件缓存
if (pluginRenderMode.value === 'vue' && props.config?.component_url) {
clearRemoteComponentCache(props.config.component_url)
if (props.config?.id) {
clearRemoteComponentCache(props.config.id, ComponentType.DASHBOARD)
}
})
</script>
@@ -85,7 +113,7 @@ onUnmounted(() => {
<!-- 插件仪表板 -->
<template v-else-if="!isNullOrEmptyObject(props.config)">
<!-- Vue 渲染模式 -->
<div v-if="pluginRenderMode === 'vue' && dynamicPluginComponent">
<div v-if="pluginRenderMode === 'vue'">
<component :is="dynamicPluginComponent" :config="props.config" :allow-refresh="props.allowRefresh" />
<!-- Vue 模式下也可以显示拖拽句柄 -->
<div class="absolute right-5 top-5">

View File

@@ -20,6 +20,7 @@ import { CronVuetify } from '@vue-js-cron/vuetify'
// 4. 工具函数和其他辅助模块
import { fetchGlobalSettings } from './api'
import { isPWA } from './@core/utils/navigator'
import { loadRemoteComponents } from './utils/federationLoader'
// 5. 其他插件和功能模块
import ToastPlugin from 'vue-toast-notification'
@@ -60,6 +61,9 @@ async function initializeApp() {
// 全局设置
const globalSettings = await fetchGlobalSettings()
app.provide('globalSettings', globalSettings)
// 加载并注册远程联邦组件
await loadRemoteComponents()
} catch (error) {
console.error('Failed to initialize app', error)
}

View File

@@ -0,0 +1,337 @@
/**
* 动态模块联邦加载器
* 支持运行时动态注册和加载远程模块联邦组件
*/
import api from '@/api'
import type { Component } from 'vue'
// 远程组件配置接口
export interface RemoteComponentConfig {
id: string
url?: string
[key: string]: any
}
// 组件类型
export enum ComponentType {
PAGE = 'page',
CONFIG = 'config',
DASHBOARD = 'dashboard',
}
// 远程组件映射
interface RemoteModuleMap {
[pluginId: string]: {
[componentType in ComponentType]?: {
loaded: boolean
loading: boolean
error: Error | null
}
}
}
// 已加载的远程组件状态
const remoteModules: RemoteModuleMap = {}
// 全局联邦容器
declare global {
interface Window {
__FEDERATION__: Record<string, any>
}
}
// 确保全局联邦对象存在
if (!window.__FEDERATION__) {
window.__FEDERATION__ = {}
}
/**
* 加载远程模块的入口文件
* @param url 远程模块URL
* @returns Promise<void>
*/
async function loadRemoteEntry(url: string): Promise<void> {
return new Promise((resolve, reject) => {
// 创建script标签
const script = document.createElement('script')
script.src = url
script.type = 'text/javascript'
script.async = true
// 加载成功
script.onload = () => {
console.log(`远程模块加载成功: ${url}`)
resolve()
}
// 加载失败
script.onerror = error => {
console.error(`远程模块加载失败: ${url}`, error)
reject(new Error(`远程模块加载失败: ${url}`))
}
// 添加到文档
document.head.appendChild(script)
})
}
/**
* 获取模块联邦远程URL的标准化地址
* @param pluginId 插件ID
* @param url 组件URL (可选)
* @returns 完整的远程组件URL
*/
function getRemoteEntryUrl(pluginId: string, url?: string): string {
// 如果提供了完整URL则直接使用
if (url && (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/'))) {
return url.endsWith('/remoteEntry.js') ? url : `${url}/remoteEntry.js`
}
// 否则使用默认路径格式
return `/api/plugin/component/${pluginId}/remoteEntry.js`
}
/**
* 获取组件类型对应的模块名称
* @param componentType 组件类型
* @returns 模块名称
*/
function getComponentModule(componentType: ComponentType): string {
switch (componentType) {
case ComponentType.PAGE:
return './Page'
case ComponentType.CONFIG:
return './Config'
case ComponentType.DASHBOARD:
return './Dashboard'
default:
return './Component'
}
}
/**
* 从后端API获取远程组件列表并加载
* @returns Promise<boolean> 是否加载成功
*/
export async function loadRemoteComponents(): Promise<boolean> {
try {
// 调用后端API获取远程组件列表
const result = await api.get('plugins/remotes')
if (!result || !Array.isArray(result)) {
console.error('获取远程组件列表失败:无效的响应格式')
return false
}
// 加载所有远程组件
const remotes = result as RemoteComponentConfig[]
const loadPromises = remotes.map(remote => {
if (remote.id) {
return registerRemotePlugin(remote.id, remote.url).catch(err => {
console.error(`注册插件失败: ${remote.id}`, err)
return false
})
}
return Promise.resolve(false)
})
await Promise.allSettled(loadPromises)
console.log(`已加载远程组件列表, 成功加载: ${loadPromises.length}`)
return true
} catch (error) {
console.error('加载远程组件列表失败', error)
return false
}
}
/**
* 注册远程插件
* @param pluginId 插件ID
* @param url 远程URL (可选)
* @returns Promise<boolean> 是否注册成功
*/
export async function registerRemotePlugin(pluginId: string, url?: string): Promise<boolean> {
try {
// 生成远程插件的标识符
const remoteId = `plugin_${pluginId.replace(/[^a-zA-Z0-9_]/g, '_')}`
// 获取完整的远程入口URL
const remoteEntryUrl = getRemoteEntryUrl(pluginId, url)
// 加载远程入口
await loadRemoteEntry(remoteEntryUrl)
// 初始化插件状态
if (!remoteModules[pluginId]) {
remoteModules[pluginId] = {}
}
// 注册到全局联邦对象 (如果尚未注册)
if (!window.__FEDERATION__[remoteId]) {
// 在这里我们假设远程模块已经挂载到全局作用域
// 真实环境中这部分可能需要根据模块联邦的实际工作方式调整
window.__FEDERATION__[remoteId] = {
get: (module: string) => {
// 动态导入远程模块
return async () => {
// 这里可能需要根据实际情况调整
try {
// 理论上这里应该使用模块联邦的import机制
// 但由于我们是运行时加载,需要用一种变通方式
const moduleUrl = `${remoteEntryUrl.replace('remoteEntry.js', '')}${module.replace('./', '')}.js`
// 使用动态导入
const moduleExports = await import(/* @vite-ignore */ moduleUrl)
return { default: moduleExports.default }
} catch (error) {
console.error(`加载远程模块失败: ${remoteId}/${module}`, error)
throw error
}
}
},
}
}
console.log(`已注册远程插件: ${pluginId} (${remoteId})`)
return true
} catch (error) {
console.error(`注册远程插件失败: ${pluginId}`, error)
return false
}
}
/**
* 异步加载远程组件
* @param pluginId 插件ID
* @param componentType 组件类型
* @returns Promise<Component> 组件
*/
export async function loadRemoteComponent(pluginId: string, componentType: ComponentType): Promise<Component | null> {
try {
// 检查插件是否已初始化
if (!remoteModules[pluginId]) {
remoteModules[pluginId] = {}
}
// 检查组件状态
const componentState = remoteModules[pluginId][componentType]
// 如果已经在加载中,等待加载完成
if (componentState?.loading) {
while (componentState.loading && !componentState.loaded) {
await new Promise(resolve => setTimeout(resolve, 100))
}
if (componentState.error) {
throw componentState.error
}
return await loadRemoteComponentModule(pluginId, componentType)
}
// 标记为正在加载
remoteModules[pluginId][componentType] = {
loading: true,
loaded: false,
error: null,
}
// 加载组件
const component = await loadRemoteComponentModule(pluginId, componentType)
// 更新状态
remoteModules[pluginId][componentType] = {
loading: false,
loaded: true,
error: null,
}
return component
} catch (error: any) {
// 更新错误状态
if (remoteModules[pluginId]?.[componentType]) {
remoteModules[pluginId][componentType] = {
loading: false,
loaded: false,
error: error,
}
}
console.error(`加载远程组件失败: ${pluginId}/${componentType}`, error)
return null
}
}
/**
* 从远程模块加载特定组件
* @param pluginId 插件ID
* @param componentType 组件类型
* @returns Promise<Component> 组件
*/
async function loadRemoteComponentModule(pluginId: string, componentType: ComponentType): Promise<Component | null> {
// 生成远程插件的标识符
const remoteId = `plugin_${pluginId.replace(/[^a-zA-Z0-9_]/g, '_')}`
// 获取组件模块名称
const moduleName = getComponentModule(componentType)
try {
// 通过模块联邦获取组件
const factory = window.__FEDERATION__[remoteId].get(moduleName)
const Module = await factory()
// 返回组件
return Module.default
} catch (error) {
console.error(`加载远程组件模块失败: ${remoteId}/${moduleName}`, error)
throw error
}
}
/**
* 检查远程组件是否已加载
* @param pluginId 插件ID
* @param componentType 组件类型
* @returns boolean 是否已加载
*/
export function isRemoteComponentLoaded(pluginId: string, componentType: ComponentType): boolean {
return !!remoteModules[pluginId]?.[componentType]?.loaded
}
/**
* 获取远程组件加载错误
* @param pluginId 插件ID
* @param componentType 组件类型
* @returns Error | null 错误
*/
export function getRemoteComponentError(pluginId: string, componentType: ComponentType): Error | null {
return remoteModules[pluginId]?.[componentType]?.error || null
}
/**
* 清除远程组件缓存
* @param pluginId 插件ID (可选,不提供则清除所有)
* @param componentType 组件类型 (可选,不提供则清除插件的所有组件)
*/
export function clearRemoteComponentCache(pluginId?: string, componentType?: ComponentType): void {
if (!pluginId) {
// 清除所有缓存
Object.keys(remoteModules).forEach(id => {
delete remoteModules[id]
})
return
}
if (!remoteModules[pluginId]) {
return
}
if (!componentType) {
// 清除插件的所有组件
delete remoteModules[pluginId]
return
}
// 清除特定组件
if (remoteModules[pluginId][componentType]) {
delete remoteModules[pluginId][componentType]
}
}

View File

@@ -1,184 +0,0 @@
/**
* 模块联邦动态加载器
* 用于动态加载远程组件
*/
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> = {}
// 动态注册的远程组件映射
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
* @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]
})
}
}
// 取消注册远程组件
export function unregisterRemoteComponent(pluginId: string) {
if (registeredRemotes[pluginId]) {
delete registeredRemotes[pluginId]
console.log(`已取消注册远程组件: ${pluginId}`)
return true
}
return false
}