feat(微前端): 远端宿主支持 mount/unmount 微应用契约

- remoteLoader 拆出 LoadRemoteModule,先探测远端模块的 mount/unmount,再回退组件工厂

- RemoteAppHost 兼容两种契约:微应用由远端自带运行时挂载,组件工厂由宿主渲染
This commit is contained in:
TermiNexus committed 2026-10-05 17:45:40 +08:00
1 parent 7db725d9f4
commit 4be6ed89ee
2 files changed
+94 -29

No files matched your search

+15 -6
View File
@@ -42,7 +42,20 @@ export function GetRemoteHostRuntime(): RemoteHostRuntime {
}
/**
* 加载远端组件
* 加载远端模块命名空间(不做契约解析,交由调用方判断 mount / createComponent)
*
* @param RemoteEntry 远端入口 URL
*/
export async function LoadRemoteModule(RemoteEntry: string): Promise<Record<string, unknown>> {
if (!RemoteEntry) {
throw new Error('远端入口地址为空')
}
// 运行期 URL:@vite-ignore 阻止打包器静态分析(远端不在本仓库内)
return (await import(/* @vite-ignore */ RemoteEntry)) as Record<string, unknown>
}
/**
* 加载远端组件(组件工厂契约)
*
* @param RemoteEntry 远端入口 URL(如 https://sub.xxx/remoteEntry.js 或 /remote-demo/entry.js)
* @param ExportName 导出名(默认 default;工厂约定忽略此参数)
@@ -53,11 +66,7 @@ export async function LoadRemoteComponent(
RemoteEntry: string,
ExportName = 'default',
): Promise<Component> {
if (!RemoteEntry) {
throw new Error('远端入口地址为空')
}
// 运行期 URL:@vite-ignore 阻止打包器静态分析(远端不在本仓库内)
const mod: Record<string, unknown> = await import(/* @vite-ignore */ RemoteEntry)
const mod = await LoadRemoteModule(RemoteEntry)
// 约定 1:工厂(宿主把运行时以参数传入)
const factory = mod.createComponent
+79 -23
View File
@@ -1,34 +1,69 @@
<script setup lang="ts">
/**
* RemoteAppHost —— 远端微前端应用宿主页(PoC)
* RemoteAppHost —— 远端微前端应用宿主页
*
* 由 `/remote-app/:code` 路由驱动:
* 按 AppCode 取远端应用描述 → 运行时加载远端组件 → 原生渲染(非 iframe)。
* 远端加载失败只影响本页(展示错误提示),不阻塞宿主其余功能。
* 由 `/remote-app/:code` 路由驱动(Layout 子路由,渲染在 GERP 框架内)。
* 支持两种远端契约(见 apps/remoteLoader.ts):
* 1) 微应用:`mount(Element, Context)` / `unmount(Element)` —— 远端自带运行时,宿主只提供容器
* 2) 组件工厂:`createComponent(Host)` —— 宿主渲染远端返回的组件
* 远端加载/挂载失败只影响本页(展示错误提示),不阻塞宿主其余功能。
*/
import { ref, shallowRef, watch, type Component } from 'vue'
import { computed, onBeforeUnmount, ref, shallowRef, watch, type Component } from 'vue'
import { useRoute } from 'vue-router'
import { getAppModule } from '@/apps'
import { LoadRemoteComponent } from '@/apps/remoteLoader'
import { LoadRemoteComponent, LoadRemoteModule } from '@/apps/remoteLoader'
import { useUserStore } from '@/stores/user'
const route = useRoute()
const userStore = useUserStore()
const loading = ref(false)
const error = ref('')
const remoteComponent = shallowRef<Component | null>(null)
/** 挂载式微应用:远端模块命名空间(含 mount/unmount) */
const remoteModule = shallowRef<Record<string, unknown> | null>(null)
const mountEl = ref<HTMLElement | null>(null)
async function load(Code: string): Promise<void> {
/** 传给远端页面的上下文(当前登录人,用于「创建人/发布人」等展示) */
const remoteContext = computed<Record<string, unknown>>(() => {
const info = userStore.userInfo
return {
UserUUID: info?.UserUUID || '',
UserName: info?.DisplayName || info?.UserName || '',
}
})
function teardown(): void {
const mod = remoteModule.value
if (mod && typeof mod.unmount === 'function' && mountEl.value) {
try {
;(mod.unmount as (el: HTMLElement) => void)(mountEl.value)
} catch {
/* 远端卸载异常不影响宿主 */
}
}
remoteModule.value = null
remoteComponent.value = null
}
async function load(code: string): Promise<void> {
loading.value = true
error.value = ''
remoteComponent.value = null
teardown()
const mod = getAppModule(Code)
const mod = getAppModule(code)
if (!mod || mod.FrontendType !== 'remote' || !mod.RemoteEntry) {
error.value = `未注册的远端应用:${Code || '(空)'}`
error.value = `未注册的远端应用:${code || '(空)'}`
loading.value = false
return
}
try {
remoteComponent.value = await LoadRemoteComponent(mod.RemoteEntry, mod.RemoteModule || 'default')
const ns = await LoadRemoteModule(mod.RemoteEntry)
if (typeof ns.mount === 'function') {
remoteModule.value = ns
} else {
remoteComponent.value = await LoadRemoteComponent(mod.RemoteEntry, mod.RemoteModule || 'default')
}
} catch (e) {
error.value = `远端组件加载失败:${(e as Error).message}`
} finally {
@@ -36,26 +71,44 @@ async function load(Code: string): Promise<void> {
}
}
/** 挂载式微应用:容器就绪后调用远端 mount(上下文变化时重挂载) */
watch(
() => route.params.code,
(Code) => {
void load(String(Code || ''))
[remoteModule, mountEl, remoteContext],
() => {
const ns = remoteModule.value
if (!ns || !mountEl.value) return
try {
;(ns.mount as (el: HTMLElement, ctx: Record<string, unknown>) => void)(
mountEl.value, remoteContext.value,
)
} catch (e) {
error.value = `远端页面挂载失败:${(e as Error).message}`
}
},
{ immediate: true },
)
watch(
() => route.params.code,
(code) => {
void load(String(code || ''))
},
{ immediate: true },
)
onBeforeUnmount(teardown)
</script>
<template>
<div class="remote-app-host">
<el-alert
v-if="error"
type="error"
:closable="false"
show-icon
:title="error"
/>
<el-alert v-if="error" type="error" :closable="false" show-icon :title="error" />
<el-skeleton v-else-if="loading" :rows="6" animated />
<component :is="remoteComponent" v-else-if="remoteComponent" />
<template v-else>
<!-- 挂载式微应用:远端自带运行时,宿主只给容器 -->
<div v-if="remoteModule" ref="mountEl" class="remote-mount" />
<!-- 组件工厂:宿主渲染远端组件 -->
<component :is="remoteComponent" v-else-if="remoteComponent" />
</template>
</div>
</template>
@@ -63,6 +116,9 @@ watch(
.remote-app-host {
padding: 16px;
box-sizing: border-box;
height: 100%;
min-height: 100%;
}
.remote-mount {
min-height: 200px;
}
</style>