Skip to content

Commit

Permalink
feat(runtime): allow errorLoadRemote hook catch remote entry resource…
Browse files Browse the repository at this point in the history
… loading error (#3474)

Co-authored-by: kyli <[email protected]>
  • Loading branch information
danpeen and KyrieLii authored Feb 6, 2025
1 parent e751ad0 commit 9566fbc
Show file tree
Hide file tree
Showing 23 changed files with 1,519 additions and 59 deletions.
6 changes: 6 additions & 0 deletions .changeset/long-forks-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@module-federation/runtime-core': patch
'website-new': patch
---

feat: allow errorLoadRemote hook catch remote entry resource loading error
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe('router-remote-error in host', () => {
describe('Remote Resource Error render and will trigger ErrorBoundary', () => {
Cypress.on('uncaught:exception', () => false);
it('jump to remote error page', async () => {
cy.get('.host-menu > li:nth-child(8)').click();
cy.get('.host-menu > li:nth-child(8)').click({ force: true });

cy.get('[data-test-id="loading"]').should('have.length', 1);
cy.get('[data-test-id="loading"]')
Expand All @@ -16,7 +16,7 @@ describe('router-remote-error in host', () => {
await wait5s();
getP().contains('Something went wrong');
getPre().contains(
'Error: The request failed three times and has now been abandoned',
'The request failed three times and has now been abandoned',
);
});
});
Expand Down
3 changes: 2 additions & 1 deletion apps/router-demo/router-host-2000/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"@module-federation/retry-plugin": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.24.1"
"react-router-dom": "^6.24.1",
"react-error-boundary": "5.0.0"
},
"devDependencies": {
"@module-federation/rsbuild-plugin": "workspace:*",
Expand Down
15 changes: 10 additions & 5 deletions apps/router-demo/router-host-2000/rsbuild.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,21 @@ export default defineConfig({
'remote-render-error':
'remote-render-error@http://localhost:2004/mf-manifest.json',
'remote-resource-error':
'remote-resource-errorr@http://localhost:2008/not-exist-mf-manifest.json',
'remote-resource-error@http://localhost:2008/not-exist-mf-manifest.json',
},
shared: {
react: {
singleton: true,
},
'react-dom': {
singleton: true,
},
},
shared: ['react', 'react-dom', 'antd'],
runtimePlugins: [
path.join(__dirname, './src/runtime-plugin/shared-strategy.ts'),
path.join(__dirname, './src/runtime-plugin/retry.ts'),
path.join(__dirname, './src/runtime-plugin/fallback.ts'),
],
// bridge: {
// disableAlias: true,
// },
}),
],
});
99 changes: 98 additions & 1 deletion apps/router-demo/router-host-2000/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { useRef, useEffect, ForwardRefExoticComponent } from 'react';
import React, {
useRef,
useEffect,
ForwardRefExoticComponent,
Suspense,
} from 'react';
import { Route, Routes, useLocation } from 'react-router-dom';
import { init, loadRemote } from '@module-federation/enhanced/runtime';
import { RetryPlugin } from '@module-federation/retry-plugin';
Expand All @@ -8,6 +13,19 @@ import Detail from './pages/Detail';
import Home from './pages/Home';
import './App.css';
import BridgeReactPlugin from '@module-federation/bridge-react/plugin';
import { ErrorBoundary } from 'react-error-boundary';
import Remote1AppNew from 'remote1/app';
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
import { Spin } from 'antd';

const fallbackPlugin: () => FederationRuntimePlugin = function () {
return {
name: 'fallback-plugin',
errorLoadRemote(args) {
return { default: () => <div> fallback component </div> };
},
};
};

init({
name: 'federation_consumer',
Expand All @@ -30,6 +48,7 @@ init({
// },
// },
// }),
// fallbackPlugin(),
],
});

Expand All @@ -54,6 +73,63 @@ const Remote1App = createRemoteComponent({
loading: FallbackComp,
});

const Remote1AppWithLoadRemote = React.lazy(
() =>
new Promise((resolve) => {
// delay 2000ms to show suspense effects
setTimeout(() => {
resolve(loadRemote('remote1/app'));
}, 2000);
}),
);

const LoadingFallback = () => (
<div
style={{
padding: '50px',
textAlign: 'center',
background: '#f5f5f5',
border: '1px solid #d9d9d9',
borderRadius: '4px',
marginTop: '20px',
}}
>
<Spin size="large" />
<div
style={{
marginTop: '16px',
color: '#1677ff',
fontSize: '16px',
}}
>
Loading Remote1 App...
</div>
</div>
);

const Remote1AppWithErrorBoundary = React.forwardRef<any, any>((props, ref) => (
<ErrorBoundary
fallback={
<div
style={{
padding: '20px',
background: '#fff2f0',
border: '1px solid #ffccc7',
borderRadius: '4px',
color: '#cf1322',
marginTop: '20px',
}}
>
Error loading Remote1App. Please try again later.
</div>
}
>
<Suspense fallback={<LoadingFallback />}>
<Remote1AppWithLoadRemote {...props} ref={ref} />
</Suspense>
</ErrorBoundary>
));

const Remote2App = createRemoteComponent({
loader: () => import('remote2/export-app'),
export: 'provider',
Expand Down Expand Up @@ -145,6 +221,27 @@ const App = () => {
path="/remote-resource-error/*"
Component={() => <RemoteResourceErrorApp />}
/>
<Route
path="/error-load-with-hook/*"
Component={() => (
<Remote1AppNew name={'Ming'} age={12} />
// <React.Suspense fallback={<div> Loading Remote1App...</div>}>
// <Remote1AppWithLoadRemote name={'Ming'} age={12} />
// </React.Suspense>
)}
/>

<Route
path="/error-load-with-error-boundary/*"
Component={() => (
<Remote1AppWithErrorBoundary
name={'Ming'}
age={12}
ref={ref}
basename="/remote1"
/>
)}
/>
</Routes>
</div>
);
Expand Down
14 changes: 14 additions & 0 deletions apps/router-demo/router-host-2000/src/navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,20 @@ function Navgation() {
key: '/remote-resource-error',
icon: <GroupOutlined />,
},
{
label: <Link to="/error-load-with-hook">error-load-with-hook</Link>,
key: '/error-load-with-hook',
icon: <GroupOutlined />,
},
{
label: (
<Link to="/error-load-with-error-boundary">
error-load-with-error-boundary
</Link>
),
key: '/error-load-with-error-boundary',
icon: <GroupOutlined />,
},
];

const onClick: MenuProps['onClick'] = (e) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Error Handling Strategies for Module Federation
*
* This module provides different strategies for handling remote module loading errors.
* Choose the strategy that best fits your needs:
*
* 1. Lifecycle-based Strategy:
* - Handles errors differently based on the lifecycle stage
* - Provides backup service support for entry file errors
* - More granular control over error handling
*
* 2. Simple Strategy:
* - Single fallback component for all error types
* - Consistent error presentation
* - Minimal configuration required
*
* Example usage:
* ```typescript
* import { createLifecycleBasedPlugin, createSimplePlugin } from './error-handling';
*
* // Use lifecycle-based strategy
* const plugin1 = createLifecycleBasedPlugin({
* backupEntryUrl: 'http://backup-server/manifest.json',
* errorMessage: 'Custom error message'
* });
*
* // Use simple strategy
* const plugin2 = createSimplePlugin({
* errorMessage: 'Module failed to load'
* });
* ```
*/

export * from './lifecycle-based';
export * from './simple';
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Lifecycle-based Error Handling Strategy
*
* This implementation demonstrates a more granular approach to error handling
* by responding differently based on the lifecycle stage where the error occurred.
*
* Two main stages are handled:
* 1. Component Loading (onLoad): Provides a UI fallback for component rendering failures
* 2. Entry File Loading (afterResolve): Attempts to load from a backup service
*/

import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';

interface LifecycleBasedConfig {
backupEntryUrl?: string;
errorMessage?: string;
}

export const createLifecycleBasedPlugin = (
config: LifecycleBasedConfig = {},
): FederationRuntimePlugin => {
const {
backupEntryUrl = 'http://localhost:2002/mf-manifest.json',
errorMessage = 'Module loading failed, please try again later',
} = config;

return {
name: 'lifecycle-based-fallback-plugin',
async errorLoadRemote(args) {
// Handle component loading errors
if (args.lifecycle === 'onLoad') {
const React = await import('react');

// Create a fallback component with error message
const FallbackComponent = React.memo(() => {
return React.createElement(
'div',
{
style: {
padding: '16px',
border: '1px solid #ffa39e',
borderRadius: '4px',
backgroundColor: '#fff1f0',
color: '#cf1322',
},
},
errorMessage,
);
});

FallbackComponent.displayName = 'ErrorFallbackComponent';

return () => ({
__esModule: true,
default: FallbackComponent,
});
}

// Handle entry file loading errors
if (args.lifecycle === 'afterResolve') {
try {
// Try to load backup service
const response = await fetch(backupEntryUrl);
if (!response.ok) {
throw new Error(
`Failed to fetch backup entry: ${response.statusText}`,
);
}
const backupManifest = await response.json();
console.info('Successfully loaded backup manifest');
return backupManifest;
} catch (error) {
console.error('Failed to load backup manifest:', error);
// If backup service also fails, return original error
return args;
}
}

return args;
},
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Simple Error Handling Strategy
*
* This implementation provides a straightforward approach to error handling
* by using a single fallback component for all types of errors.
*
* Benefits:
* - Simple to understand and implement
* - Consistent error presentation
* - Requires minimal configuration
*
* Use this when you don't need different handling strategies for different error types.
*/

import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';

interface SimpleConfig {
errorMessage?: string;
}

export const createSimplePlugin = (
config: SimpleConfig = {},
): FederationRuntimePlugin => {
const { errorMessage = 'Module loading failed, please try again later' } =
config;

return {
name: 'simple-fallback-plugin',
async errorLoadRemote() {
const React = await import('react');

// Create a fallback component with error message
const FallbackComponent = React.memo(() => {
return React.createElement(
'div',
{
style: {
padding: '16px',
border: '1px solid #ffa39e',
borderRadius: '4px',
backgroundColor: '#fff1f0',
color: '#cf1322',
},
},
errorMessage,
);
});

FallbackComponent.displayName = 'ErrorFallbackComponent';

return () => ({
__esModule: true,
default: FallbackComponent,
});
},
};
};
Loading

0 comments on commit 9566fbc

Please sign in to comment.