-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
908450d
commit 9ef3c07
Showing
7 changed files
with
260 additions
and
122 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,48 @@ | ||
# Offline Remotes Runtime Plugin | ||
# Single Runtime Plugin Example | ||
|
||
This demo boots only app1, app2 remains offline. | ||
Loading localhost:3001 will render a error message from the runtime plugin as a react module when the container if offline. | ||
This example demonstrates how the Module Federation single runtime plugin works to ensure shared dependencies use a single runtime instance when a remote application loads components from its host. | ||
|
||
## Running the Demo | ||
|
||
1. Start both applications: | ||
```bash | ||
# In app1 directory | ||
npm start # Runs on port 3001 | ||
|
||
# In app2 directory | ||
npm start # Runs on port 3002 | ||
``` | ||
|
||
## What to Observe | ||
|
||
### On App1 (port 3001) | ||
When you browse to `localhost:3001`, observe the Runtime Information section: | ||
- Notice that App2's module is using `app1_partial.js` instead of `remoteEntry.js` | ||
- This happens because App2 lists App1 as a remote, and to avoid loading conflicting runtimes from the same build (App1), the plugin switches to using the partial bundle | ||
- The partial bundle ensures App1's components use the host's runtime when loaded in App2 | ||
|
||
### On App2 (port 3002) | ||
When you browse to `localhost:3002`, observe the Runtime Information section: | ||
- When loading App1's remote components, it uses the standard `remoteEntry.js` | ||
- This is because App1 is not the host in this context | ||
- Since there's no host/remote pattern here, App1 needs its full standalone runtime to operate | ||
|
||
## How it Works | ||
|
||
The single runtime plugin prevents runtime conflicts by: | ||
1. When a remote app loads components from its host: | ||
- The plugin detects this pattern and switches to using `{hostName}_partial.js` | ||
- This ensures the remote uses the host's runtime instead of loading a duplicate | ||
- Prevents conflicts in singleton modules and shared dependencies | ||
|
||
2. When loading other remotes: | ||
- Uses the standard `remoteEntry.js` | ||
- No runtime conflict possible since it's loading from a different build | ||
|
||
### Shared Dependencies | ||
Both apps share: | ||
- React (singleton) | ||
- ReactDOM (singleton) | ||
- Lodash (version matching) | ||
|
||
The single runtime plugin ensures these shared dependencies maintain their singleton status by preventing duplicate runtime loading from the same build. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,69 +1,86 @@ | ||
import React from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
import lodash from 'lodash'; | ||
|
||
import LocalButton from './Button'; | ||
import RemoteButton from 'app2/Button'; | ||
|
||
// A function to generate a color from a string | ||
const getColorFromString = str => { | ||
// Prime numbers used for generating a hash | ||
let primes = [1, 2, 3, 5, 7, 11, 13, 17, 19, 23]; | ||
let hash = 0; | ||
|
||
// Generate a hash from the string | ||
for (let i = 0; i < str.length; i++) { | ||
hash += str.charCodeAt(i) * primes[i % primes.length]; | ||
} | ||
const App = () => { | ||
const [count, setCount] = React.useState(0); | ||
|
||
return ( | ||
<div style={{ fontFamily: 'Arial, sans-serif', padding: '20px', maxWidth: '800px', margin: '0 auto' }}> | ||
<div style={{ marginBottom: '20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> | ||
<h1>App 1 - Single Runtime Demo</h1> | ||
<a | ||
href="http://localhost:3002" | ||
style={{ | ||
padding: '8px 16px', | ||
background: '#e24a90', | ||
color: 'white', | ||
textDecoration: 'none', | ||
borderRadius: '4px', | ||
fontSize: '14px' | ||
}} | ||
> | ||
Go to App 2 → | ||
</a> | ||
</div> | ||
|
||
// Convert the hash to a color | ||
let color = '#'; | ||
for (let i = 0; i < 3; i++) { | ||
const value = (hash >> (i * 8)) & 0xff; | ||
color += ('00' + value.toString(16)).substr(-2); | ||
} | ||
<div style={{ | ||
padding: '15px', | ||
background: '#f8f9fa', | ||
borderRadius: '4px', | ||
marginBottom: '20px', | ||
border: '1px solid #e9ecef' | ||
}}> | ||
<h3 style={{ margin: '0 0 10px 0', color: '#4a90e2' }}>What's Happening Here?</h3> | ||
<p style={{ margin: '0', lineHeight: '1.5' }}> | ||
This is App1 running on port 3001. Notice in the Runtime Information that App2's module is using <code>app1_partial.js</code> instead | ||
of <code>remoteEntry.js</code>. This happens because App2 lists App1 as a remote, and to avoid loading conflicting runtimes from the | ||
same build (App1), the plugin switches to using the partial bundle. | ||
</p> | ||
</div> | ||
|
||
<div style={{ marginBottom: '20px' }}> | ||
<h3>Shared State Counter: {count}</h3> | ||
<button onClick={() => setCount(c => c + 1)}>Increment Counter</button> | ||
</div> | ||
|
||
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}> | ||
<div> | ||
<h3>Local Button:</h3> | ||
<LocalButton /> | ||
</div> | ||
<div> | ||
<h3>Remote Button (from App 2):</h3> | ||
<React.Suspense fallback="Loading Remote Button..."> | ||
<RemoteButton /> | ||
</React.Suspense> | ||
</div> | ||
</div> | ||
|
||
return color; | ||
<div style={{ marginTop: '20px' }}> | ||
<h3>Runtime Information:</h3> | ||
<div style={{ background: '#f0f0f0', padding: '10px', borderRadius: '4px' }}> | ||
{__FEDERATION__.__INSTANCES__.map(instance => ( | ||
<div key={instance.name} style={{ margin: '10px 0' }}> | ||
<div> | ||
<strong>Module: </strong>{instance.name} | ||
</div> | ||
{instance.options?.remotes?.length > 0 && ( | ||
<div style={{ marginLeft: '20px', fontSize: '14px' }}> | ||
<strong>Remote Entries:</strong> | ||
{instance.options.remotes.map((remote, idx) => ( | ||
<div key={idx} style={{ marginTop: '5px', color: '#666' }}> | ||
• {remote.alias || remote.name}: <code>{remote.entry}</code> | ||
</div> | ||
))} | ||
</div> | ||
)} | ||
</div> | ||
))} | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
// The main App component | ||
const App = () => ( | ||
<div> | ||
<h1>Single Runtime</h1> | ||
<h2>Remotes currently in use</h2> | ||
{/* Display the names of the remotes loaded by the CustomPlugin */} | ||
{__FEDERATION__.__INSTANCES__.map(inst => ( | ||
<span | ||
style={{ | ||
padding: 10, | ||
color: '#fff', | ||
background: getColorFromString(inst.name.split().reverse().join('')), | ||
}} | ||
key={inst.name} | ||
> | ||
{inst.name} | ||
</span> | ||
))} | ||
<p> | ||
Click The second button. This will cause the <i>pick-remote.ts</i> to load remoteEntry urls | ||
from a mock api call. | ||
</p> | ||
{/* LocalButton is a button component from the local app */} | ||
<LocalButton /> | ||
{/* RemoteButton is a button component loaded from a remote app */} | ||
<React.Suspense fallback="Loading Button"> | ||
<RemoteButton /> | ||
</React.Suspense> | ||
{/* The Reset button clears the 'button' item from localStorage */} | ||
<button | ||
onClick={() => { | ||
localStorage.clear('button'); | ||
window.location.reload(); | ||
}} | ||
> | ||
Reset{' '} | ||
</button> | ||
</div> | ||
); | ||
|
||
export default App; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,25 @@ | ||
import React from 'react'; | ||
|
||
const style = { | ||
background: '#800', | ||
color: '#fff', | ||
padding: 12, | ||
}; | ||
const Button = () => { | ||
const [clickCount, setClickCount] = React.useState(0); | ||
|
||
const style = { | ||
background: '#4a90e2', | ||
color: '#fff', | ||
padding: '10px 20px', | ||
border: 'none', | ||
borderRadius: '4px', | ||
cursor: 'pointer' | ||
}; | ||
|
||
const Button = () => <button style={style}>App 1 Button</button>; | ||
return ( | ||
<button | ||
style={style} | ||
onClick={() => setClickCount(c => c + 1)} | ||
> | ||
App 1 Button (Clicks: {clickCount}) | ||
</button> | ||
); | ||
}; | ||
|
||
export default Button; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,85 @@ | ||
import LocalButton from './Button'; | ||
import React from 'react'; | ||
import LocalButton from './Button'; | ||
import RemoteButton from 'app1/Button'; | ||
|
||
const App = () => { | ||
const [count, setCount] = React.useState(0); | ||
|
||
return ( | ||
<div style={{ fontFamily: 'Arial, sans-serif', padding: '20px', maxWidth: '800px', margin: '0 auto' }}> | ||
<div style={{ marginBottom: '20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> | ||
<h1>App 2 - Single Runtime Demo</h1> | ||
<a | ||
href="http://localhost:3001" | ||
style={{ | ||
padding: '8px 16px', | ||
background: '#4a90e2', | ||
color: 'white', | ||
textDecoration: 'none', | ||
borderRadius: '4px', | ||
fontSize: '14px' | ||
}} | ||
> | ||
← Go to App 1 | ||
</a> | ||
</div> | ||
|
||
const RemoteButton = React.lazy(() => import('app1/Button')); | ||
<div style={{ | ||
padding: '15px', | ||
background: '#f8f9fa', | ||
borderRadius: '4px', | ||
marginBottom: '20px', | ||
border: '1px solid #e9ecef' | ||
}}> | ||
<h3 style={{ margin: '0 0 10px 0', color: '#e24a90' }}>What's Happening Here?</h3> | ||
<p style={{ margin: '0', lineHeight: '1.5' }}> | ||
This is App2 running on port 3002. When loading App1's remote components, it uses the standard <code>remoteEntry.js</code> because | ||
App1 is not the host in this context. Since there's no host/remote pattern here, App1 needs its full standalone runtime to operate. | ||
</p> | ||
</div> | ||
|
||
<div style={{ marginBottom: '20px' }}> | ||
<h3>Shared State Counter: {count}</h3> | ||
<button onClick={() => setCount(c => c + 1)}>Increment Counter</button> | ||
</div> | ||
|
||
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}> | ||
<div> | ||
<h3>Local Button:</h3> | ||
<LocalButton /> | ||
</div> | ||
<div> | ||
<h3>Remote Button (from App 1):</h3> | ||
<React.Suspense fallback="Loading Remote Button..."> | ||
<RemoteButton /> | ||
</React.Suspense> | ||
</div> | ||
</div> | ||
|
||
const App = () => ( | ||
<div> | ||
<h1>API controlled remote configs</h1> | ||
<h2>App 2</h2> | ||
<LocalButton /> | ||
<React.Suspense fallback="Loading Button"> | ||
<RemoteButton /> | ||
</React.Suspense> | ||
</div> | ||
); | ||
<div style={{ marginTop: '20px' }}> | ||
<h3>Runtime Information:</h3> | ||
<div style={{ background: '#f0f0f0', padding: '10px', borderRadius: '4px' }}> | ||
{__FEDERATION__.__INSTANCES__.map(instance => ( | ||
<div key={instance.name} style={{ margin: '10px 0' }}> | ||
<div> | ||
<strong>Module: </strong>{instance.name} | ||
</div> | ||
{instance.options?.remotes?.length > 0 && ( | ||
<div style={{ marginLeft: '20px', fontSize: '14px' }}> | ||
<strong>Remote Entries:</strong> | ||
{instance.options.remotes.map((remote, idx) => ( | ||
<div key={idx} style={{ marginTop: '5px', color: '#666' }}> | ||
• {remote.alias || remote.name}: <code>{remote.entry}</code> | ||
</div> | ||
))} | ||
</div> | ||
)} | ||
</div> | ||
))} | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default App; |
Oops, something went wrong.