I was looking into Webpack 5 Module federation feature, and have some trouble understanding why my code does not work. The idea is pretty similar to what standard module federation examples do:
app1
- is the host app
app2
- is a remote exposing the whole app to app1
(app1
renders the header and horizontal line, below which the app2
should be rendered)
Both app1
and app2
declares react
and react-dom
as their shared, singleton, eager dependencies in the weback.config.js
:
// app1 webpack.config.js
module.exports = {
entry: path.resolve(SRC_DIR, './index.js');,
...
plugins: [
new ModuleFederationPlugin({
name: "app1",
remotes: {
app2: `app2@//localhost:2002/remoteEntry.js`,
},
shared: { react: { singleton: true, eager: true }, "react-dom": { singleton: true, eager: true } },
}),
...
],
};
// app2 webpack.config.js
module.exports = {
entry: path.resolve(SRC_DIR, './index.js');,
...
plugins: [
new ModuleFederationPlugin({
name: "app2",
library: { type: "var", name: "app2" },
filename: "remoteEntry.js",
exposes: {
"./App": "./src/App",
},
shared: { react: { singleton: true, eager: true }, "react-dom": { singleton: true, eager: true } },
}),
...
],
};
In the App1 index.js I have next code:
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));
The App1 App.js
component is next:
import React, { Suspense } from 'react';
const RemoteApp2 = React.lazy(() => import("app2/App"));
export default function App() {
return (
<div>
<h1>App 1</h1>
<p>Below will be some content</p>
<hr/>
<Suspense fallback={'Loading App 2'}>
<RemoteApp2 />
</Suspense>
</div>
);
}
But when I start the application I get the next error:
Uncaught Error: Shared module is not available for eager consumption: webpack/sharing/consume/default/react/react?1bb3
at Object.__webpack_modules__.<computed> (consumes:133)
at __webpack_require__ (bootstrap:21)
at fn (hot module replacement:61)
at Module../src/index.js (main.bundle.a8d89941f5dd9a37d429.js:239)
at __webpack_require__ (bootstrap:21)
at startup:4
at startup:6
If I extract everything from index.js
to bootstrap.js
and in index.js
will do
import('./bootstrap');
Everything works just fine.
This confuses me as official docs and blog posts from the creator states that you can do either bootstrap.js
way OR declare dependency as an eager one.
Would appreciate any help/insights on why it does not work without bootstrap.js
pattern.
Here is a link to full GitHub sandbox I was building: https://github.com/vovkvlad/webpack-module-fedaration-sandbox/tree/master/simple
Webpack 5 Module Federation aims to solve the sharing of modules in a distributed system, by shipping those critical shared pieces as macro or as micro as you would like. It does this by pulling them out of the the build pipeline and out of your apps.
Webpack 5 recently became production ready and with it, Module Federation , Webpack's solution for micro frontends. With Module Federation we have the concept of the shell application or host , which houses your microfrontend apps or remotes .
One of the most important aspects of using module federation is sharing code. When a micro-app gets built, it contains all the files it needs to run. As stated by webpack, “These separate builds should not have dependencies between each other, so they can be developed and deployed individually”.
Just to make it clear for those who might miss the comment to the initial answer:
It seems like the main reason of why it failed initially was that remoteEntry.js
file was loaded after the code that actually runs the host app.
Both bootstrap.js
approach and adding direct script <script src="http://localhost:2002/remoteEntry.js"></script>
to the <head></head>
tag has exactly the same outcome - they make remoteEntry.js
be loaded and parsed BEFORE the main app's code.
In case of bootstrap the order is next:
main_bundle
is loadedbootstrap.js
file - remoteEntry.js
is loadedbootstrap.js
is loaded which actually runs the main appwith proposed variant by Oleg Vodolazsky events order is next:
remoteEntry.js
is loaded first as it is directly added to html
file and webpack's main_bundle
is being appended to <head></head>
after remoteEntry linkmain_bundle
is loaded and runs the applicationand in case of just trying to run app without bootstrap and without hardcoded script in the <head></head>
main_bundle
is loaded before remoteEntry.js
and as main_bundle
tries to actually run the app, it fails with an error:
In order to make it work you need to change the way you are loading remote entry.
ModuleFederationPlugin
config in webpack.config.js
for app1 to this:...
new ModuleFederationPlugin({
name: "app1",
remoteType: 'var',
remotes: {
app2: 'app2',
},
shared: {
...packageJsonDeps,
react: { singleton: true, eager: true, requiredVersion: packageJsonDeps.react },
"react-dom": { singleton: true, eager: true, requiredVersion: packageJsonDeps["react-dom"] }
},
}),
...
script
tag to the head
of your index.html
in app1 :<script src="http://localhost:2002/remoteEntry.js"></script>
Good look with further hacking!
UPDATE:
Just for the sake of it: I've create a PR to your sandbox repo with fixes described above: https://github.com/vovkvlad/webpack-module-fedaration-sandbox/pull/2
You can set the dependency as eager inside the advanced API of Module Federation, which doesn’t put the modules in an async chunk, but provides them synchronously. This allows us to use these shared modules in the initial chunk. But be careful as all provided and fallback modules will always be downloaded. It’s recommended to provide it only at one point of your application, e.g. the shell.
Webpack's website strongly recommend using an asynchronous boundary. It will split out the initialization code of a larger chunk to avoid any additional round trips and improve performance in general.
For example, your entry looked like this:
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
Let's create bootstrap.js file and move contents of the entry into it, and import that bootstrap into the entry:
index.js
+ import('./bootstrap');
- import React from 'react';
- import ReactDOM from 'react-dom';
- import App from './App';
- ReactDOM.render(<App />, document.getElementById('root'));
bootstrap.js
+ import React from 'react';
+ import ReactDOM from 'react-dom';
+ import App from './App';
+ ReactDOM.render(<App />, document.getElementById('root'));
This method works but can have limitations or drawbacks.
Setting eager: true for dependency via the ModuleFederationPlugin
webpack.config.js
// ...
new ModuleFederationPlugin({
shared: {
...deps,
react: {
eager: true,
},
},
});
Source
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With