@fulgurjs/federation 6.8.7 → 6.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.en.md +3 -0
- package/README.md +3 -0
- package/dist/application-core-BuhXQmLF.d.cts +16 -0
- package/dist/application-core-DWI3eKme.d.ts +16 -0
- package/dist/bridge-app-react.cjs +177 -9
- package/dist/bridge-app-react.d.cts +15 -3
- package/dist/bridge-app-react.d.ts +15 -3
- package/dist/bridge-app-react.js +101 -9
- package/dist/bridge-app-vue.cjs +138 -12
- package/dist/bridge-app-vue.d.cts +20 -5
- package/dist/bridge-app-vue.d.ts +20 -5
- package/dist/bridge-app-vue.js +62 -12
- package/dist/bridge-host-react.d.cts +1 -1
- package/dist/bridge-host-react.d.ts +1 -1
- package/dist/bridge-host-vue.d.cts +1 -1
- package/dist/bridge-host-vue.d.ts +1 -1
- package/dist/chunk-6OB47FSR.js +81 -0
- package/dist/cli.js +349 -23
- package/dist/index.cjs +7 -6
- package/dist/index.js +7 -6
- package/dist/react-adapter.d.cts +1 -1
- package/dist/react-adapter.d.ts +1 -1
- package/dist/react.d.ts +29 -4
- package/dist/react.js +1 -0
- package/dist/{remote-types-xf1B6_pK.d.ts → remote-types-COmBhjRc.d.ts} +1 -1
- package/dist/{remote-types-58hdbx6x.d.cts → remote-types-CStGOJwu.d.cts} +1 -1
- package/dist/runtime.js +1 -1
- package/dist/vue-adapter.d.cts +1 -1
- package/dist/vue-adapter.d.ts +1 -1
- package/dist/vue-store.cjs +40 -0
- package/dist/vue-store.d.cts +4 -0
- package/dist/vue-store.d.ts +4 -0
- package/dist/vue-store.js +15 -0
- package/dist/vue.d.ts +37 -6
- package/dist/vue.js +2 -0
- package/docs/en/reference/api.md +38 -2
- package/docs/en/reference/types.md +27 -0
- package/docs/zh/reference/api.md +46 -2
- package/docs/zh/reference/types.md +27 -0
- package/examples/templates/README.md +3 -0
- package/examples/templates/react-host-vue-remote/README.md +24 -0
- package/examples/templates/react-host-vue-remote/package.json +2 -1
- package/examples/templates/react-host-vue-remote/pnpm-lock.yaml +189 -189
- package/examples/templates/react-host-vue-remote/pnpm-workspace.yaml +1 -1
- package/examples/templates/react-host-vue-remote/react-host/package.json +1 -1
- package/examples/templates/react-host-vue-remote/react-host/src/application.tsx +62 -0
- package/examples/templates/react-host-vue-remote/react-host/src/main.tsx +2 -62
- package/examples/templates/react-host-vue-remote/scripts/prod.mjs +101 -0
- package/examples/templates/react-host-vue-remote/vue-remote/README.md +17 -16
- package/examples/templates/react-host-vue-remote/vue-remote/fulgurjs.config.ts +2 -2
- package/examples/templates/react-host-vue-remote/vue-remote/package.json +1 -1
- package/examples/templates/react-host-vue-remote/vue-remote/src/AboutPage.vue +1 -0
- package/examples/templates/react-host-vue-remote/vue-remote/src/App.vue +15 -5
- package/examples/templates/react-host-vue-remote/vue-remote/src/HomePage.vue +1 -0
- package/examples/templates/react-host-vue-remote/vue-remote/src/application.ts +19 -0
- package/examples/templates/react-host-vue-remote/vue-remote/src/main.ts +2 -15
- package/examples/templates/react-react/README.md +25 -1
- package/examples/templates/react-react/host/package.json +1 -1
- package/examples/templates/react-react/host/src/application.tsx +33 -0
- package/examples/templates/react-react/host/src/main.tsx +3 -32
- package/examples/templates/react-react/package.json +2 -1
- package/examples/templates/react-react/pnpm-lock.yaml +123 -123
- package/examples/templates/react-react/pnpm-workspace.yaml +1 -1
- package/examples/templates/react-react/remote/package.json +1 -1
- package/examples/templates/react-react/remote/src/application.tsx +11 -0
- package/examples/templates/react-react/remote/src/main.tsx +3 -10
- package/examples/templates/react-react/scripts/prod.mjs +101 -0
- package/examples/templates/showcase/README.md +35 -7
- package/examples/templates/showcase/package.json +2 -1
- package/examples/templates/showcase/pnpm-lock.yaml +196 -196
- package/examples/templates/showcase/pnpm-workspace.yaml +1 -1
- package/examples/templates/showcase/react-host/package.json +1 -1
- package/examples/templates/showcase/react-host/src/application.tsx +28 -0
- package/examples/templates/showcase/react-host/src/demo-log.ts +5 -8
- package/examples/templates/showcase/react-host/src/main.tsx +2 -33
- package/examples/templates/showcase/react-host/src/routing.ts +2 -2
- package/examples/templates/showcase/react-remote/fulgurjs.config.ts +1 -1
- package/examples/templates/showcase/react-remote/package.json +1 -1
- package/examples/templates/showcase/react-remote/src/ChildLayout.tsx +3 -2
- package/examples/templates/showcase/react-remote/src/{bridge.tsx → application.tsx} +21 -21
- package/examples/templates/showcase/react-remote/src/child-bus.ts +23 -23
- package/examples/templates/showcase/react-remote/src/main.tsx +2 -18
- package/examples/templates/showcase/react-remote/src/pages/Locked.tsx +2 -1
- package/examples/templates/showcase/react-remote/src/pages/OrderDetail.tsx +2 -1
- package/examples/templates/showcase/react-remote/src/pages/OrderList.tsx +2 -1
- package/examples/templates/showcase/scripts/prod.mjs +101 -0
- package/examples/templates/showcase/vue-host/package.json +1 -1
- package/examples/templates/showcase/vue-host/src/application.ts +28 -0
- package/examples/templates/showcase/vue-host/src/demo-log.ts +4 -7
- package/examples/templates/showcase/vue-host/src/main.ts +2 -30
- package/examples/templates/showcase/vue-host/src/routing.ts +2 -2
- package/examples/templates/showcase/vue-remote/fulgurjs.config.ts +1 -1
- package/examples/templates/showcase/vue-remote/package.json +1 -1
- package/examples/templates/showcase/vue-remote/src/ChildLayout.vue +3 -2
- package/examples/templates/showcase/vue-remote/src/{bridge.ts → application.ts} +10 -13
- package/examples/templates/showcase/vue-remote/src/child-bus.ts +23 -23
- package/examples/templates/showcase/vue-remote/src/main.ts +2 -16
- package/examples/templates/showcase/vue-remote/src/pages/Locked.vue +2 -1
- package/examples/templates/showcase/vue-remote/src/pages/OrderDetail.vue +2 -1
- package/examples/templates/showcase/vue-remote/src/pages/OrderList.vue +2 -1
- package/examples/templates/vue-host-react-remote/README.md +24 -0
- package/examples/templates/vue-host-react-remote/package.json +2 -1
- package/examples/templates/vue-host-react-remote/pnpm-lock.yaml +184 -184
- package/examples/templates/vue-host-react-remote/pnpm-workspace.yaml +1 -1
- package/examples/templates/vue-host-react-remote/react-remote/README.md +17 -17
- package/examples/templates/vue-host-react-remote/react-remote/fulgurjs.config.ts +2 -2
- package/examples/templates/vue-host-react-remote/react-remote/package.json +1 -1
- package/examples/templates/vue-host-react-remote/react-remote/src/{bridge.tsx → application.tsx} +13 -24
- package/examples/templates/vue-host-react-remote/react-remote/src/main.tsx +2 -7
- package/examples/templates/vue-host-react-remote/scripts/prod.mjs +101 -0
- package/examples/templates/vue-host-react-remote/vue-host/package.json +1 -1
- package/examples/templates/vue-host-react-remote/vue-host/src/application.ts +5 -0
- package/examples/templates/vue-host-react-remote/vue-host/src/main.ts +2 -4
- package/examples/templates/vue-vue/README.md +25 -1
- package/examples/templates/vue-vue/host/package.json +1 -1
- package/examples/templates/vue-vue/host/src/application.ts +54 -0
- package/examples/templates/vue-vue/host/src/main.ts +3 -50
- package/examples/templates/vue-vue/package.json +2 -1
- package/examples/templates/vue-vue/pnpm-lock.yaml +182 -182
- package/examples/templates/vue-vue/pnpm-workspace.yaml +1 -1
- package/examples/templates/vue-vue/remote/package.json +1 -1
- package/examples/templates/vue-vue/remote/src/application.ts +6 -0
- package/examples/templates/vue-vue/remote/src/main.ts +3 -5
- package/examples/templates/vue-vue/scripts/prod.mjs +101 -0
- package/package.json +14 -9
- package/examples/templates/react-host-vue-remote/react-host/src/fulgurjs/types/bridge-vue-remote/metadata.json +0 -15
- package/examples/templates/react-host-vue-remote/react-host/src/fulgurjs/types/bridge-vue-remote/modules.d.ts +0 -14
- package/examples/templates/react-host-vue-remote/react-host/src/fulgurjs/types/bridge-vue-remote/registry.d.ts +0 -10
- package/examples/templates/react-host-vue-remote/vue-remote/src/bridge.ts +0 -57
- package/examples/templates/showcase/react-host/src/fulgurjs/types/vue-remote/metadata.json +0 -16
- package/examples/templates/showcase/react-host/src/fulgurjs/types/vue-remote/modules.d.ts +0 -71
- package/examples/templates/showcase/react-host/src/fulgurjs/types/vue-remote/registry.d.ts +0 -10
- package/examples/templates/showcase/vue-host/src/fulgurjs/types/react-remote/metadata.json +0 -16
- package/examples/templates/showcase/vue-host/src/fulgurjs/types/react-remote/modules.d.ts +0 -79
- package/examples/templates/showcase/vue-host/src/fulgurjs/types/react-remote/registry.d.ts +0 -10
- package/examples/templates/vue-host-react-remote/vue-host/src/fulgurjs/types/bridge-react-remote/metadata.json +0 -16
- package/examples/templates/vue-host-react-remote/vue-host/src/fulgurjs/types/bridge-react-remote/modules.d.ts +0 -10
- package/examples/templates/vue-host-react-remote/vue-host/src/fulgurjs/types/bridge-react-remote/registry.d.ts +0 -10
|
@@ -4,7 +4,7 @@ packages:
|
|
|
4
4
|
- react-host
|
|
5
5
|
- react-remote
|
|
6
6
|
minimumReleaseAgeExclude:
|
|
7
|
-
- '@fulgurjs/federation@6.
|
|
7
|
+
- '@fulgurjs/federation@6.9.0'
|
|
8
8
|
# pnpm >=12 构建脚本批准(12 起弃用 onlyBuiltDependencies;pnpm 9-11 忽略本键:
|
|
9
9
|
# esbuild >=0.16 二进制经 optionalDependencies 平台包分发,postinstall 仅校验,不阻塞安装)
|
|
10
10
|
allowBuilds:
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineApplication } from '@fulgurjs/federation/react'
|
|
2
|
+
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
|
3
|
+
import Layout from './Layout'
|
|
4
|
+
import HomePage from './pages/HomePage'
|
|
5
|
+
import AboutPage from './pages/AboutPage'
|
|
6
|
+
import BridgeVuePage from './pages/BridgeVuePage'
|
|
7
|
+
import { initBridgeRouting } from './routing'
|
|
8
|
+
import { installPopStateLogger } from './demo-log'
|
|
9
|
+
|
|
10
|
+
export default defineApplication((_props, { signal }) => {
|
|
11
|
+
const router = createBrowserRouter([
|
|
12
|
+
{
|
|
13
|
+
path: '/',
|
|
14
|
+
element: <Layout />,
|
|
15
|
+
children: [
|
|
16
|
+
{ index: true, element: <HomePage /> },
|
|
17
|
+
{ path: 'about', element: <AboutPage /> },
|
|
18
|
+
// catch-all:子应用挂载在 /br-vue 前缀下,内部导航全部命中同一条路由记录
|
|
19
|
+
{ path: 'br-vue/*', element: <BridgeVuePage /> },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
], { basename: import.meta.env.BASE_URL })
|
|
23
|
+
|
|
24
|
+
initBridgeRouting(router)
|
|
25
|
+
installPopStateLogger(signal)
|
|
26
|
+
|
|
27
|
+
return { element: <RouterProvider router={router} />, dispose: () => router.dispose() }
|
|
28
|
+
})
|
|
@@ -62,13 +62,10 @@ export function handleChildEvent(event: DemoChildEvent): void {
|
|
|
62
62
|
log(event.source, `${event.action} ${event.detail}`)
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
export function installPopStateLogger(): void {
|
|
69
|
-
if (popInstalled) return
|
|
70
|
-
popInstalled = true
|
|
65
|
+
/** POP 观测属于本应用实例;卸载后由 signal 移除,不遗留窗口监听。 */
|
|
66
|
+
export function installPopStateLogger(signal: AbortSignal): void {
|
|
67
|
+
if (signal.aborted) return
|
|
71
68
|
window.addEventListener('popstate', () => {
|
|
72
|
-
|
|
73
|
-
})
|
|
69
|
+
logNav('浏览器前进后退', window.location.pathname + window.location.search)
|
|
70
|
+
}, { signal })
|
|
74
71
|
}
|
|
@@ -1,33 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* data router(createBrowserRouter + RouterProvider)是 URL 同步的前提(declarative 模式无取消语义);
|
|
4
|
-
* catch-all 路由(/br-vue/* 单条记录渲染同一组件)保证子应用内部导航不重挂宿主组件。
|
|
5
|
-
* 注意:刻意不包 StrictMode——桥接契约工厂在每次 mount 时执行,StrictMode 的双 effect
|
|
6
|
-
* 会让子应用挂载计数翻倍,干扰「mount 次数恒为 1」演示(生产语义不受影响)。
|
|
7
|
-
*/
|
|
8
|
-
import { createRoot } from 'react-dom/client'
|
|
9
|
-
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
|
10
|
-
import Layout from './Layout'
|
|
11
|
-
import HomePage from './pages/HomePage'
|
|
12
|
-
import AboutPage from './pages/AboutPage'
|
|
13
|
-
import BridgeVuePage from './pages/BridgeVuePage'
|
|
14
|
-
import { initBridgeRouting } from './routing'
|
|
15
|
-
import { installPopStateLogger } from './demo-log'
|
|
16
|
-
|
|
17
|
-
const router = createBrowserRouter([
|
|
18
|
-
{
|
|
19
|
-
path: '/',
|
|
20
|
-
element: <Layout />,
|
|
21
|
-
children: [
|
|
22
|
-
{ index: true, element: <HomePage /> },
|
|
23
|
-
{ path: 'about', element: <AboutPage /> },
|
|
24
|
-
// catch-all:子应用挂载在 /br-vue 前缀下,内部导航全部命中同一条路由记录
|
|
25
|
-
{ path: 'br-vue/*', element: <BridgeVuePage /> },
|
|
26
|
-
],
|
|
27
|
-
},
|
|
28
|
-
])
|
|
29
|
-
|
|
30
|
-
initBridgeRouting(router)
|
|
31
|
-
installPopStateLogger()
|
|
32
|
-
|
|
33
|
-
createRoot(document.getElementById('root')!).render(<RouterProvider router={router} />)
|
|
1
|
+
import application from './application'
|
|
2
|
+
void application.start('#root').catch(console.error)
|
|
@@ -11,10 +11,10 @@ export const BRIDGE_BASE_PATH = '/br-vue'
|
|
|
11
11
|
let routing: BridgeHostRouting | undefined
|
|
12
12
|
|
|
13
13
|
export function initBridgeRouting(router: Parameters<typeof createReactBridgeNavigation>[0]): void {
|
|
14
|
-
routing = { basePath: BRIDGE_BASE_PATH, navigation: createReactBridgeNavigation(router) }
|
|
14
|
+
routing = { basePath: BRIDGE_BASE_PATH, navigation: createReactBridgeNavigation(router, { basename: import.meta.env.BASE_URL }) }
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function getBridgeRouting(): BridgeHostRouting {
|
|
18
|
-
if (!routing) throw new Error('桥接路由端口未初始化:请先在
|
|
18
|
+
if (!routing) throw new Error('桥接路由端口未初始化:请先在 application.tsx 调用 initBridgeRouting(router)')
|
|
19
19
|
return routing
|
|
20
20
|
}
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Outlet, useNavigate } from 'react-router-dom'
|
|
7
7
|
import type { ReactElement } from 'react'
|
|
8
|
-
import {
|
|
8
|
+
import { useChildReporter } from './child-bus'
|
|
9
9
|
|
|
10
10
|
export default function ChildLayout({ bridged = false }: { bridged?: boolean }): ReactElement {
|
|
11
|
+
const { getMountCount, reportNav } = useChildReporter()
|
|
11
12
|
const navigate = useNavigate()
|
|
12
13
|
|
|
13
14
|
const handlePushDemo = (): void => {
|
|
@@ -35,7 +36,7 @@ export default function ChildLayout({ bridged = false }: { bridged?: boolean }):
|
|
|
35
36
|
<div style={{ borderTop: '1px dashed #ccc', marginTop: 8, paddingTop: 8 }}>
|
|
36
37
|
<p data-demo-bridge-mode>当前应用被桥接:{String(bridged)}</p>
|
|
37
38
|
<p style={{ margin: '4px 0' }}>
|
|
38
|
-
<strong>React 子应用</strong
|
|
39
|
+
<strong>React 子应用</strong>({bridged ? '受控 memory' : '独立 web'} 路由)· 本会话挂载次数:{getMountCount()}
|
|
39
40
|
</p>
|
|
40
41
|
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', margin: '8px 0' }}>
|
|
41
42
|
<button onClick={handlePushDemo}>子应用 push /orders?q=演示&page=2</button>
|
|
@@ -1,18 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* 受控路由 = createReactBridgeRouter(memory data router):工厂返回 RouterProvider 元素;
|
|
5
|
-
* 宿主未提供路由通道时由插件统一抛 MFU-034(工厂无需手写守卫);根路径 / 经 loader
|
|
6
|
-
* redirect 规范化到 /orders,由插件以 replace 同步宿主(无新增历史条目)。
|
|
7
|
-
*/
|
|
8
|
-
import { redirect } from 'react-router-dom'
|
|
9
|
-
import { defineBridgeApp, createReactBridgeRouter, hasBridgeMount } from '@fulgurjs/federation/react'
|
|
1
|
+
// 独立与桥接使用同一套真实业务路由;每次工厂创建自己的 router。
|
|
2
|
+
import { createBrowserRouter, RouterProvider, redirect } from 'react-router-dom'
|
|
3
|
+
import { defineApplication, createReactBridgeRouter, hasBridgeMount } from '@fulgurjs/federation/react'
|
|
10
4
|
import ChildLayout from './ChildLayout'
|
|
11
5
|
import OrderList from './pages/OrderList'
|
|
12
6
|
import OrderDetail from './pages/OrderDetail'
|
|
13
7
|
import Settings from './pages/Settings'
|
|
14
8
|
import Locked from './pages/Locked'
|
|
15
|
-
import {
|
|
9
|
+
import { createChildReporter, ChildReporterContext, type ChildProps, type ChildReporter } from './child-bus'
|
|
16
10
|
|
|
17
11
|
/** 初始位置读取用的最小 data router 结构面(避免耦合 react-router 内部类型路径) */
|
|
18
12
|
interface MemoryRouterProbe {
|
|
@@ -32,27 +26,33 @@ const routes = [
|
|
|
32
26
|
},
|
|
33
27
|
]
|
|
34
28
|
|
|
35
|
-
export default
|
|
36
|
-
|
|
37
|
-
const init = ctx
|
|
29
|
+
export default defineApplication<ChildProps>((props, ctx) => {
|
|
30
|
+
const reporter = createChildReporter(props, ctx.signal)
|
|
31
|
+
const init = ctx.routing?.getLocation()
|
|
38
32
|
// 工厂上下文只属于当前实例;通过 props 传给布局,不写页面全局模式。
|
|
39
33
|
const instanceRoutes = routes.map(route => route.element
|
|
40
34
|
? { ...route, element: <ChildLayout bridged={hasBridgeMount(ctx)} /> }
|
|
41
35
|
: route)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
if (ctx.mode === 'standalone') {
|
|
37
|
+
const router = createBrowserRouter(instanceRoutes, { basename: import.meta.env.BASE_URL })
|
|
38
|
+
return { element: <ChildReporterContext.Provider value={reporter}><RouterProvider router={router} /></ChildReporterContext.Provider>, dispose: () => router.dispose() }
|
|
39
|
+
}
|
|
40
|
+
const conn = createReactBridgeRouter(ctx.routing!, instanceRoutes, { signal: ctx.signal })
|
|
41
|
+
// 迟到的就绪回调不得向已卸载实例上报;拒绝由实际挂载链处理。
|
|
42
|
+
void conn.routerReady.then(router => {
|
|
43
|
+
if (!ctx.signal.aborted && init) reportInitRedirect(router, init.pathname + init.search + init.hash, ctx.signal, reporter)
|
|
44
|
+
}, () => {})
|
|
45
|
+
return { element: <ChildReporterContext.Provider value={reporter}>{conn.element}</ChildReporterContext.Provider>, dispose: conn.dispose }
|
|
46
|
+
|
|
48
47
|
}, { routing: true })
|
|
49
48
|
|
|
50
49
|
/** 初始位置上报:createMemoryRouter 同步初始化,'/' 的 loader 重定向在微任务内落定,
|
|
51
50
|
* 下一个宏任务读取即为最终初始位置(订阅式一次性上报会在首个用户导航时才误触发)。 */
|
|
52
|
-
function reportInitRedirect(router: MemoryRouterProbe, initPath: string): void {
|
|
51
|
+
function reportInitRedirect(router: MemoryRouterProbe, initPath: string, signal: AbortSignal, reporter: ChildReporter): void {
|
|
53
52
|
setTimeout(() => {
|
|
53
|
+
if (signal.aborted) return
|
|
54
54
|
const current = router.state.location.pathname + router.state.location.search + router.state.location.hash
|
|
55
|
-
reportNav(
|
|
55
|
+
reporter.reportNav(
|
|
56
56
|
'子应用初始化',
|
|
57
57
|
'init',
|
|
58
58
|
current !== initPath
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
* 宿主经 appProps 传入稳定回调 onChildEvent;本模块持有该引用并维护挂载计数。
|
|
4
|
-
* 单页单实例假设:同一宿主页同时只挂一个本子应用实例(路由同步前缀互斥的常规形态)。
|
|
5
|
-
*/
|
|
6
|
-
export type DemoChildNavAction = 'push' | 'replace' | 'go' | 'init'
|
|
1
|
+
/** 应用实例自己的观测回调,通过原生 provide/Context 传给实际消费树。 */
|
|
2
|
+
import { createContext, useContext } from 'react'
|
|
7
3
|
|
|
4
|
+
export type DemoChildNavAction = 'push' | 'replace' | 'go' | 'init'
|
|
8
5
|
export type DemoChildEvent =
|
|
9
6
|
| { type: 'mounted'; mountCount: number }
|
|
10
7
|
| { type: 'nav'; source: string; action: DemoChildNavAction; detail: string }
|
|
11
|
-
|
|
12
8
|
export type DemoChildReporter = (event: DemoChildEvent) => void
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
/** 桥接工厂内调用:从 props 快照中取出宿主回调,并上报本次挂载计数 */
|
|
18
|
-
export function bindReporter(props: Record<string, unknown> | undefined): void {
|
|
19
|
-
const candidate = (props as { onChildEvent?: DemoChildReporter } | undefined)?.onChildEvent
|
|
20
|
-
bus.report = typeof candidate === 'function' ? candidate : undefined
|
|
21
|
-
mountCount += 1
|
|
22
|
-
bus.report?.({ type: 'mounted', mountCount })
|
|
9
|
+
export interface ChildReporter {
|
|
10
|
+
reportNav(source: string, action: DemoChildNavAction, detail: string): void
|
|
11
|
+
getMountCount(): number
|
|
23
12
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
13
|
+
export interface ChildProps { onChildEvent?: DemoChildReporter }
|
|
14
|
+
// 仅观测编号跨挂载累加;回调和所属实例不放在模块全局。
|
|
15
|
+
let nextMount = 0
|
|
16
|
+
export function createChildReporter(props: ChildProps, signal: AbortSignal): ChildReporter {
|
|
17
|
+
const mountCount = ++nextMount
|
|
18
|
+
if (!signal.aborted) props.onChildEvent?.({ type: 'mounted', mountCount })
|
|
19
|
+
return {
|
|
20
|
+
getMountCount: () => mountCount,
|
|
21
|
+
reportNav(source, action, detail) {
|
|
22
|
+
if (!signal.aborted) props.onChildEvent?.({ type: 'nav', source, action, detail })
|
|
23
|
+
},
|
|
24
|
+
}
|
|
28
25
|
}
|
|
29
26
|
|
|
30
|
-
export
|
|
31
|
-
|
|
27
|
+
export const ChildReporterContext = createContext<ChildReporter | null>(null)
|
|
28
|
+
export function useChildReporter(): ChildReporter {
|
|
29
|
+
const reporter = useContext(ChildReporterContext)
|
|
30
|
+
if (!reporter) throw new Error('子应用观测台未装配;从 application 工厂启动本应用。')
|
|
31
|
+
return reporter
|
|
32
32
|
}
|
|
@@ -1,18 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* 子应用真实能力经 exposes './bridge' 被宿主加载;直开本页仅作 dev server 健康检查。
|
|
4
|
-
*/
|
|
5
|
-
import { createRoot } from 'react-dom/client'
|
|
6
|
-
import type { ReactElement } from 'react'
|
|
7
|
-
|
|
8
|
-
function Standalone(): ReactElement {
|
|
9
|
-
return (
|
|
10
|
-
<div style={{ fontFamily: "'PingFang SC', 'Microsoft YaHei', sans-serif", padding: 24 }}>
|
|
11
|
-
<h1>react-remote(联邦子应用,5333)</h1>
|
|
12
|
-
<p>本应用通过 expose ./bridge(defineBridgeApp + routing 协议)被宿主加载。</p>
|
|
13
|
-
<p>请打开 Vue 宿主桥接页:<a href="http://localhost:5334/br-react/orders">http://localhost:5334/br-react/orders</a></p>
|
|
14
|
-
</div>
|
|
15
|
-
)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
createRoot(document.getElementById('root')!).render(<Standalone />)
|
|
1
|
+
import application from './application'
|
|
2
|
+
void application.start('#root').catch(console.error)
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { ReactElement } from 'react'
|
|
6
6
|
import { Link } from 'react-router-dom'
|
|
7
|
-
import {
|
|
7
|
+
import { useChildReporter } from '../child-bus'
|
|
8
8
|
|
|
9
9
|
export default function Locked(): ReactElement {
|
|
10
|
+
const { reportNav } = useChildReporter()
|
|
10
11
|
const handleBack = (): void => {
|
|
11
12
|
reportNav('子应用 Link', 'push', '/orders')
|
|
12
13
|
}
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { ReactElement } from 'react'
|
|
6
6
|
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
|
7
|
-
import {
|
|
7
|
+
import { useChildReporter } from '../child-bus'
|
|
8
8
|
|
|
9
9
|
export default function OrderDetail(): ReactElement {
|
|
10
|
+
const { reportNav } = useChildReporter()
|
|
10
11
|
const params = useParams()
|
|
11
12
|
const location = useLocation()
|
|
12
13
|
const navigate = useNavigate()
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { useMemo, useState } from 'react'
|
|
7
7
|
import type { ChangeEvent, ReactElement } from 'react'
|
|
8
8
|
import { useLocation, useNavigate } from 'react-router-dom'
|
|
9
|
-
import {
|
|
9
|
+
import { useChildReporter } from '../child-bus'
|
|
10
10
|
|
|
11
11
|
interface OrderItem {
|
|
12
12
|
id: number
|
|
@@ -26,6 +26,7 @@ const ORDERS: OrderItem[] = [
|
|
|
26
26
|
const PAGE_SIZE = 3
|
|
27
27
|
|
|
28
28
|
export default function OrderList(): ReactElement {
|
|
29
|
+
const { reportNav } = useChildReporter()
|
|
29
30
|
const location = useLocation()
|
|
30
31
|
const navigate = useNavigate()
|
|
31
32
|
const search = new URLSearchParams(location.search)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Standalone template production launcher. Uses only Node and the existing dev.config.json. */
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import http from 'node:http'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
9
|
+
const { apps } = JSON.parse(fs.readFileSync(path.join(root, 'scripts/dev.config.json'), 'utf8'))
|
|
10
|
+
if (!Array.isArray(apps) || !apps.length) throw new Error('dev.config.json 未登记应用。')
|
|
11
|
+
|
|
12
|
+
const mime = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.woff': 'font/woff', '.woff2': 'font/woff2', '.map': 'application/json' }
|
|
13
|
+
const inside = (file, directory) => file === directory || file.startsWith(directory + path.sep)
|
|
14
|
+
const mounts = apps.map(app => {
|
|
15
|
+
const dir = path.resolve(root, app.dir, 'dist')
|
|
16
|
+
if (!inside(dir, root)) throw new Error(`${app.name}: 构建目录超出模板。`)
|
|
17
|
+
const index = path.join(dir, 'index.html')
|
|
18
|
+
if (!fs.existsSync(index)) throw new Error(`${app.name}: 缺少 dist/index.html,请先 pnpm build。`)
|
|
19
|
+
const html = fs.readFileSync(index, 'utf8')
|
|
20
|
+
const tags = [...html.matchAll(/<script\b[^>]*>/gi)]
|
|
21
|
+
const tag = tags.find(item => /\btype=["']module["']/i.test(item[0]))?.[0]
|
|
22
|
+
const src = tag?.match(/\bsrc=["']([^"']+)["']/i)?.[1]
|
|
23
|
+
if (!src || /^(?:https?:)?\/\//.test(src)) throw new Error(`${app.name}: 无可本地挂载的模块入口。`)
|
|
24
|
+
// Resolve the actual emitted file suffix, rather than guessing base from an app name.
|
|
25
|
+
const pathname = decodeURIComponent(new URL(src, 'http://template.local/').pathname)
|
|
26
|
+
const parts = pathname.split('/').filter(Boolean)
|
|
27
|
+
const bases = []
|
|
28
|
+
for (let i = 0; i < parts.length; i++) {
|
|
29
|
+
const file = path.resolve(dir, ...parts.slice(i))
|
|
30
|
+
if (inside(file, dir) && fs.existsSync(file) && fs.statSync(file).isFile()) bases.push('/' + parts.slice(0, i).join('/') + (i ? '/' : ''))
|
|
31
|
+
}
|
|
32
|
+
if (bases.length !== 1) throw new Error(`${app.name}: 无法唯一对应构建入口 ${src};检查 Vite base 和干净构建输出。`)
|
|
33
|
+
return { ...app, dir, prefix: bases[0] }
|
|
34
|
+
})
|
|
35
|
+
const hosts = mounts.filter(app => app.host)
|
|
36
|
+
if (!hosts.length) throw new Error('dev.config.json 未登记 host:true 的宿主。')
|
|
37
|
+
const ports = new Set()
|
|
38
|
+
const servers = []
|
|
39
|
+
let stopping = false
|
|
40
|
+
function stop(code = 0) {
|
|
41
|
+
if (stopping) return
|
|
42
|
+
stopping = true
|
|
43
|
+
for (const server of servers) server.close()
|
|
44
|
+
const timer = setTimeout(() => {
|
|
45
|
+
for (const server of servers) server.closeAllConnections?.()
|
|
46
|
+
process.exit(code)
|
|
47
|
+
}, 1000)
|
|
48
|
+
timer.unref()
|
|
49
|
+
process.exitCode = code
|
|
50
|
+
}
|
|
51
|
+
process.once('SIGINT', () => stop(0))
|
|
52
|
+
process.once('SIGTERM', () => stop(0))
|
|
53
|
+
process.once('uncaughtException', error => { console.error(error); stop(1) })
|
|
54
|
+
|
|
55
|
+
for (const host of hosts) {
|
|
56
|
+
if (!Number.isInteger(host.port) || host.port < 1 || host.port > 65535 || ports.has(host.port)) throw new Error(`无效或重复宿主端口:${host.port}`)
|
|
57
|
+
ports.add(host.port)
|
|
58
|
+
const site = [host, ...mounts.filter(app => !app.host)].sort((a, b) => b.prefix.length - a.prefix.length)
|
|
59
|
+
const prefixes = new Set()
|
|
60
|
+
for (const app of site) {
|
|
61
|
+
if (prefixes.has(app.prefix)) throw new Error(`${host.name}: 构建 base ${app.prefix} 冲突;宿主和远程不能部署在同一路径。`)
|
|
62
|
+
prefixes.add(app.prefix)
|
|
63
|
+
}
|
|
64
|
+
const server = http.createServer((req, res) => {
|
|
65
|
+
try {
|
|
66
|
+
const url = new URL(req.url, `http://localhost:${host.port}`)
|
|
67
|
+
if (url.pathname === '/' && host.prefix !== '/') {
|
|
68
|
+
res.writeHead(302, { location: host.prefix, 'cache-control': 'no-cache' })
|
|
69
|
+
return res.end()
|
|
70
|
+
}
|
|
71
|
+
const app = site.find(item => url.pathname === item.prefix.slice(0, -1) || url.pathname.startsWith(item.prefix))
|
|
72
|
+
if (!app) { res.writeHead(404); return res.end('未登记此部署路径') }
|
|
73
|
+
const relative = decodeURIComponent(url.pathname.slice(app.prefix.length)) || 'index.html'
|
|
74
|
+
let file = path.resolve(app.dir, relative)
|
|
75
|
+
if (!inside(file, app.dir)) { res.writeHead(403); return res.end() }
|
|
76
|
+
if (!fs.existsSync(file) || fs.statSync(file).isDirectory()) {
|
|
77
|
+
if (path.extname(relative)) { res.writeHead(404); return res.end('资源不存在;不会回退成HTML') }
|
|
78
|
+
file = path.join(app.dir, 'index.html')
|
|
79
|
+
}
|
|
80
|
+
const stable = /(?:^|\/)(?:index\.html|fulgurjs-(?:remoteEntry\.js|manifest\.json)|_app\.config\.js)$/.test(file)
|
|
81
|
+
res.writeHead(200, {
|
|
82
|
+
'content-type': mime[path.extname(file)] ?? 'application/octet-stream',
|
|
83
|
+
'cache-control': stable ? 'no-cache' : 'public, max-age=31536000, immutable',
|
|
84
|
+
'access-control-allow-origin': '*',
|
|
85
|
+
})
|
|
86
|
+
res.end(fs.readFileSync(file))
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error(`生产请求失败:${error.message}`)
|
|
89
|
+
if (!res.headersSent) res.writeHead(400)
|
|
90
|
+
res.end('无效请求或资源不可读')
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
servers.push(server)
|
|
94
|
+
server.on('error', error => {
|
|
95
|
+
console.error(`${host.name}:${host.port} 启动失败:${error.message};停止本次全部生产服务器,不会按端口杀已有进程。`)
|
|
96
|
+
stop(1)
|
|
97
|
+
})
|
|
98
|
+
server.listen(host.port, '127.0.0.1', () => {
|
|
99
|
+
if (!stopping) console.log(`生产宿主 ${host.name}: http://localhost:${host.port}${host.prefix}(仅dist,无开发服务器)`)
|
|
100
|
+
})
|
|
101
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createApp } from 'vue'
|
|
2
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
3
|
+
import { defineApplication } from '@fulgurjs/federation/vue'
|
|
4
|
+
import App from './App.vue'
|
|
5
|
+
import HomePage from './pages/HomePage.vue'
|
|
6
|
+
import AboutPage from './pages/AboutPage.vue'
|
|
7
|
+
import BridgeReactPage from './pages/BridgeReactPage.vue'
|
|
8
|
+
import { initBridgeRouting } from './routing'
|
|
9
|
+
import { installGuard } from './guard'
|
|
10
|
+
import { installPopStateLogger } from './demo-log'
|
|
11
|
+
|
|
12
|
+
export default defineApplication((_props, { signal }) => {
|
|
13
|
+
const router = createRouter({
|
|
14
|
+
history: createWebHistory(import.meta.env.BASE_URL),
|
|
15
|
+
routes: [
|
|
16
|
+
{ path: '/', component: HomePage },
|
|
17
|
+
{ path: '/about', component: AboutPage },
|
|
18
|
+
// catch-all:子应用挂载在 /br-react 前缀下,内部导航全部命中同一条路由记录
|
|
19
|
+
{ path: '/br-react/:pathMatch(.*)*', component: BridgeReactPage },
|
|
20
|
+
],
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
initBridgeRouting(router)
|
|
24
|
+
installGuard(router)
|
|
25
|
+
installPopStateLogger(signal)
|
|
26
|
+
|
|
27
|
+
return { app: createApp(App), router }
|
|
28
|
+
})
|
|
@@ -37,13 +37,10 @@ export function handleChildEvent(event: DemoChildEvent): void {
|
|
|
37
37
|
logNav(event.source, `${event.action} ${event.detail}`)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
export function installPopStateLogger(): void {
|
|
44
|
-
if (popInstalled) return
|
|
45
|
-
popInstalled = true
|
|
40
|
+
/** POP 观测属于本应用实例;卸载后由 signal 移除,不遗留窗口监听。 */
|
|
41
|
+
export function installPopStateLogger(signal: AbortSignal): void {
|
|
42
|
+
if (signal.aborted) return
|
|
46
43
|
window.addEventListener('popstate', () => {
|
|
47
44
|
logNav('浏览器前进后退', window.location.pathname + window.location.search)
|
|
48
|
-
})
|
|
45
|
+
}, { signal })
|
|
49
46
|
}
|
|
@@ -1,30 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* 宿主 Router 是浏览器历史唯一写入方;catch-all 路由(/br-react/* 单条记录渲染同一组件)
|
|
4
|
-
* 保证子应用内部导航不重挂宿主组件。守卫与 POP 日志在此一次性安装。
|
|
5
|
-
*/
|
|
6
|
-
import { createApp } from 'vue'
|
|
7
|
-
import { createRouter, createWebHistory } from 'vue-router'
|
|
8
|
-
import App from './App.vue'
|
|
9
|
-
import HomePage from './pages/HomePage.vue'
|
|
10
|
-
import AboutPage from './pages/AboutPage.vue'
|
|
11
|
-
import BridgeReactPage from './pages/BridgeReactPage.vue'
|
|
12
|
-
import { initBridgeRouting } from './routing'
|
|
13
|
-
import { installGuard } from './guard'
|
|
14
|
-
import { installPopStateLogger } from './demo-log'
|
|
15
|
-
|
|
16
|
-
const router = createRouter({
|
|
17
|
-
history: createWebHistory(),
|
|
18
|
-
routes: [
|
|
19
|
-
{ path: '/', component: HomePage },
|
|
20
|
-
{ path: '/about', component: AboutPage },
|
|
21
|
-
// catch-all:子应用挂载在 /br-react 前缀下,内部导航全部命中同一条路由记录
|
|
22
|
-
{ path: '/br-react/:pathMatch(.*)*', component: BridgeReactPage },
|
|
23
|
-
],
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
initBridgeRouting(router)
|
|
27
|
-
installGuard(router)
|
|
28
|
-
installPopStateLogger()
|
|
29
|
-
|
|
30
|
-
createApp(App).use(router).mount('#app')
|
|
1
|
+
import application from './application'
|
|
2
|
+
void application.start('#app').catch(console.error)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* examples/templates/showcase/vue-host/src/routing.ts — 桥接宿主导航端口(模块级单例)。
|
|
3
|
-
*
|
|
3
|
+
* application.ts 组装宿主 router 后初始化一次:createVueBridgeNavigation(router)——
|
|
4
4
|
* Vue Router fullPath 已剥离 history base,逻辑路径无需再处理部署前缀;
|
|
5
5
|
* 端口引用保持稳定 → routing 键不变,宿主不重挂、不重复订阅。
|
|
6
6
|
*/
|
|
@@ -16,6 +16,6 @@ export function initBridgeRouting(router: Router): void {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function getBridgeRouting(): BridgeHostRouting {
|
|
19
|
-
if (!routing) throw new Error('桥接路由端口未初始化:请先在
|
|
19
|
+
if (!routing) throw new Error('桥接路由端口未初始化:请先在 application.ts 调用 initBridgeRouting(router)')
|
|
20
20
|
return routing
|
|
21
21
|
}
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
import { getCurrentInstance } from 'vue'
|
|
8
8
|
import { hasBridgeMount } from '@fulgurjs/federation/vue'
|
|
9
9
|
import { RouterView, useRouter } from 'vue-router'
|
|
10
|
-
import {
|
|
10
|
+
import { useChildReporter } from './child-bus'
|
|
11
|
+
const { getMountCount, reportNav } = useChildReporter()
|
|
11
12
|
|
|
12
13
|
// 查询当前 app,而不是页面是否存在其他桥接实例。
|
|
13
14
|
const bridged = hasBridgeMount(getCurrentInstance()?.appContext.app)
|
|
@@ -39,7 +40,7 @@ const handleGoForward = (): void => {
|
|
|
39
40
|
<section class="demo-child">
|
|
40
41
|
<p data-demo-bridge-mode>当前应用被桥接:{{ String(bridged) }}</p>
|
|
41
42
|
<p class="demo-child-title">
|
|
42
|
-
<strong>Vue 子应用</strong
|
|
43
|
+
<strong>Vue 子应用</strong>({{ bridged ? '受控 memory' : '独立 web' }} 路由)· 本会话挂载次数:{{ getMountCount() }}
|
|
43
44
|
</p>
|
|
44
45
|
<div class="demo-row">
|
|
45
46
|
<button @click="handlePushDemo">子应用 push /orders?q=演示&page=2</button>
|