From e38e48d1a5e0a7d22eb46843b0763469f3572a98 Mon Sep 17 00:00:00 2001 From: Zhou xiao Date: Wed, 10 Jul 2024 11:07:57 +0800 Subject: [PATCH] fix(react-bridge): optimize bridge router pathname (#2696) --- .../router-remote1-2001/rsbuild.config.ts | 12 +- packages/bridge/bridge-react/src/create.tsx | 242 +++++------------- .../bridge/bridge-react/src/remote/index.tsx | 165 ++++++++++++ 3 files changed, 233 insertions(+), 186 deletions(-) create mode 100644 packages/bridge/bridge-react/src/remote/index.tsx diff --git a/apps/router-demo/router-remote1-2001/rsbuild.config.ts b/apps/router-demo/router-remote1-2001/rsbuild.config.ts index 0d8b0b97940..8a1c82c37b7 100644 --- a/apps/router-demo/router-remote1-2001/rsbuild.config.ts +++ b/apps/router-demo/router-remote1-2001/rsbuild.config.ts @@ -38,12 +38,12 @@ export default defineConfig({ './export-app': './src/export-App.tsx', }, shared: { - react: { - singleton: true, - }, - 'react-dom': { - singleton: true, - }, + // react: { + // singleton: true, + // }, + // 'react-dom': { + // singleton: true, + // }, // 'react-router-dom': { // singleton: true, // }, diff --git a/packages/bridge/bridge-react/src/create.tsx b/packages/bridge/bridge-react/src/create.tsx index 5d0e9fcabc7..20bdefc2ebd 100644 --- a/packages/bridge/bridge-react/src/create.tsx +++ b/packages/bridge/bridge-react/src/create.tsx @@ -1,14 +1,11 @@ -import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; -import * as ReactRouterDOM from 'react-router-dom'; +import React, { forwardRef } from 'react'; import type { ProviderParams } from '@module-federation/bridge-shared'; -import { LoggerInstance, pathJoin } from './utils'; -import { dispatchPopstateEnv } from '@module-federation/bridge-shared'; +import { LoggerInstance } from './utils'; import { ErrorBoundary, ErrorBoundaryPropsWithComponent, } from 'react-error-boundary'; - -declare const __APP_VERSION__: string; +import RemoteApp from './remote'; export interface RenderFnParams extends ProviderParams { dom?: any; @@ -25,79 +22,67 @@ interface RemoteModule { }; } -interface RemoteAppParams { - name: string; - providerInfo: NonNullable; - dispathPopstate: boolean; -} - -const RemoteApp = ({ - name, - memoryRoute, - basename, - providerInfo, - dispathPopstate, - ...resProps -}: RemoteAppParams & ProviderParams) => { - const rootRef = useRef(null); - const renderDom = useRef(null); - const providerInfoRef = useRef(null); - if (dispathPopstate) { - const location = ReactRouterDOM.useLocation(); - const [pathname, setPathname] = useState(location.pathname); - - useEffect(() => { - if (pathname !== '' && pathname !== location.pathname) { - LoggerInstance.log(`createRemoteComponent dispatchPopstateEnv >>>`, { - name, - pathname: location.pathname, - }); - dispatchPopstateEnv(); - } - setPathname(location.pathname); - }, [location]); - } - - useEffect(() => { - const renderTimeout = setTimeout(() => { - const providerReturn = providerInfo(); - providerInfoRef.current = providerReturn; - const renderProps = { - name, - dom: rootRef.current, - basename, - memoryRoute, - ...resProps, - }; - renderDom.current = rootRef.current; +function createLazyRemoteComponent(info: { + loader: () => Promise; + loading: React.ReactNode; + fallback: ErrorBoundaryPropsWithComponent['FallbackComponent']; + export?: E; +}) { + const exportName = info?.export || 'default'; + return React.lazy(async () => { + LoggerInstance.log(`createRemoteComponent LazyComponent create >>>`, { + lazyComponent: info.loader, + exportName, + }); + try { + const m = (await info.loader()) as RemoteModule; + // @ts-ignore + const moduleName = m && m[Symbol.for('mf_module_id')]; LoggerInstance.log( - `createRemoteComponent LazyComponent render >>>`, - renderProps, + `createRemoteComponent LazyComponent loadRemote info >>>`, + { name: moduleName, module: m, exportName }, ); - providerReturn.render(renderProps); - }); - return () => { - clearTimeout(renderTimeout); - setTimeout(() => { - if (providerInfoRef.current?.destroy) { - LoggerInstance.log( - `createRemoteComponent LazyComponent destroy >>>`, - { name, basename, dom: renderDom.current }, - ); - providerInfoRef.current?.destroy({ - dom: renderDom.current, - }); - } - }); - }; - }, []); + // @ts-ignore + const exportFn = m[exportName] as any; - //@ts-ignore - return
; -}; + if (exportName in m && typeof exportFn === 'function') { + const RemoteAppComponent = forwardRef< + HTMLDivElement, + { + basename?: ProviderParams['basename']; + memoryRoute?: ProviderParams['memoryRoute']; + } + >((props, _ref) => { + return ( + + ); + }); -(RemoteApp as any)['__APP_VERSION__'] = __APP_VERSION__; + return { + default: RemoteAppComponent, + }; + } else { + LoggerInstance.log( + `createRemoteComponent LazyComponent module not found >>>`, + { name: moduleName, module: m, exportName }, + ); + throw Error( + `Make sure that ${moduleName} has the correct export when export is ${String( + exportName, + )}`, + ); + } + } catch (error) { + throw error; + } + }); +} export function createRemoteComponent(info: { loader: () => Promise; @@ -114,121 +99,18 @@ export function createRemoteComponent(info: { : {} : {}; + const LazyComponent = createLazyRemoteComponent(info); + return ( props: { basename?: ProviderParams['basename']; memoryRoute?: ProviderParams['memoryRoute']; } & RawComponentType, ) => { - const exportName = info?.export || 'default'; - let basename = '/'; - let enableDispathPopstate = false; - let routerContextVal: any; - try { - ReactRouterDOM.useLocation(); - enableDispathPopstate = true; - } catch { - enableDispathPopstate = false; - } - - if (props.basename) { - basename = props.basename; - } else if (enableDispathPopstate) { - const ReactRouterDOMAny: any = ReactRouterDOM; - // Avoid building tools checking references - const useRouteMatch = ReactRouterDOMAny['use' + 'RouteMatch']; //v5 - const useHistory = ReactRouterDOMAny['use' + 'History']; //v5 - const useHref = ReactRouterDOMAny['use' + 'Href']; - const UNSAFE_RouteContext = ReactRouterDOMAny['UNSAFE_' + 'RouteContext']; - - if (UNSAFE_RouteContext /* react-router@6 */) { - if (useHref) { - basename = useHref?.('/'); - } - routerContextVal = useContext(UNSAFE_RouteContext); - if ( - routerContextVal && - routerContextVal.matches && - routerContextVal.matches[0] && - routerContextVal.matches[0].pathnameBase - ) { - basename = pathJoin( - basename, - routerContextVal.matches[0].pathnameBase || '/', - ); - } - } /* react-router@5 */ else { - const match = useRouteMatch?.(); // v5 - if (useHistory /* react-router@5 */) { - // there is no dynamic switching of the router version in the project - // so hooks can be used in conditional judgment - const history = useHistory?.(); - // To be compatible to history@4.10.1 and @5.3.0 we cannot write like this `history.createHref(pathname)` - basename = history?.createHref?.({ pathname: '/' }); - } - if (match /* react-router@5 */) { - basename = pathJoin(basename, match?.path || '/'); - } - } - } - - const LazyComponent = useMemo(() => { - //@ts-ignore - return React.lazy(async () => { - LoggerInstance.log(`createRemoteComponent LazyComponent create >>>`, { - basename, - lazyComponent: info.loader, - exportName, - props, - routerContextVal, - }); - try { - const m = (await info.loader()) as RemoteModule; - // @ts-ignore - const moduleName = m && m[Symbol.for('mf_module_id')]; - LoggerInstance.log( - `createRemoteComponent LazyComponent loadRemote info >>>`, - { basename, name: moduleName, module: m, exportName, props }, - ); - - // @ts-ignore - const exportFn = m[exportName] as any; - - if (exportName in m && typeof exportFn === 'function') { - return { - default: () => ( - - ), - }; - } else { - LoggerInstance.log( - `createRemoteComponent LazyComponent module not found >>>`, - { basename, name: moduleName, module: m, exportName, props }, - ); - throw Error( - `Make sure that ${moduleName} has the correct export when export is ${String( - exportName, - )}`, - ); - } - } catch (error) { - throw error; - } - }); - }, [exportName, basename, props.memoryRoute]); - - //@ts-ignore return ( - + ); diff --git a/packages/bridge/bridge-react/src/remote/index.tsx b/packages/bridge/bridge-react/src/remote/index.tsx new file mode 100644 index 00000000000..3b66871b627 --- /dev/null +++ b/packages/bridge/bridge-react/src/remote/index.tsx @@ -0,0 +1,165 @@ +import React, { useContext, useEffect, useRef, useState } 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'; + +declare const __APP_VERSION__: string; + +export interface RenderFnParams extends ProviderParams { + dom?: any; +} + +interface RemoteModule { + provider: () => { + render: ( + info: ProviderParams & { + dom: any; + }, + ) => void; + destroy: (info: { dom: any }) => void; + }; +} + +interface RemoteAppParams { + name: string; + providerInfo: NonNullable; + exportName: string | number | symbol; +} + +const RemoteApp = ({ + name, + memoryRoute, + basename, + providerInfo, + ...resProps +}: RemoteAppParams & ProviderParams) => { + const rootRef = useRef(null); + const renderDom = useRef(null); + const providerInfoRef = useRef(null); + + useEffect(() => { + const renderTimeout = setTimeout(() => { + const providerReturn = providerInfo(); + providerInfoRef.current = providerReturn; + const renderProps = { + name, + dom: rootRef.current, + basename, + memoryRoute, + ...resProps, + }; + renderDom.current = rootRef.current; + LoggerInstance.log( + `createRemoteComponent LazyComponent render >>>`, + renderProps, + ); + providerReturn.render(renderProps); + }); + + return () => { + clearTimeout(renderTimeout); + setTimeout(() => { + if (providerInfoRef.current?.destroy) { + LoggerInstance.log( + `createRemoteComponent LazyComponent destroy >>>`, + { name, basename, dom: renderDom.current }, + ); + providerInfoRef.current?.destroy({ + dom: renderDom.current, + }); + } + }); + }; + }, []); + + //@ts-ignore + return
; +}; + +(RemoteApp as any)['__APP_VERSION__'] = __APP_VERSION__; + +interface ExtraDataProps { + basename?: string; +} + +export function withRouterData

[0]>( + WrappedComponent: React.ComponentType

, +): React.FC> { + return (props: any) => { + let enableDispathPopstate = false; + let routerContextVal: any; + try { + ReactRouterDOM.useLocation(); + enableDispathPopstate = true; + } catch { + enableDispathPopstate = false; + } + let basename = '/'; + + if (!props.basename && enableDispathPopstate) { + const ReactRouterDOMAny: any = ReactRouterDOM; + // Avoid building tools checking references + const useRouteMatch = ReactRouterDOMAny['use' + 'RouteMatch']; //v5 + const useHistory = ReactRouterDOMAny['use' + 'History']; //v5 + const useHref = ReactRouterDOMAny['use' + 'Href']; + const UNSAFE_RouteContext = ReactRouterDOMAny['UNSAFE_' + 'RouteContext']; + + if (UNSAFE_RouteContext /* react-router@6 */) { + if (useHref) { + basename = useHref?.('/'); + } + routerContextVal = useContext(UNSAFE_RouteContext); + if ( + routerContextVal && + routerContextVal.matches && + routerContextVal.matches.length > 0 + ) { + const matchIndex = routerContextVal.matches.length - 1; + const pathnameBase = + routerContextVal.matches[matchIndex].pathnameBase; + basename = pathJoin(basename, pathnameBase || '/'); + } + } /* react-router@5 */ else { + const match = useRouteMatch?.(); // v5 + if (useHistory /* react-router@5 */) { + // there is no dynamic switching of the router version in the project + // so hooks can be used in conditional judgment + const history = useHistory?.(); + // To be compatible to history@4.10.1 and @5.3.0 we cannot write like this `history.createHref(pathname)` + basename = history?.createHref?.({ pathname: '/' }); + } + if (match /* react-router@5 */) { + basename = pathJoin(basename, match?.path || '/'); + } + } + } + + LoggerInstance.log(`createRemoteComponent withRouterData >>>`, { + ...props, + basename, + routerContextVal, + enableDispathPopstate, + }); + + if (enableDispathPopstate) { + const location = ReactRouterDOM.useLocation(); + const [pathname, setPathname] = useState(location.pathname); + + useEffect(() => { + if (pathname !== '' && pathname !== location.pathname) { + LoggerInstance.log(`createRemoteComponent dispatchPopstateEnv >>>`, { + name: props.name, + pathname: location.pathname, + }); + dispatchPopstateEnv(); + } + setPathname(location.pathname); + }, [location]); + } + + return ; + }; +} + +export default withRouterData(RemoteApp);