Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support lifecycyle hooks in module-deferation bridge #2992

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3118053
feat: add hook when snapshot is ready
nyqykk Aug 15, 2024
3082116
feat: support isolated report
nyqykk Aug 22, 2024
f716f18
chore: merge main
nyqykk Aug 22, 2024
edaffa2
Merge branch 'main' into feat/isolated-monitor
nyqykk Aug 26, 2024
62480b6
feat: support bridge lifecycle
nyqykk Aug 30, 2024
c868988
Merge branch 'feat/isolated-monitor' of github.com:module-federation/…
nyqykk Aug 30, 2024
cf83897
chore: modify bridge lifecycle
nyqykk Aug 30, 2024
aa05fc5
chore: change symbol name for module
nyqykk Sep 2, 2024
8aebc66
chore: sync main code
nyqykk Sep 3, 2024
e414ded
feat: add vue3 bridge lifecycle
nyqykk Sep 3, 2024
f575ec7
chore: export bridge plugin type
nyqykk Sep 4, 2024
fe9ad08
Merge branch 'main' into feat/isolated-monitor
nyqykk Sep 5, 2024
7257784
chore: sync branch
nyqykk Sep 5, 2024
74f057a
chore: merge main branch
danpeen Sep 14, 2024
14a30fa
fix: fix router ci failed issue
danpeen Sep 14, 2024
b47ea80
feat: update bridge render hook name
danpeen Sep 18, 2024
b02d9f0
chore: merge main branch
danpeen Sep 19, 2024
93627d9
chore: merge main branch
danpeen Sep 23, 2024
5e33da2
feat: update bridge hook
danpeen Sep 23, 2024
83e54ad
feat: update bridge lifecycle register logic in bridge-vue
danpeen Sep 23, 2024
2fac2ea
fix: bridge should use raw basename from application router itself
danpeen Sep 24, 2024
4e319bb
Merge branch 'main' into feat/bridge-lifecycle-hook
ScriptedAlchemy Sep 25, 2024
a891716
feat: add params to pass bridge hooks
danpeen Sep 30, 2024
5c7f0be
chore: merge main branch
danpeen Sep 30, 2024
a6c9c97
feat: add afterBridgeRender hook and afterBridgeDestroy hook
danpeen Oct 10, 2024
10bc78b
feat: receive extraProps from rederhook
danpeen Oct 15, 2024
19acf8b
feat: receive extraProps from rederhook update
danpeen Oct 15, 2024
25d850b
feat: add default className for bridge root component
danpeen Oct 18, 2024
8588609
chore: merge main branch
danpeen Oct 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/great-feet-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@module-federation/bridge-react': patch
'@module-federation/bridge-vue3': patch
'@module-federation/runtime': patch
---

feat: support module isolated reported
3 changes: 2 additions & 1 deletion packages/bridge/bridge-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0",
"react-router-dom": ">=4"
"react-router-dom": ">=4",
"@module-federation/runtime": "workspace:*"
},
"devDependencies": {
"@testing-library/react": "15.0.7",
Expand Down
4 changes: 2 additions & 2 deletions packages/bridge/bridge-react/src/create.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { forwardRef } from 'react';
import type { ProviderParams } from '@module-federation/bridge-shared';
import { LoggerInstance } from './utils';
import {
ErrorBoundary,
ErrorBoundaryPropsWithComponent,
} from 'react-error-boundary';
import { LoggerInstance } from './utils';
import RemoteApp from './remote';
import type { ProviderParams } from '@module-federation/bridge-shared';

export interface RenderFnParams extends ProviderParams {
dom?: any;
Expand Down
28 changes: 28 additions & 0 deletions packages/bridge/bridge-react/src/lifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { getInstance } from '@module-federation/runtime';
import helper from '@module-federation/runtime/helpers';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wonder if we should make this use named exports in the future, so that better tree shake or destructure is possible later. (for the helpers stuff)
import {global} from helper


function registerBridgeLifeCycle() {
const { registerPlugins, pluginHelper } = helper.global;
const host = getInstance();
const pluginSystem = new pluginHelper.PluginSystem({
beforeBridgeRender: new pluginHelper.SyncHook<[Record<string, any>], any>(),
afterBridgeRender: new pluginHelper.SyncHook<[Record<string, any>], any>(),
beforeBridgeDestroy: new pluginHelper.SyncHook<
[Record<string, any>],
any
>(),
afterBridgeDestroy: new pluginHelper.SyncHook<[Record<string, any>], any>(),
});
Comment on lines +5 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code introduces a plugin system with lifecycle hooks for a bridge component. While the implementation looks good, there are a few suggestions to improve type safety and readability:

  1. Consider using more specific types for the hook parameters instead of Record<string, any>. This will provide better type checking and documentation.

  2. The SyncHook generic type could be simplified if all hooks have the same signature. You could define a type alias for the hook signature.

Here's an example of how you could improve the code:

Suggested change
const { registerPlugins, pluginHelper } = helper.global;
const host = getInstance();
const pluginSystem = new pluginHelper.PluginSystem({
beforeBridgeRender: new pluginHelper.SyncHook<[Record<string, any>], any>(),
afterBridgeRender: new pluginHelper.SyncHook<[Record<string, any>], any>(),
beforeBridgeDestroy: new pluginHelper.SyncHook<
[Record<string, any>],
any
>(),
afterBridgeDestroy: new pluginHelper.SyncHook<[Record<string, any>], any>(),
});
type BridgeHookParams = {
// Define specific properties here
bridgeProps: Record<string, unknown>;
// Add more relevant properties
};
type BridgeHook = pluginHelper.SyncHook<[BridgeHookParams], void>;
const pluginSystem = new pluginHelper.PluginSystem({
beforeBridgeRender: new BridgeHook(),
afterBridgeRender: new BridgeHook(),
beforeBridgeDestroy: new BridgeHook(),
afterBridgeDestroy: new BridgeHook(),
});

This change improves type safety and makes the code more concise and easier to maintain.


if (host) {
registerPlugins<typeof pluginSystem.lifecycle, typeof pluginSystem>(
host?.options?.plugins,
[pluginSystem],
);
return pluginSystem;
}

return null;
}

export { registerBridgeLifeCycle };
70 changes: 62 additions & 8 deletions packages/bridge/bridge-react/src/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,37 @@ import { useLayoutEffect, useRef, useState } from 'react';
import * as React from 'react';
import ReactDOM from 'react-dom';
import ReactDOMClient from 'react-dom/client';
import { RouterContext } from './context';
import type {
ProviderParams,
RenderFnParams,
} from '@module-federation/bridge-shared';
import { LoggerInstance, atLeastReact18 } from './utils';
import { ErrorBoundary } from 'react-error-boundary';
import { RouterContext } from './context';
import { LoggerInstance, atLeastReact18 } from './utils';

type RenderParams = RenderFnParams & any;
type DestroyParams = {
dom: HTMLElement;
};
type RootType = HTMLElement | ReactDOMClient.Root;

type BridgeHooks = {
beforeBridgeRender?: (params: RenderFnParams) => any;
afterBridgeRender?: (params: RenderFnParams) => any;
beforeBridgeDestroy?: (params: DestroyParams) => any;
afterBridgeDestroy?: (params: DestroyParams) => any;
};

type ProviderFnParams<T> = {
rootComponent: React.ComponentType<T>;
render?: (
App: React.ReactElement,
id?: HTMLElement | string,
) => RootType | Promise<RootType>;
hooks?: BridgeHooks;
};

export function createBridgeComponent<T>(bridgeInfo: ProviderFnParams<T>) {
return () => {
return (params: { hooks?: BridgeHooks }) => {
const rootMap = new Map<any, RootType>();
const RawComponent = (info: { propsInfo: T; appInfo: ProviderParams }) => {
const { appInfo, propsInfo, ...restProps } = info;
Expand All @@ -37,7 +49,7 @@ export function createBridgeComponent<T>(bridgeInfo: ProviderFnParams<T>) {
};

return {
async render(info: RenderFnParams & any) {
async render(info: RenderParams) {
LoggerInstance.log(`createBridgeComponent render Info`, info);
const {
moduleName,
Expand All @@ -47,6 +59,21 @@ export function createBridgeComponent<T>(bridgeInfo: ProviderFnParams<T>) {
fallback,
...propsInfo
} = info;

const beforeBridgeRender =
(bridgeInfo?.hooks && bridgeInfo?.hooks.beforeBridgeRender) ||
params?.hooks?.beforeBridgeRender;
Comment on lines +63 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code for retrieving beforeBridgeRender can be simplified using the nullish coalescing operator:

Suggested change
const beforeBridgeRender =
(bridgeInfo?.hooks && bridgeInfo?.hooks.beforeBridgeRender) ||
params?.hooks?.beforeBridgeRender;
const beforeBridgeRender = bridgeInfo?.hooks?.beforeBridgeRender ?? params?.hooks?.beforeBridgeRender;

This change improves readability and reduces the chance of errors.


// 可通过beforeBridgeRender返回一个props对象,用于传递额外的 props 参数
const beforeBridgeRenderRes =
beforeBridgeRender && beforeBridgeRender(info);
const extraProps =
beforeBridgeRenderRes &&
typeof beforeBridgeRenderRes === 'object' &&
beforeBridgeRenderRes?.extraProps
? beforeBridgeRenderRes?.extraProps
: {};

const rootComponentWithErrorBoundary = (
// set ErrorBoundary for RawComponent rendering error, usually caused by user app rendering error
<ErrorBoundary FallbackComponent={fallback}>
Expand All @@ -56,11 +83,11 @@ export function createBridgeComponent<T>(bridgeInfo: ProviderFnParams<T>) {
basename,
memoryRoute,
}}
propsInfo={propsInfo}
propsInfo={{ ...propsInfo, ...extraProps } as T}
/>
</ErrorBoundary>
);

// call render function
if (atLeastReact18(React)) {
if (bridgeInfo?.render) {
// in case bridgeInfo?.render is an async function, resolve this to promise
Expand All @@ -77,18 +104,45 @@ export function createBridgeComponent<T>(bridgeInfo: ProviderFnParams<T>) {
const renderFn = bridgeInfo?.render || ReactDOM.render;
renderFn?.(rootComponentWithErrorBoundary, info.dom);
}

const afterBridgeRender =
(bridgeInfo?.hooks && bridgeInfo?.hooks.afterBridgeDestroy) ||
params?.hooks?.afterBridgeRender;
afterBridgeRender && afterBridgeRender(info);
},
async destroy(info: { dom: HTMLElement }) {

async destroy(info: DestroyParams) {
LoggerInstance.log(`createBridgeComponent destroy Info`, {
dom: info.dom,
});

// call beforeBridgeDestroy hook
if (
bridgeInfo?.hooks &&
bridgeInfo?.hooks.beforeBridgeDestroy &&
typeof bridgeInfo?.hooks.beforeBridgeDestroy === 'function'
) {
bridgeInfo.hooks.beforeBridgeDestroy(info);
}

const beforeBridgeDestroy =
(bridgeInfo?.hooks && bridgeInfo?.hooks.beforeBridgeDestroy) ||
params?.hooks?.beforeBridgeDestroy;
beforeBridgeDestroy && beforeBridgeDestroy(info);

// call destroy function
if (atLeastReact18(React)) {
const root = rootMap.get(info.dom);
(root as ReactDOMClient.Root)?.unmount();
rootMap.delete(info.dom);
} else {
ReactDOM.unmountComponentAtNode(info.dom);
}

const afterBridgeDestroy =
(bridgeInfo?.hooks && bridgeInfo?.hooks.afterBridgeDestroy) ||
params?.hooks?.afterBridgeDestroy;
afterBridgeDestroy && afterBridgeDestroy(info);
},
rawComponent: bridgeInfo.rootComponent,
__BRIDGE_FN__: (_args: T) => {},
Expand Down
34 changes: 32 additions & 2 deletions packages/bridge/bridge-react/src/remote/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import React, {
} from 'react';
import * as ReactRouterDOM from 'react-router-dom';
import type { ProviderParams } from '@module-federation/bridge-shared';
import { LoggerInstance, pathJoin } from '../utils';
import { dispatchPopstateEnv } from '@module-federation/bridge-shared';
import { ErrorBoundaryPropsWithComponent } from 'react-error-boundary';
import { registerBridgeLifeCycle } from '../lifecycle';
import { LoggerInstance, pathJoin } from '../utils';

declare const __APP_VERSION__: string;
export interface RenderFnParams extends ProviderParams {
Expand Down Expand Up @@ -39,6 +40,7 @@ const RemoteAppWrapper = forwardRef(function (
props: RemoteAppParams & RenderFnParams,
ref,
) {
const bridgeHook = registerBridgeLifeCycle();
const RemoteApp = () => {
LoggerInstance.log(`RemoteAppWrapper RemoteApp props >>>`, { props });
const {
Expand All @@ -65,7 +67,7 @@ const RemoteAppWrapper = forwardRef(function (
const providerReturn = providerInfo();
providerInfoRef.current = providerReturn;

const renderProps = {
let renderProps = {
moduleName,
dom: rootRef.current,
basename,
Expand All @@ -78,6 +80,24 @@ const RemoteAppWrapper = forwardRef(function (
`createRemoteComponent LazyComponent render >>>`,
renderProps,
);

if (bridgeHook && bridgeHook?.lifecycle?.beforeBridgeRender) {
const beforeBridgeRenderRes =
bridgeHook?.lifecycle?.beforeBridgeRender.emit({
...renderProps,
});
const extraProps =
beforeBridgeRenderRes &&
typeof beforeBridgeRenderRes === 'object' &&
beforeBridgeRenderRes?.extraProps
? beforeBridgeRenderRes?.extraProps
: {};

renderProps = {
...renderProps,
...extraProps,
} as any;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the as any type assertion. Instead, properly type renderProps or use a more specific type assertion if necessary. Avoiding any helps maintain type safety throughout your codebase.

Suggested change
} as any;
} as RenderProps; // Replace 'RenderProps' with the actual type of renderProps

}
Comment on lines +99 to +115
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lifecycle hook implementation can be simplified and made more robust:

  1. Use optional chaining to simplify nested property access.
  2. Use destructuring to extract the beforeBridgeRender method.
  3. Use the nullish coalescing operator for default values.
  4. Simplify the type check for beforeBridgeRenderRes.

Here's a suggested refactoring:

Suggested change
if (bridgeHook && bridgeHook?.lifecycle?.beforeBridgeRender) {
const beforeBridgeRenderRes =
bridgeHook?.lifecycle?.beforeBridgeRender.emit({
...renderProps,
});
const extraProps =
beforeBridgeRenderRes &&
typeof beforeBridgeRenderRes === 'object' &&
beforeBridgeRenderRes?.extraProps
? beforeBridgeRenderRes?.extraProps
: {};
renderProps = {
...renderProps,
...extraProps,
} as any;
}
if (bridgeHook?.lifecycle?.beforeBridgeRender) {
const { beforeBridgeRender } = bridgeHook.lifecycle;
const beforeBridgeRenderRes = beforeBridgeRender.emit(renderProps);
const extraProps = beforeBridgeRenderRes?.extraProps ?? {};
renderProps = {
...renderProps,
...extraProps,
};
}

This change improves readability and reduces the chance of runtime errors.

providerReturn.render(renderProps);
});

Expand All @@ -89,6 +109,16 @@ const RemoteAppWrapper = forwardRef(function (
`createRemoteComponent LazyComponent destroy >>>`,
{ moduleName, basename, dom: renderDom.current },
);
if (bridgeHook && bridgeHook?.lifecycle?.afterBridgeDestroy) {
bridgeHook?.lifecycle?.afterBridgeDestroy.emit({
moduleName,
dom: renderDom.current,
basename,
Comment on lines +127 to +131
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the beforeBridgeRender hook, the afterBridgeDestroy hook implementation can be simplified:

Suggested change
if (bridgeHook && bridgeHook?.lifecycle?.afterBridgeDestroy) {
bridgeHook?.lifecycle?.afterBridgeDestroy.emit({
moduleName,
dom: renderDom.current,
basename,
if (bridgeHook?.lifecycle?.afterBridgeDestroy) {
bridgeHook.lifecycle.afterBridgeDestroy.emit({
moduleName,
dom: renderDom.current,
basename,

This change uses optional chaining to simplify the nested property access and improve readability.

memoryRoute,
fallback,
...resProps,
});
}
providerInfoRef.current?.destroy({
dom: renderDom.current,
});
Expand Down
1 change: 0 additions & 1 deletion packages/bridge/bridge-react/src/router-v5.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, { useContext } from 'react';
// The upper alias react-router-dom$ into this file avoids the loop
// @ts-ignore
import * as ReactRouterDom from 'react-router-dom/index.js';

import { RouterContext } from './context';
import { LoggerInstance } from './utils';

Expand Down
2 changes: 1 addition & 1 deletion packages/bridge/bridge-react/src/router-v6.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ function WraperRouterProvider(
return <RouterProvider router={MemeoryRouterInstance} />;
} else {
const BrowserRouterInstance = createBrowserRouter(routers, {
basename: routerContextProps.basename,
basename: routerContextProps.basename || router?.basename,
future: router.future,
window: router.window,
});
Expand Down
2 changes: 1 addition & 1 deletion packages/bridge/bridge-react/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function WrapperRouterProvider(
return <RouterProvider router={MemeoryRouterInstance} />;
} else {
const BrowserRouterInstance = createBrowserRouter(routers, {
basename: routerContextProps.basename,
basename: routerContextProps.basename || router?.basename,
future: router.future,
window: router.window,
});
Expand Down
3 changes: 2 additions & 1 deletion packages/bridge/vue3-bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
},
"peerDependencies": {
"vue": "=3",
"vue-router": "=3"
"vue-router": "=3",
"@module-federation/runtime": "workspace:*"
},
"dependencies": {
"@module-federation/bridge-shared": "workspace:*"
Expand Down
2 changes: 1 addition & 1 deletion packages/bridge/vue3-bridge/src/create.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineAsyncComponent, h } from 'vue';
import { useRoute } from 'vue-router';
import RemoteApp from './remoteApp.jsx';
import { LoggerInstance } from './utils.js';
import { useRoute } from 'vue-router';

declare const __APP_VERSION__: string;

Expand Down
29 changes: 29 additions & 0 deletions packages/bridge/vue3-bridge/src/lifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { getInstance } from '@module-federation/runtime';
import helper from '@module-federation/runtime/helpers';

function registerBridgeLifeCycle() {
const { registerPlugins, pluginHelper } = helper.global;
const host = getInstance();
const pluginSystem = new pluginHelper.PluginSystem({
beforeBridgeRender: new pluginHelper.SyncHook<
[Record<string, any>],
void
>(),
beforeBridgeDestroy: new pluginHelper.SyncHook<
[Record<string, any>],
void
>(),
});

if (host) {
registerPlugins<typeof pluginSystem.lifecycle, typeof pluginSystem>(
host?.options?.plugins,
[pluginSystem],
);
return pluginSystem;
}

return null;
}

export { registerBridgeLifeCycle };
2 changes: 2 additions & 0 deletions packages/bridge/vue3-bridge/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function createBridgeComponent(bridgeInfo: any) {
LoggerInstance.log(`createBridgeComponent render Info`, info);
const app = Vue.createApp(bridgeInfo.rootComponent);
rootMap.set(info.dom, app);
bridgeInfo?.renderLifecycle?.(info);
const appOptions = bridgeInfo.appOptions({
basename: info.basename,
memoryRoute: info.memoryRoute,
Expand Down Expand Up @@ -46,6 +47,7 @@ export function createBridgeComponent(bridgeInfo: any) {
destroy(info: { dom: HTMLElement }) {
LoggerInstance.log(`createBridgeComponent destroy Info`, info);
const root = rootMap.get(info?.dom);
bridgeInfo?.destroyLifecycle?.(info);
root?.unmount();
},
};
Expand Down
Loading
Loading