feat(微前端): 远端宿主支持 mount/unmount 微应用契约
- remoteLoader 拆出 LoadRemoteModule,先探测远端模块的 mount/unmount,再回退组件工厂 - RemoteAppHost 兼容两种契约:微应用由远端自带运行时挂载,组件工厂由宿主渲染
This commit is contained in:
1 parent
7db725d9f4
commit
4be6ed89ee
2 files changed
+94
-29
No files matched your search
@@ -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
@@ -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>
|
||||
Reference in new issue
Block a user