Skip to content

Commit ff8ce29

Browse files
danpeennyqykkScriptedAlchemy
authored
feat: add bridgeHook plugin system to support lifecycyle in bridge (#2992)
Co-authored-by: nyqykk <[email protected]> Co-authored-by: nyqykk <[email protected]> Co-authored-by: Zack Jackson <[email protected]>
1 parent f8742b7 commit ff8ce29

File tree

22 files changed

+2660
-1710
lines changed

22 files changed

+2660
-1710
lines changed

.changeset/great-feet-rule.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@module-federation/bridge-react': patch
3+
'@module-federation/bridge-vue3': patch
4+
'@module-federation/runtime': patch
5+
---
6+
7+
feat: feat: support lifecycyle hooks in module-deferation bridge

packages/bridge/bridge-react/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
"@loadable/component": "^5.16.4",
4848
"@module-federation/bridge-shared": "workspace:*",
4949
"@module-federation/sdk": "workspace:*",
50-
"react-error-boundary": "^4.0.13"
50+
"react-error-boundary": "^4.0.13",
51+
"@module-federation/runtime": "workspace:*"
5152
},
5253
"peerDependencies": {
5354
"react": ">=16.9.0",

packages/bridge/bridge-react/src/create.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import React, { forwardRef } from 'react';
2-
import type { ProviderParams } from '@module-federation/bridge-shared';
3-
import { LoggerInstance } from './utils';
42
import {
53
ErrorBoundary,
64
ErrorBoundaryPropsWithComponent,
75
} from 'react-error-boundary';
6+
import { LoggerInstance } from './utils';
87
import RemoteApp from './remote';
8+
import type { ProviderParams } from '@module-federation/bridge-shared';
99

1010
export interface RenderFnParams extends ProviderParams {
1111
dom?: any;

packages/bridge/bridge-react/src/provider.tsx

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,24 @@ import { useLayoutEffect, useRef, useState } from 'react';
22
import * as React from 'react';
33
import ReactDOM from 'react-dom';
44
import ReactDOMClient from 'react-dom/client';
5-
import { RouterContext } from './context';
65
import type {
76
ProviderParams,
87
RenderFnParams,
98
} from '@module-federation/bridge-shared';
10-
import { LoggerInstance, atLeastReact18 } from './utils';
119
import { ErrorBoundary } from 'react-error-boundary';
10+
import { RouterContext } from './context';
11+
import { LoggerInstance, atLeastReact18 } from './utils';
12+
import { getInstance } from '@module-federation/runtime';
1213

14+
type RenderParams = RenderFnParams & {
15+
[key: string]: unknown;
16+
};
17+
type DestroyParams = {
18+
moduleName: string;
19+
dom: HTMLElement;
20+
};
1321
type RootType = HTMLElement | ReactDOMClient.Root;
22+
1423
type ProviderFnParams<T> = {
1524
rootComponent: React.ComponentType<T>;
1625
render?: (
@@ -22,6 +31,9 @@ type ProviderFnParams<T> = {
2231
export function createBridgeComponent<T>(bridgeInfo: ProviderFnParams<T>) {
2332
return () => {
2433
const rootMap = new Map<any, RootType>();
34+
const instance = getInstance();
35+
LoggerInstance.log(`createBridgeComponent remote instance`, instance);
36+
2537
const RawComponent = (info: { propsInfo: T; appInfo: ProviderParams }) => {
2638
const { appInfo, propsInfo, ...restProps } = info;
2739
const { moduleName, memoryRoute, basename = '/' } = appInfo;
@@ -37,7 +49,7 @@ export function createBridgeComponent<T>(bridgeInfo: ProviderFnParams<T>) {
3749
};
3850

3951
return {
40-
async render(info: RenderFnParams & any) {
52+
async render(info: RenderParams) {
4153
LoggerInstance.log(`createBridgeComponent render Info`, info);
4254
const {
4355
moduleName,
@@ -47,6 +59,10 @@ export function createBridgeComponent<T>(bridgeInfo: ProviderFnParams<T>) {
4759
fallback,
4860
...propsInfo
4961
} = info;
62+
63+
const beforeBridgeRenderRes =
64+
instance?.bridgeHook?.lifecycle?.beforeBridgeRender?.emit(info) || {};
65+
5066
const rootComponentWithErrorBoundary = (
5167
// set ErrorBoundary for RawComponent rendering error, usually caused by user app rendering error
5268
<ErrorBoundary FallbackComponent={fallback}>
@@ -56,11 +72,13 @@ export function createBridgeComponent<T>(bridgeInfo: ProviderFnParams<T>) {
5672
basename,
5773
memoryRoute,
5874
}}
59-
propsInfo={propsInfo}
75+
propsInfo={
76+
{ ...propsInfo, ...beforeBridgeRenderRes?.extraProps } as T
77+
}
6078
/>
6179
</ErrorBoundary>
6280
);
63-
81+
// call render function
6482
if (atLeastReact18(React)) {
6583
if (bridgeInfo?.render) {
6684
// in case bridgeInfo?.render is an async function, resolve this to promise
@@ -77,18 +95,27 @@ export function createBridgeComponent<T>(bridgeInfo: ProviderFnParams<T>) {
7795
const renderFn = bridgeInfo?.render || ReactDOM.render;
7896
renderFn?.(rootComponentWithErrorBoundary, info.dom);
7997
}
98+
99+
instance?.bridgeHook?.lifecycle?.afterBridgeRender?.emit(info) || {};
80100
},
81-
async destroy(info: { dom: HTMLElement }) {
101+
102+
async destroy(info: DestroyParams) {
82103
LoggerInstance.log(`createBridgeComponent destroy Info`, {
83104
dom: info.dom,
84105
});
106+
107+
instance?.bridgeHook?.lifecycle?.beforeBridgeDestroy?.emit(info);
108+
109+
// call destroy function
85110
if (atLeastReact18(React)) {
86111
const root = rootMap.get(info.dom);
87112
(root as ReactDOMClient.Root)?.unmount();
88113
rootMap.delete(info.dom);
89114
} else {
90115
ReactDOM.unmountComponentAtNode(info.dom);
91116
}
117+
118+
instance?.bridgeHook?.lifecycle?.afterBridgeDestroy?.emit(info);
92119
},
93120
rawComponent: bridgeInfo.rootComponent,
94121
__BRIDGE_FN__: (_args: T) => {},

packages/bridge/bridge-react/src/remote/index.tsx

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import React, {
77
} from 'react';
88
import * as ReactRouterDOM from 'react-router-dom';
99
import type { ProviderParams } from '@module-federation/bridge-shared';
10-
import { LoggerInstance, pathJoin } from '../utils';
1110
import { dispatchPopstateEnv } from '@module-federation/bridge-shared';
1211
import { ErrorBoundaryPropsWithComponent } from 'react-error-boundary';
12+
import { LoggerInstance, pathJoin, getRootDomDefaultClassName } from '../utils';
13+
import { getInstance } from '@module-federation/runtime';
1314

1415
declare const __APP_VERSION__: string;
1516
export interface RenderFnParams extends ProviderParams {
@@ -59,13 +60,15 @@ const RemoteAppWrapper = forwardRef(function (
5960

6061
const renderDom: React.MutableRefObject<HTMLElement | null> = useRef(null);
6162
const providerInfoRef = useRef<any>(null);
63+
const hostInstance = getInstance();
64+
LoggerInstance.log(`RemoteAppWrapper hostInstance >>>`, hostInstance);
6265

6366
useEffect(() => {
6467
const renderTimeout = setTimeout(() => {
6568
const providerReturn = providerInfo();
6669
providerInfoRef.current = providerReturn;
6770

68-
const renderProps = {
71+
let renderProps = {
6972
moduleName,
7073
dom: rootRef.current,
7174
basename,
@@ -78,7 +81,21 @@ const RemoteAppWrapper = forwardRef(function (
7881
`createRemoteComponent LazyComponent render >>>`,
7982
renderProps,
8083
);
84+
85+
LoggerInstance.log(
86+
`createRemoteComponent LazyComponent hostInstance >>>`,
87+
hostInstance,
88+
);
89+
const beforeBridgeRenderRes =
90+
hostInstance?.bridgeHook?.lifecycle?.beforeBridgeRender?.emit(
91+
renderProps,
92+
) || {};
93+
// @ts-ignore
94+
renderProps = { ...renderProps, ...beforeBridgeRenderRes.extraProps };
8195
providerReturn.render(renderProps);
96+
hostInstance?.bridgeHook?.lifecycle?.afterBridgeRender?.emit(
97+
renderProps,
98+
);
8299
});
83100

84101
return () => {
@@ -89,17 +106,39 @@ const RemoteAppWrapper = forwardRef(function (
89106
`createRemoteComponent LazyComponent destroy >>>`,
90107
{ moduleName, basename, dom: renderDom.current },
91108
);
109+
110+
hostInstance?.bridgeHook?.lifecycle?.beforeBridgeDestroy?.emit({
111+
moduleName,
112+
dom: renderDom.current,
113+
basename,
114+
memoryRoute,
115+
fallback,
116+
...resProps,
117+
});
118+
92119
providerInfoRef.current?.destroy({
120+
moduleName,
121+
dom: renderDom.current,
122+
});
123+
124+
hostInstance?.bridgeHook?.lifecycle?.afterBridgeDestroy?.emit({
125+
moduleName,
93126
dom: renderDom.current,
127+
basename,
128+
memoryRoute,
129+
fallback,
130+
...resProps,
94131
});
95132
}
96133
});
97134
};
98135
}, []);
99136

137+
// bridge-remote-root
138+
const rootComponentClassName = `${getRootDomDefaultClassName(moduleName)} ${props?.className}`;
100139
return (
101140
<div
102-
className={props?.className}
141+
className={rootComponentClassName}
103142
style={props?.style}
104143
ref={rootRef}
105144
></div>

packages/bridge/bridge-react/src/router-v5.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import React, { useContext } from 'react';
22
// The upper alias react-router-dom$ into this file avoids the loop
33
// @ts-ignore
44
import * as ReactRouterDom from 'react-router-dom/index.js';
5-
65
import { RouterContext } from './context';
76
import { LoggerInstance } from './utils';
87

packages/bridge/bridge-react/src/router.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ function WrapperRouterProvider(
5959
return <RouterProvider router={MemeoryRouterInstance} />;
6060
} else {
6161
const BrowserRouterInstance = createBrowserRouter(routers, {
62-
basename: routerContextProps.basename,
62+
basename: routerContextProps.basename || router?.basename,
6363
future: router.future,
6464
window: router.window,
6565
});

packages/bridge/bridge-react/src/utils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react';
2+
import { FederationHost } from '@module-federation/runtime';
23
import { createLogger } from '@module-federation/sdk';
34

45
export const LoggerInstance = createLogger(
@@ -41,3 +42,24 @@ export function pathJoin(...args: string[]) {
4142
}, '');
4243
return res || '/';
4344
}
45+
46+
export const getModuleName = (id: string) => {
47+
if (!id) {
48+
return id;
49+
}
50+
// separate module name without detailed module path
51+
// @vmok-e2e/edenx-demo-app2/button -> @vmok-e2e/edenx-demo-app2
52+
const idArray = id.split('/');
53+
if (idArray.length < 2) {
54+
return id;
55+
}
56+
return idArray[0] + '/' + idArray[1];
57+
};
58+
59+
export const getRootDomDefaultClassName = (moduleName: string) => {
60+
if (!moduleName) {
61+
return '';
62+
}
63+
const name = getModuleName(moduleName).replace(/\@/, '').replace(/\//, '-');
64+
return `bridge-root-component-${name}`;
65+
};

packages/bridge/bridge-react/vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export default defineConfig({
3636
'react-router-dom/',
3737
'react-router-dom/index.js',
3838
'react-router-dom/dist/index.js',
39+
'@module-federation/runtime',
3940
],
4041
plugins: [
4142
{

packages/bridge/vue3-bridge/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
},
3939
"dependencies": {
4040
"@module-federation/bridge-shared": "workspace:*",
41-
"@module-federation/sdk": "workspace:*"
41+
"@module-federation/sdk": "workspace:*",
42+
"@module-federation/runtime": "workspace:*"
4243
},
4344
"devDependencies": {
4445
"@vitejs/plugin-vue": "^5.0.4",

0 commit comments

Comments
 (0)