Reputation: 1720
I am currently exploring about micro frontend with Module Federation. I just forked a sandbox, tried it with success when the both modules available. It has 2 modules, app1
as the host, and app2
as the remote component. But as I think that each modules in module federation should be independent, I tried to make the app2
unavailable as I didn't start it. Therefore I got error when I run the app1
, it finished loading with displaying the fallback of the React's Suspense
, but milliseconds later, it becomes blank as there's error I can't retrieve thus I don't really know.
After that, I tried Webpack's Promise Based Dynamic Remotes, then my webpack-config.js
becomes like this:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const ExternalTemplateRemotesPlugin = require('external-remotes-plugin');
const path = require('path');
module.exports = {
entry: './src/index',
mode: 'development',
devServer: {
static: path.join(__dirname, 'dist'),
port: 3001,
},
output: {
publicPath: 'auto',
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: ['@babel/preset-react'],
},
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'app1',
remotes: {
app2: Promise((resolve) => {
const urlParams = new URLSearchParams(window.location.search);
const version = urlParams.get('app1VersionParam');
// This part depends on how you plan on hosting and versioning your federated modules
const remoteUrlWithVersion = '[app2Url]' + '/remoteEntry.js';
const script = document.createElement('script');
script.src = remoteUrlWithVersion;
script.onload = () => {
// the injected script has loaded and is available on window
// we can now resolve this Promise
const proxy = {
get: (request) => window.app1.get(request),
init: (arg) => {
try {
return window.app1.init(arg);
} catch (e) {
console.log('remote container already initialized');
}
},
};
resolve(proxy);
};
// inject this script with the src set to the versioned remoteEntry.js
document.head.appendChild(script);
}),
// "app2@[app2Url]/remoteEntry.js",
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
new ExternalTemplateRemotesPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
I tried start the app1
again, then this error comes:
$ webpack serve
[webpack-cli] Failed to load '/home/projects/github-rl5uyr/app1/webpack.config.js' config
[webpack-cli] TypeError: undefined is not a promise
at Promise (<anonymous>)
at Object.eval (/home/projects/github-rl5uyr/app1/webpack.config.js:32:15)
at Object.function (https://github-rl5uyr.w.staticblitz.com/blitz.01faa899fac41642342f4b7113feacabea334fa1.js:11:114831)
at Module._compile (https://github-rl5uyr.w.staticblitz.com/blitz.01faa899fac41642342f4b7113feacabea334fa1.js:6:167880)
at Object.Module._extensions..js (https://github-rl5uyr.w.staticblitz.com/blitz.01faa899fac41642342f4b7113feacabea334fa1.js:6:168239)
at Module.load (https://github-rl5uyr.w.staticblitz.com/blitz.01faa899fac41642342f4b7113feacabea334fa1.js:6:166317)
at Function.Module._load (https://github-rl5uyr.w.staticblitz.com/blitz.01faa899fac41642342f4b7113feacabea334fa1.js:6:163857)
at Module.require (https://github-rl5uyr.w.staticblitz.com/blitz.01faa899fac41642342f4b7113feacabea334fa1.js:6:166635)
at i (https://github-rl5uyr.w.staticblitz.com/blitz.01faa899fac41642342f4b7113feacabea334fa1.js:6:427483)
at _0x5301a6 (https://github-rl5uyr.w.staticblitz.com/blitz.01faa899fac41642342f4b7113feacabea334fa1.js:11:114450)
So, can the module federations run independently each other? If not, what's the real difference as normal library dependencies of monolith front end instead of this sophisticated micro frontend, that I assumed it should be able to work independently like microservices?
Upvotes: 6
Views: 15212
Reputation: 1246
Problem: TypeError: Failed to fetch dynamically imported module:
I had this when trying to load an unavailable module while loading it lazily.
const Counter = lazy(() => import("remoteApp/Counter")); // it may break!
I solved it natively on the react
side WITHOUT adding any extra dependencies by catching the error on the import statement
like so:
import { lazy, Suspense } from "react";
const Counter = lazy(() =>
import("remoteApp/Counter").catch(() => ({
default: ({ onLoadingError }: { onLoadingError: (error: Error) => void }) => {
onLoadingError(new Error("Unable to load Counter"));
return <h5>Unable to load Counter, please reload the page!</h5>;
},
}))
);
export default function App() {
function errorHandler(e: Error) {
console.error(e.message);
// Your app is saved from bowing up!
// Do whatever you want here...
}
return (
<>
<h1>Host App</h1>
<div className="card">
<Suspense fallback="Loading Counter...">
<Counter onLoadingError={errorHandler} />
</Suspense>
</div>
</>
);
}
Counter
is a regular react component.
If the error occurs it will return a fallback UI in this case:
<h5>Unable to load Counter, please reload the page!</h5>
I passed a custom error handler named onLoadingError
to inform my business logic that the remote component failed to load.
Upvotes: 0
Reputation: 219
I faced the same problem and the solution I found was not in the webpack.config.js
but in the import of the modules in react. The first thing is to install these 2 dependencies
npm i react-lazily (https://www.npmjs.com/package/react-lazily)
npm i react-error-boundary (https://www.npmjs.com/package/react-error-boundary)
The components or views that you want to import from any of your apps would be called with lazily
, for example
const { Login } = lazily(() => import("auth/Auth"));
const { Directory } = lazily(() => import("directory/Directory"));
const { Resources } = lazily(() => import("resources/Resources"));
const { Statistics } = lazily(() => import("statistics/Statistics"));
For the use of react-error-boundary
I would recommend making a separate component, so I did
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
export const Externals = ({ children }) =>
{
return (
<Suspense
fallback="loading...."
>
<ErrorBoundary>
{ children }
</ErrorBoundary>
</Suspense>
)
}
In my case I made use of this component in my AppRouter.jsx
file as follows
import React from 'react';
import { createBrowserRouter, Navigate } from "react-router-dom";
import { lazily } from 'react-lazily'
import { Authentification, AuthentificationChecked } from '../views/grid/index';
import { Externals } from './Externals';
const { Login } = lazily(() => import("auth/Auth"));
const { Directory } = lazily(() => import("directory/Directory"));
const { Resources } = lazily(() => import("resources/Resources"));
const { Statistics } = lazily(() => import("statistics/Statistics"));
export const router = createBrowserRouter(
[
{
path: '/*', element: <Navigate to="auth/login" />
},
{
path: 'auth',
element: <Authentification />,
children:
[
{ path: 'login', element: <Externals><Login /></Externals>, errorElement: <div>Error al cargar el modulo</div> }
]
},
{
path: 'main',
element: <AuthentificationChecked />,
children:
[
{ path: 'directory', element: <Externals><Directory /></Externals>, errorElement: <div>Error al cargar el modulo</div> },
{ path: 'resources', element: <Externals><Resources /></Externals>, errorElement: <div>Error al cargar el modulo</div> },
{ path: 'statistics', element: <Externals><Statistics /></Externals>, errorElement: <div>Error al cargar el modulo</div> }
]
}
]);
Where <Externals />
encapsulates my component that I import from any of the apps
, if it responds correctly in the view the component will be reflected, but if an error occurs the application that imports it will not hang or stay in white, in its case it will show the error message that is included in errorElement
of each route.
And this without having to do any extra configuration in the webpack.config.js
, this is how my file looks
remotes:
{
auth: "auth@http://localhost:8081/remoteEntry.js",
directory: "directory@http://localhost:8082/remoteEntry.js",
resources: "resources@http://localhost:8083/remoteEntry.js",
statistics: "statistics@http://localhost:8084/remoteEntry.js"
},
Upvotes: 1
Reputation: 41
I think there are some stuff to be fixed in that promise definition: first of all it should be defined as a string
app2: new Promise((resolve) => { ....
Where the configuration passed for app2 is a string between ` character
As stated in the docs
After that you should change your proxy definition to fetch window.app2
not window.app1
.
Finally remoteUrlWithVersion = '[app2Url]' + '/remoteEntry.js';
is not a valid URL
Hope it helps
Upvotes: 1