diff --git a/apps/website-new/docs/en/guide/framework/modernjs.mdx b/apps/website-new/docs/en/guide/framework/modernjs.mdx index af969eb0d18..021c94caacb 100644 --- a/apps/website-new/docs/en/guide/framework/modernjs.mdx +++ b/apps/website-new/docs/en/guide/framework/modernjs.mdx @@ -91,7 +91,7 @@ The Modern.js plugin re-exports `@module-federation/bridge-react` from `@module- ### createRemoteComponent Deprecated ::: danger -This API has been deprecated. Please use [createLazyComponent](/practice/bridge/react-bridge/load-component.html#createlazycomponent-vs-createremoteappcomponent) instead. +This API has been deprecated. Please use [createLazyComponent](/practice/bridge/react-bridge/load-component.html#what-is-createlazycomponent) instead. ::: #### Migration Guide diff --git a/apps/website-new/docs/en/practice/bridge/react-bridge/load-app.mdx b/apps/website-new/docs/en/practice/bridge/react-bridge/load-app.mdx index 1bb921d7db8..007b8182522 100644 --- a/apps/website-new/docs/en/practice/bridge/react-bridge/load-app.mdx +++ b/apps/website-new/docs/en/practice/bridge/react-bridge/load-app.mdx @@ -310,4 +310,112 @@ interface RemoteComponentProps> { loader: () => loadRemote('remote1/export-app'), export: 'dashboard' }) - ``` \ No newline at end of file + ``` + +## Bundle Size Optimization + +### React Router Dependency Explanation + +By default, `@module-federation/bridge-react` includes `react-router-dom` in your bundle to provide the following out-of-the-box capabilities: + +- ✅ Automatic basename injection - No manual route base path configuration needed +- ✅ Router context passing - Automatic React Router context handling +- ✅ Nested routing support - Complete router integration capabilities + +**However**, if your project meets any of these conditions: +- Doesn't need routing functionality (pure component loading) +- Uses a non-react-router routing framework (e.g., TanStack Router) +- Wants to minimize bundle size + +**We recommend** disabling the `enableBridgeRouter` configuration to turn off this capability, which will: +- ✅ Reduce bundle size by ~3KB (gzipped) +- ✅ Avoid unnecessary dependency injection +- ✅ Eliminate potential version conflict risks + +### How to Disable Router Dependency + +You can control whether to include router support through the `bridge.enableBridgeRouter` configuration: + +```ts title="rsbuild.config.ts" +import { pluginModuleFederation } from '@module-federation/rsbuild-plugin'; + +export default { + plugins: [ + pluginModuleFederation({ + name: 'host-app', + remotes: { + remote1: 'remote1@http://localhost:3001/mf-manifest.json', + }, + bridge: { + // Disable router support to reduce bundle size + enableBridgeRouter: false, + }, + }), + ], +}; +``` + +:::tip Configuration Behavior +- **`enableBridgeRouter: false`**: Automatically aliases to `/base` entry point (no react-router-dom code) +- **`enableBridgeRouter: true`** or **`undefined`**: Includes router support (default behavior) +::: + +### When to Disable Router? + +**Disable router** (`enableBridgeRouter: false`) when: +- ✅ Your application doesn't use react-router +- ✅ You want to minimize bundle size +- ✅ You can manually manage basename if needed + +**Keep router enabled** (default) when: +- ✅ Your application uses react-router +- ✅ You need automatic basename injection +- ✅ You need routing context integration + +### Migration Example + +#### Before: With Router (Default) +```tsx +import { createRemoteAppComponent } from '@module-federation/bridge-react'; + +const RemoteApp = createRemoteAppComponent({ + loader: () => loadRemote('remote1/app'), + loading:
Loading...
, + fallback: ErrorBoundary, +}); + +// basename automatically retrieved from router context + +``` + +#### After: Without Router (Optimized) +```ts title="rsbuild.config.ts" +// Configuration +pluginModuleFederation({ + bridge: { + enableBridgeRouter: false, // Disable router + }, +}) +``` + +```tsx +import { createRemoteAppComponent } from '@module-federation/bridge-react'; + +const RemoteApp = createRemoteAppComponent({ + loader: () => loadRemote('remote1/app'), + loading:
Loading...
, + fallback: ErrorBoundary, +}); + +// No changes needed! The plugin automatically aliases to /base entry + // Manually pass basename if needed +``` + +:::info How It Works +When `enableBridgeRouter: false`, the Module Federation plugin automatically sets up a webpack alias: +``` +'@module-federation/bridge-react' → '@module-federation/bridge-react/base' +``` + +This means your imports automatically resolve to the router-free version without changing any code! +::: diff --git a/apps/website-new/docs/zh/guide/framework/modernjs.mdx b/apps/website-new/docs/zh/guide/framework/modernjs.mdx index 338da794e66..18c29e4fd74 100644 --- a/apps/website-new/docs/zh/guide/framework/modernjs.mdx +++ b/apps/website-new/docs/zh/guide/framework/modernjs.mdx @@ -127,7 +127,7 @@ export default App; ### createRemoteSSRComponent 废弃 ::: danger -此 API 已被废弃,请使用[createLazyComponent](../../practice/bridge/react-bridge#createlazycomponent) 。 +此 API 已被废弃,请使用[createLazyComponent](/practice/bridge/react-bridge/load-component.html#什么是-createlazycomponent) 。 ::: #### 如何迁移 diff --git a/apps/website-new/docs/zh/practice/bridge/react-bridge.mdx b/apps/website-new/docs/zh/practice/bridge/react-bridge.mdx deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/apps/website-new/docs/zh/practice/bridge/react-bridge/load-app.mdx b/apps/website-new/docs/zh/practice/bridge/react-bridge/load-app.mdx index 08b8a5c05cd..f8d021cc3a7 100644 --- a/apps/website-new/docs/zh/practice/bridge/react-bridge/load-app.mdx +++ b/apps/website-new/docs/zh/practice/bridge/react-bridge/load-app.mdx @@ -183,7 +183,7 @@ function createRemoteAppComponent, E extends keyof T > ``` -### RemoteComponentParams\ +#### RemoteComponentParams\ 配置参数接口: @@ -206,7 +206,7 @@ interface RemoteComponentParams, E extends keyof T = } ``` -### RemoteComponentProps\ +#### RemoteComponentProps\ 返回组件的属性接口: @@ -241,7 +241,7 @@ interface RemoteComponentProps> { ### 参数详解 -### loader +#### loader - **类型**: `() => Promise` - **必需**: 是 @@ -252,7 +252,7 @@ interface RemoteComponentProps> { loader: () => import('remote1/export-app') ``` -### loading +#### loading - **类型**: `React.ReactNode` - **必需**: 是 @@ -264,7 +264,7 @@ interface RemoteComponentProps> { loading: ``` -### fallback +#### fallback - **类型**: `React.ComponentType<{ error: Error }>` - **必需**: 是 @@ -275,7 +275,7 @@ interface RemoteComponentProps> { fallback: ErrorBoundaryComponent ``` -### export +#### export - **类型**: `E extends keyof T` (泛型约束,通常是 `string`) - **必需**: 否 @@ -310,4 +310,114 @@ interface RemoteComponentProps> { loader: () => loadRemote('remote1/export-app'), export: 'dashboard' }) - ``` \ No newline at end of file + ``` + + +## Bundle 体积优化 + +### React Router 依赖说明 + +默认情况下,`@module-federation/bridge-react` 会将 `react-router-dom` 打包到你的 bundle 中,这是为了提供以下开箱即用的能力: + +- ✅ 自动 basename 注入 - 无需手动配置路由基础路径 +- ✅ 路由上下文传递 - 自动处理 React Router 上下文 +- ✅ 嵌套路由支持 - 完整的路由集成能力 + +**但是**,如果你的项目满足以下任一条件: +- 不需要路由功能(纯组件加载) +- 使用非 react-router 的路由框架(如 TanStack Router) +- 希望最小化 bundle 体积 + +**建议关闭** `enableBridgeRouter` 配置来禁用此能力,这将: +- ✅ 减小 bundle 体积约 3KB (gzipped) +- ✅ 避免不必要的依赖注入 +- ✅ 消除潜在的版本冲突风险 + +### 如何禁用 Router 依赖 + +你可以通过 `bridge.enableBridgeRouter` 配置来控制是否包含路由支持: + +```ts title="rsbuild.config.ts" +import { pluginModuleFederation } from '@module-federation/rsbuild-plugin'; + +export default { + plugins: [ + pluginModuleFederation({ + name: 'host-app', + remotes: { + remote1: 'remote1@http://localhost:3001/mf-manifest.json', + }, + bridge: { + // 禁用路由支持以减小 bundle 体积 + enableBridgeRouter: false, + }, + }), + ], +}; +``` + +:::tip 配置行为 +- **`enableBridgeRouter: false`**: 自动 alias 到 `/base` 入口(不包含 react-router-dom 代码) +- **`enableBridgeRouter: true`** 或 **`undefined`**: 包含路由支持(默认行为) + +::: + +### 何时禁用 Router? + +**禁用 router** (`enableBridgeRouter: false`) 适用于: +- ✅ 应用不使用 react-router +- ✅ 想要最小化 bundle 体积 +- ✅ 可以手动管理 basename(如果需要) + +**保持 router 启用**(默认)适用于: +- ✅ 应用使用 react-router +- ✅ 需要自动 basename 注入 +- ✅ 需要路由上下文集成 + +### 迁移示例 + +#### 迁移前:启用 Router(默认) +```tsx +import { createRemoteAppComponent } from '@module-federation/bridge-react'; + +const RemoteApp = createRemoteAppComponent({ + loader: () => loadRemote('remote1/app'), + loading:
Loading...
, + fallback: ErrorBoundary, +}); + +// basename 自动从路由上下文获取 + +``` + +#### 迁移后:禁用 Router(优化) +```ts title="rsbuild.config.ts" +// 配置 +pluginModuleFederation({ + bridge: { + enableBridgeRouter: false, // 禁用 router + }, +}) +``` + +```tsx +import { createRemoteAppComponent } from '@module-federation/bridge-react'; + +const RemoteApp = createRemoteAppComponent({ + loader: () => loadRemote('remote1/app'), + loading:
Loading...
, + fallback: ErrorBoundary, +}); + +// 无需修改代码!插件会自动 alias 到 /base 入口 + // 如果需要,手动传递 basename +``` + +:::info 工作原理 +当设置 `enableBridgeRouter: false` 时,Module Federation 插件会自动设置 webpack alias: +``` +'@module-federation/bridge-react' → '@module-federation/bridge-react/base' +``` + +这意味着你的导入会自动解析到无路由版本,无需修改任何代码! +::: diff --git a/packages/bridge/bridge-react-webpack-plugin/src/index.ts b/packages/bridge/bridge-react-webpack-plugin/src/index.ts index fdc850c894b..345f888ec65 100644 --- a/packages/bridge/bridge-react-webpack-plugin/src/index.ts +++ b/packages/bridge/bridge-react-webpack-plugin/src/index.ts @@ -41,8 +41,8 @@ class ReactBridgeAliasChangerPlugin { const originalResolve = compiler.options.resolve || {}; const originalAlias = originalResolve.alias || {}; - // Update alias - const updatedAlias = { + // Update alias - set up router version alias + const updatedAlias: Record = { // allow `alias` can be override // [this.alias]: targetFilePath, ...getBridgeRouterAlias(originalAlias['react-router-dom']), diff --git a/packages/bridge/bridge-react/package.json b/packages/bridge/bridge-react/package.json index 6b1e887edc9..a9cc4ec3f55 100644 --- a/packages/bridge/bridge-react/package.json +++ b/packages/bridge/bridge-react/package.json @@ -21,6 +21,11 @@ "import": "./dist/index.es.js", "require": "./dist/index.cjs.js" }, + "./base": { + "types": "./dist/base.d.ts", + "import": "./dist/base.es.js", + "require": "./dist/base.cjs.js" + }, "./v18": { "types": "./dist/v18.d.ts", "import": "./dist/v18.es.js", @@ -83,6 +88,9 @@ ".": [ "./dist/index.d.ts" ], + "base": [ + "./dist/base.d.ts" + ], "v18": [ "./dist/v18.d.ts" ], diff --git a/packages/bridge/bridge-react/src/base.ts b/packages/bridge/bridge-react/src/base.ts new file mode 100644 index 00000000000..11e4aa58ff5 --- /dev/null +++ b/packages/bridge/bridge-react/src/base.ts @@ -0,0 +1,50 @@ +export { createBridgeComponent } from './provider/versions/legacy'; + +// Export router-free versions of remote component creators +export { + createRemoteComponent, + createRemoteAppComponent, +} from './remote/base-component'; +export type { LazyRemoteComponentInfo } from './remote/base-component'; + +// Export all lazy loading and data fetching utilities (no router dependencies) +export { + ERROR_TYPE, + createLazyComponent, + collectSSRAssets, + callDataFetch, + setSSREnv, + autoFetchDataPlugin, + CacheSize, + CacheTime, + configureCache, + generateKey, + cache, + revalidateTag, + clearStore, + prefetch, +} from './lazy'; + +export { lazyLoadComponentPlugin } from './plugins/lazy-load-component-plugin'; + +// Export all types +export type { CreateRootOptions, Root } from './provider/versions/legacy'; +export type { + ProviderParams, + ProviderFnParams, + RootType, + DestroyParams, + RenderParams, + RemoteComponentParams, + RenderFnParams, + RemoteComponentProps, + RemoteModule, +} from './types'; +export type { + DataFetchParams, + NoSSRRemoteInfo, + CollectSSRAssetsOptions, + CreateLazyComponentOptions, + CacheStatus, + CacheStatsInfo, +} from './lazy'; diff --git a/packages/bridge/bridge-react/src/index.ts b/packages/bridge/bridge-react/src/index.ts index 0c30c931c77..0899925815b 100644 --- a/packages/bridge/bridge-react/src/index.ts +++ b/packages/bridge/bridge-react/src/index.ts @@ -6,8 +6,8 @@ export { createBridgeComponent } from './provider/versions/legacy'; export { createRemoteComponent, createRemoteAppComponent, -} from './remote/create'; -export type { LazyRemoteComponentInfo } from './remote/create'; +} from './remote/router-component'; +export type { LazyRemoteComponentInfo } from './remote/router-component'; export { ERROR_TYPE, createLazyComponent, diff --git a/packages/bridge/bridge-react/src/remote/RemoteAppWrapper.tsx b/packages/bridge/bridge-react/src/remote/RemoteAppWrapper.tsx new file mode 100644 index 00000000000..44b6887f43c --- /dev/null +++ b/packages/bridge/bridge-react/src/remote/RemoteAppWrapper.tsx @@ -0,0 +1,108 @@ +/** + * Shared RemoteAppWrapper component used by both base and router versions + * This component handles the lifecycle of remote Module Federation apps + */ +import React, { useEffect, useRef, useState, forwardRef } from 'react'; +import { LoggerInstance, getRootDomDefaultClassName } from '../utils'; +import { federationRuntime } from '../provider/plugin'; +import { RemoteComponentProps, RemoteAppParams } from '../types'; + +export const RemoteAppWrapper = forwardRef(function ( + props: RemoteAppParams & RemoteComponentProps, + ref, +) { + const { + moduleName, + memoryRoute, + basename, + providerInfo, + className, + style, + fallback, + loading, + ...resProps + } = props; + + const instance = federationRuntime.instance; + const rootRef: React.MutableRefObject = + ref && 'current' in ref + ? (ref as React.MutableRefObject) + : useRef(null); + + const renderDom: React.MutableRefObject = useRef(null); + const providerInfoRef = useRef(null); + const [initialized, setInitialized] = useState(false); + + LoggerInstance.debug(`RemoteAppWrapper instance from props >>>`, instance); + + // 初始化远程组件 + useEffect(() => { + if (initialized) return; + const providerReturn = providerInfo(); + providerInfoRef.current = providerReturn; + setInitialized(true); + + return () => { + if (providerInfoRef.current?.destroy) { + LoggerInstance.debug( + `createRemoteAppComponent LazyComponent destroy >>>`, + { moduleName, basename, dom: renderDom.current }, + ); + + instance?.bridgeHook?.lifecycle?.beforeBridgeDestroy?.emit({ + moduleName, + dom: renderDom.current, + basename, + memoryRoute, + fallback, + ...resProps, + }); + + providerInfoRef.current?.destroy({ + moduleName, + dom: renderDom.current, + }); + + instance?.bridgeHook?.lifecycle?.afterBridgeDestroy?.emit({ + moduleName, + dom: renderDom.current, + basename, + memoryRoute, + fallback, + ...resProps, + }); + } + }; + }, [moduleName]); + + // trigger render after props updated + useEffect(() => { + if (!initialized || !providerInfoRef.current) return; + + let renderProps = { + moduleName, + dom: rootRef.current, + basename, + memoryRoute, + fallback, + ...resProps, + }; + renderDom.current = rootRef.current; + + const beforeBridgeRenderRes = + instance?.bridgeHook?.lifecycle?.beforeBridgeRender?.emit(renderProps) || + {}; + // @ts-ignore + renderProps = { ...renderProps, ...beforeBridgeRenderRes.extraProps }; + providerInfoRef.current.render(renderProps); + instance?.bridgeHook?.lifecycle?.afterBridgeRender?.emit(renderProps); + }, [initialized, ...Object.values(props)]); + + // bridge-remote-root + const rootComponentClassName = `${getRootDomDefaultClassName(moduleName)} ${className || ''}`; + return ( +
+ {loading} +
+ ); +}); diff --git a/packages/bridge/bridge-react/src/remote/base-component/component.tsx b/packages/bridge/bridge-react/src/remote/base-component/component.tsx new file mode 100644 index 00000000000..2055b0d597b --- /dev/null +++ b/packages/bridge/bridge-react/src/remote/base-component/component.tsx @@ -0,0 +1,2 @@ +import { RemoteAppWrapper } from '../RemoteAppWrapper'; +export default RemoteAppWrapper; diff --git a/packages/bridge/bridge-react/src/remote/base-component/create.tsx b/packages/bridge/bridge-react/src/remote/base-component/create.tsx new file mode 100644 index 00000000000..9c3493dddee --- /dev/null +++ b/packages/bridge/bridge-react/src/remote/base-component/create.tsx @@ -0,0 +1,23 @@ +import RemoteApp from './component'; +import { + createLazyRemoteComponentFactory, + createRemoteAppComponentFactory, + createDeprecatedRemoteComponentFactory, + type LazyRemoteComponentInfo, +} from '../createHelpers'; + +export type { LazyRemoteComponentInfo }; + +const createLazyRemoteComponent = createLazyRemoteComponentFactory(RemoteApp); + +export const createRemoteAppComponent = + createRemoteAppComponentFactory(RemoteApp); + +/** + * @deprecated createRemoteComponent is deprecated, please use createRemoteAppComponent instead! + */ +export const createRemoteComponent = createDeprecatedRemoteComponentFactory( + createRemoteAppComponent, +); + +export { createLazyRemoteComponent }; diff --git a/packages/bridge/bridge-react/src/remote/base-component/index.tsx b/packages/bridge/bridge-react/src/remote/base-component/index.tsx new file mode 100644 index 00000000000..470fe8b25f1 --- /dev/null +++ b/packages/bridge/bridge-react/src/remote/base-component/index.tsx @@ -0,0 +1,10 @@ +/** + * Base entry point without React Router dependencies + * Re-exports from base/create.tsx + */ +export { + createRemoteAppComponent, + createRemoteComponent, + createLazyRemoteComponent, + type LazyRemoteComponentInfo, +} from './create'; diff --git a/packages/bridge/bridge-react/src/remote/create.tsx b/packages/bridge/bridge-react/src/remote/create.tsx deleted file mode 100644 index 67ab5dd84e7..00000000000 --- a/packages/bridge/bridge-react/src/remote/create.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import React, { forwardRef } from 'react'; -import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; -import { LoggerInstance } from '../utils'; -import RemoteApp from './component'; -import { - RemoteComponentParams, - RemoteComponentProps, - RemoteModule, -} from '../types'; - -export type LazyRemoteComponentInfo< - T, - _E extends keyof T, -> = RemoteComponentParams; - -function createLazyRemoteComponent< - T = Record, - E extends keyof T = keyof T, ->(info: LazyRemoteComponentInfo) { - const exportName = info?.export || 'default'; - return React.lazy(async () => { - LoggerInstance.debug(`createRemoteAppComponent 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.debug( - `createRemoteAppComponent LazyComponent loadRemote info >>>`, - { name: moduleName, module: m, exportName }, - ); - - // @ts-ignore - const exportFn = m[exportName]; - if (exportName in m && typeof exportFn === 'function') { - const RemoteAppComponent = forwardRef< - HTMLDivElement, - RemoteComponentProps - >((props, ref) => { - return ( - - ); - }); - - return { - default: RemoteAppComponent, - }; - } else { - LoggerInstance.debug( - `createRemoteAppComponent 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 createRemoteAppComponent< - T = Record, - E extends keyof T = keyof T, ->(info: LazyRemoteComponentInfo) { - const LazyComponent = createLazyRemoteComponent(info); - return forwardRef((props, ref) => { - return ( - } - > - - - - - ); - }); -} - -/** - * @deprecated createRemoteAppComponent is deprecated, please use createRemoteAppComponent instead! - */ -export function createRemoteComponent< - T = Record, - E extends keyof T = keyof T, ->(info: LazyRemoteComponentInfo) { - LoggerInstance.warn( - `createRemoteComponent is deprecated, please use createRemoteAppComponent instead!`, - ); - return createRemoteAppComponent(info); -} diff --git a/packages/bridge/bridge-react/src/remote/createHelpers.tsx b/packages/bridge/bridge-react/src/remote/createHelpers.tsx new file mode 100644 index 00000000000..068bcee52b7 --- /dev/null +++ b/packages/bridge/bridge-react/src/remote/createHelpers.tsx @@ -0,0 +1,130 @@ +import React, { forwardRef } from 'react'; +import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; +import { LoggerInstance } from '../utils'; +import { + RemoteComponentParams, + RemoteComponentProps, + RemoteModule, +} from '../types'; + +export type LazyRemoteComponentInfo< + T, + _E extends keyof T, +> = RemoteComponentParams; + +/** + * Creates a factory function for creating lazy remote components + * @param RemoteApp The RemoteAppWrapper component to use (with or without router) + */ +export function createLazyRemoteComponentFactory( + RemoteApp: React.ComponentType, +) { + return function createLazyRemoteComponent< + T = Record, + E extends keyof T = keyof T, + >(info: LazyRemoteComponentInfo) { + const exportName = info?.export || 'default'; + return React.lazy(async () => { + LoggerInstance.debug( + `createRemoteAppComponent 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.debug( + `createRemoteAppComponent LazyComponent loadRemote info >>>`, + { name: moduleName, module: m, exportName }, + ); + + // @ts-ignore + const exportFn = m[exportName]; + if (exportName in m && typeof exportFn === 'function') { + const RemoteAppComponent = forwardRef< + HTMLDivElement, + RemoteComponentProps + >((props, ref) => { + return ( + + ); + }); + + return { + default: RemoteAppComponent, + }; + } else { + LoggerInstance.debug( + `createRemoteAppComponent 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; + } + }); + }; +} + +/** + * Creates a factory function for creating remote app components + * @param RemoteApp The RemoteAppWrapper component to use (with or without router) + */ +export function createRemoteAppComponentFactory( + RemoteApp: React.ComponentType, +) { + const createLazyRemoteComponent = createLazyRemoteComponentFactory(RemoteApp); + + return function createRemoteAppComponent< + T = Record, + E extends keyof T = keyof T, + >(info: LazyRemoteComponentInfo) { + const LazyComponent = createLazyRemoteComponent(info); + return forwardRef((props, ref) => { + return ( + + } + > + + + + + ); + }); + }; +} + +/** + * Creates the deprecated createRemoteComponent function + */ +export function createDeprecatedRemoteComponentFactory< + T = Record, + E extends keyof T = keyof T, +>(createFn: (info: LazyRemoteComponentInfo) => any) { + return function createRemoteComponent(info: LazyRemoteComponentInfo) { + LoggerInstance.warn( + `createRemoteComponent is deprecated, please use createRemoteAppComponent instead!`, + ); + return createFn(info); + }; +} diff --git a/packages/bridge/bridge-react/src/remote/component.tsx b/packages/bridge/bridge-react/src/remote/router-component/component.tsx similarity index 53% rename from packages/bridge/bridge-react/src/remote/component.tsx rename to packages/bridge/bridge-react/src/remote/router-component/component.tsx index a7ae62d841d..f499fc3ad20 100644 --- a/packages/bridge/bridge-react/src/remote/component.tsx +++ b/packages/bridge/bridge-react/src/remote/router-component/component.tsx @@ -1,115 +1,8 @@ -import React, { - useContext, - useEffect, - useRef, - useState, - forwardRef, -} from 'react'; +import React, { useContext, useEffect, useState, forwardRef } from 'react'; import * as ReactRouterDOM from 'react-router-dom'; import { dispatchPopstateEnv } from '@module-federation/bridge-shared'; -import { LoggerInstance, pathJoin, getRootDomDefaultClassName } from '../utils'; -import { federationRuntime } from '../provider/plugin'; -import { RemoteComponentProps, RemoteAppParams } from '../types'; - -const RemoteAppWrapper = forwardRef(function ( - props: RemoteAppParams & RemoteComponentProps, - ref, -) { - const { - moduleName, - memoryRoute, - basename, - providerInfo, - className, - style, - fallback, - loading, - ...resProps - } = props; - - const instance = federationRuntime.instance; - const rootRef: React.MutableRefObject = - ref && 'current' in ref - ? (ref as React.MutableRefObject) - : useRef(null); - - const renderDom: React.MutableRefObject = useRef(null); - const providerInfoRef = useRef(null); - const [initialized, setInitialized] = useState(false); - - LoggerInstance.debug(`RemoteAppWrapper instance from props >>>`, instance); - - // 初始化远程组件 - useEffect(() => { - if (initialized) return; - const providerReturn = providerInfo(); - providerInfoRef.current = providerReturn; - setInitialized(true); - - return () => { - if (providerInfoRef.current?.destroy) { - LoggerInstance.debug( - `createRemoteAppComponent LazyComponent destroy >>>`, - { moduleName, basename, dom: renderDom.current }, - ); - - instance?.bridgeHook?.lifecycle?.beforeBridgeDestroy?.emit({ - moduleName, - dom: renderDom.current, - basename, - memoryRoute, - fallback, - ...resProps, - }); - - providerInfoRef.current?.destroy({ - moduleName, - dom: renderDom.current, - }); - - instance?.bridgeHook?.lifecycle?.afterBridgeDestroy?.emit({ - moduleName, - dom: renderDom.current, - basename, - memoryRoute, - fallback, - ...resProps, - }); - } - }; - }, [moduleName]); - - // trigger render after props updated - useEffect(() => { - if (!initialized || !providerInfoRef.current) return; - - let renderProps = { - moduleName, - dom: rootRef.current, - basename, - memoryRoute, - fallback, - ...resProps, - }; - renderDom.current = rootRef.current; - - const beforeBridgeRenderRes = - instance?.bridgeHook?.lifecycle?.beforeBridgeRender?.emit(renderProps) || - {}; - // @ts-ignore - renderProps = { ...renderProps, ...beforeBridgeRenderRes.extraProps }; - providerInfoRef.current.render(renderProps); - instance?.bridgeHook?.lifecycle?.afterBridgeRender?.emit(renderProps); - }, [initialized, ...Object.values(props)]); - - // bridge-remote-root - const rootComponentClassName = `${getRootDomDefaultClassName(moduleName)} ${className || ''}`; - return ( -
- {loading} -
- ); -}); +import { LoggerInstance, pathJoin } from '../../utils'; +import { RemoteAppWrapper } from '../RemoteAppWrapper'; interface ExtraDataProps { basename?: string; diff --git a/packages/bridge/bridge-react/src/remote/router-component/create.tsx b/packages/bridge/bridge-react/src/remote/router-component/create.tsx new file mode 100644 index 00000000000..9c3493dddee --- /dev/null +++ b/packages/bridge/bridge-react/src/remote/router-component/create.tsx @@ -0,0 +1,23 @@ +import RemoteApp from './component'; +import { + createLazyRemoteComponentFactory, + createRemoteAppComponentFactory, + createDeprecatedRemoteComponentFactory, + type LazyRemoteComponentInfo, +} from '../createHelpers'; + +export type { LazyRemoteComponentInfo }; + +const createLazyRemoteComponent = createLazyRemoteComponentFactory(RemoteApp); + +export const createRemoteAppComponent = + createRemoteAppComponentFactory(RemoteApp); + +/** + * @deprecated createRemoteComponent is deprecated, please use createRemoteAppComponent instead! + */ +export const createRemoteComponent = createDeprecatedRemoteComponentFactory( + createRemoteAppComponent, +); + +export { createLazyRemoteComponent }; diff --git a/packages/bridge/bridge-react/src/remote/router-component/index.tsx b/packages/bridge/bridge-react/src/remote/router-component/index.tsx new file mode 100644 index 00000000000..6b197fd3cda --- /dev/null +++ b/packages/bridge/bridge-react/src/remote/router-component/index.tsx @@ -0,0 +1,10 @@ +/** + * Default entry point with React Router support + * Re-exports from router/create.tsx + */ +export { + createRemoteAppComponent, + createRemoteComponent, + createLazyRemoteComponent, + type LazyRemoteComponentInfo, +} from './create'; diff --git a/packages/bridge/bridge-react/vite.config.ts b/packages/bridge/bridge-react/vite.config.ts index be2badcdafd..2e3b5cfc243 100644 --- a/packages/bridge/bridge-react/vite.config.ts +++ b/packages/bridge/bridge-react/vite.config.ts @@ -20,6 +20,7 @@ export default defineConfig({ lib: { entry: { index: path.resolve(__dirname, 'src/index.ts'), + base: path.resolve(__dirname, 'src/base.ts'), plugin: path.resolve(__dirname, 'src/provider/plugin.ts'), router: path.resolve(__dirname, 'src/router/default.tsx'), 'router-v5': path.resolve(__dirname, 'src/router/v5.tsx'), diff --git a/packages/enhanced/src/wrapper/ModuleFederationPlugin.ts b/packages/enhanced/src/wrapper/ModuleFederationPlugin.ts index 857ecc309ba..f53fcfe92d6 100644 --- a/packages/enhanced/src/wrapper/ModuleFederationPlugin.ts +++ b/packages/enhanced/src/wrapper/ModuleFederationPlugin.ts @@ -105,7 +105,42 @@ export default class ModuleFederationPlugin implements WebpackPluginInstance { return false; }; - if (shouldEnableBridgePlugin()) { + const enableBridgePlugin = shouldEnableBridgePlugin(); + + // When bridge plugin is disabled (router disabled), alias to /base entry + if (!enableBridgePlugin && hasBridgeReact) { + compiler.hooks.afterPlugins.tap('BridgeReactBaseAliasPlugin', () => { + try { + const path = require('path'); + const fs = require('fs'); + const bridgeReactBasePath = path.resolve( + compiler.context, + 'node_modules/@module-federation/bridge-react/dist/base.es.js', + ); + + if (!fs.existsSync(bridgeReactBasePath)) { + infrastructureLogger.warn( + '⚠️ [ModuleFederationPlugin] bridge-react /base entry not found, falling back to default entry', + ); + return; + } + + compiler.options.resolve.alias = { + ...compiler.options.resolve.alias, + '@module-federation/bridge-react$': bridgeReactBasePath, + }; + infrastructureLogger.info( + '✅ [ModuleFederationPlugin] Router disabled - using /base entry (no react-router-dom)', + ); + } catch (error) { + infrastructureLogger.warn( + '⚠️ [ModuleFederationPlugin] Failed to set /base alias, using default entry', + ); + } + }); + } + + if (enableBridgePlugin) { new ReactBridgePlugin({ moduleFederationOptions: this._options, }).apply(compiler); diff --git a/packages/rspack/src/ModuleFederationPlugin.ts b/packages/rspack/src/ModuleFederationPlugin.ts index c7c6a119af4..821831bd2b1 100644 --- a/packages/rspack/src/ModuleFederationPlugin.ts +++ b/packages/rspack/src/ModuleFederationPlugin.ts @@ -250,7 +250,41 @@ export class ModuleFederationPlugin implements RspackPluginInstance { return false; }; - if (shouldEnableBridgePlugin()) { + const enableBridgePlugin = shouldEnableBridgePlugin(); + + // When bridge plugin is disabled (router disabled), alias to /base entry + if (!enableBridgePlugin && hasBridgeReact) { + compiler.hooks.afterPlugins.tap('BridgeReactBaseAliasPlugin', () => { + try { + const bridgeReactBasePath = path.resolve( + compiler.context, + 'node_modules/@module-federation/bridge-react/dist/base.es.js', + ); + + if (!fs.existsSync(bridgeReactBasePath)) { + logger.warn( + '⚠️ [ModuleFederationPlugin] bridge-react /base entry not found, falling back to default entry', + ); + return; + } + + compiler.options.resolve.alias = { + ...compiler.options.resolve.alias, + '@module-federation/bridge-react$': bridgeReactBasePath, + }; + + logger.info( + '✅ [ModuleFederationPlugin] Router disabled - using /base entry (no react-router-dom)', + ); + } catch (error) { + logger.warn( + '⚠️ [ModuleFederationPlugin] Failed to set /base alias, using default entry', + ); + } + }); + } + + if (enableBridgePlugin) { new ReactBridgePlugin({ moduleFederationOptions: this._options, }).apply(compiler);