Unveiling the Magic of Web Development: demystifying Microfrontends
Introduction to Microfrontends
Microfrontends is an architectural style for building modern web applications. It involves breaking down a large application into smaller, independent parts called micro-frontends. Each microfrontend is a self-contained unit of functionality with its codebase, build process, and deployment pipeline. This approach offers several advantages over traditional monolithic architectures.
Benefits of micro-frontend:
Increased agility: Microfrontends can be developed and deployed independently of each other, which allows teams to work on features in parallel and release them more frequently.
Improved scalability: Microfrontends can be scaled independently based on their individual needs, which makes it easier to handle high-traffic loads.
Enhanced maintainability: Microfrontends are easier to understand and maintain than monolithic applications, as the codebase is smaller and more focused.
Greater flexibility: Microfrontends can be built using different technologies and frameworks, which allows teams to choose the best tools for the job.
Challenges of Micro-frontend:
Increased complexity: Microfrontends can increase the complexity of a web application, as there are more moving parts to manage.
Performance overhead: Microfrontends can introduce additional performance overhead, as there are more HTTP requests to be made.
Security concerns: Microfrontends can introduce new security concerns, as each microfrontend is a potential attack vector.
A way to micro-frontend
there are several ways to build micro-frontend architecture, we would focus on module federation but what is module federation?
Module Federation: Dynamically Loading Microfrontends
Module Federation is a powerful concept and feature of Webpack that allows for the dynamic loading of modules across multiple independent applications. This enables developers to build micro-frontends, which are essentially small, independent units of functionality that can be combined and loaded dynamically at runtime to create a larger web application.
Key Features of Module Federation:
Dynamic loading: Modules load on demand, reducing initial page load time.
Independent builds: Microfrontends have autonomous build processes and can be developed and deployed independently.
Cross-origin loading: Modules from different origins can be combined, fostering collaboration across teams or projects.
Sharing and isolation: Choose to share or isolate specific modules across micro-frontends for code reuse while maintaining independence.
Modular architecture: Encourages modularity and maintainability in large applications.
Benefits of Module Federation:
Reduced bundle size: Only necessary modules are loaded, leading to smaller initial bundles and faster loading times.
Improved development agility: Microfrontends can be developed and deployed independently, enabling faster iteration and release cycles.
Increased scalability: Each micro-frontend can scale independently based on its individual needs.
Flexibility in technology choices: Different micro-frontends can be built with diverse technologies and frameworks.
Modular codebase: Promotes modular design, facilitating easier maintenance and updates.
Essential Plugins in JavaScript Module Federation:
ContainerPlugin is used in applications that expose remote modules.
ContainerReferencePlugin is used in host applications that consume remote modules.
SharePlugin is used by both remote and host applications to manage shared packages and handle versioning.
Decoding the Role of remoteEntry.js in Module Federation
The remoteEntry.js file serves as a crucial component in Module Federation, containing references to exposed modules within an application. It differs from typical code files as it doesn't store the actual module code but rather understands where to retrieve it. This distinction is vital for enabling the lazy loading of modules through import, offering flexibility in how code is loaded. The file name itself, remoteEntry.js, is customizable, allowing developers to choose a suitable identifier.
One paramount requirement for remoteEntry.js is that the publicPath, specified in webpack.config.js, aligns with the deployment location of the code. Since remoteEntry.js solely contains references to the code locations and not the code itself, Webpack relies on the publicPath to locate and load modules asynchronously. Therefore, maintaining coherence between the publicPath and the deployment location is essential for the seamless operation of the Module Federation system.
Why the shared key in the ModuleFederationPlugin is so critical?
Duplication: Without proper sharing, package duplication occurs, leading to a slowdown in the application experience.
Version Mismatch: Incorrect sharing setups can result in different remote modules consuming incompatible versions, potentially causing crashes.
Singletons and Internal State: Many frontend libraries rely on the internal state for proper functioning, necessitating a singleton instance. Failure to manage this correctly can lead to issues, emphasizing the importance of precise configuration.
Join the Journey: Crafting a Micro Frontend Together
Embark on the development of a React host integrating React and Angular micro apps. Start by constructing the host application and explore the ModuleFederationPlugin configuration for the host app. Explore the tailored ModuleFederationPlugin configuration, including key parameters like name, remoteType, remotes, and shared.
new ModuleFederationPlugin({
name: 'HostApp',
filename: 'remoteEntry.js',
remoteType: 'var',
remotes: {
ReactClient: "ReactClient",
AngularClient:"AngularClient",
},
shared: {
...Object.assign({}, ...Object.keys(deps).map((dep) => ({
[dep]: {
singleton: true,
requiredVersion: deps[dep],
eager: true,
}
}))),
},
}),
name
: This prop specifies the name of the current module or application. It is used to identify the module when it is consumed by other modules.remoteType
: This prop specifies the type of remote module federation. The value'var'
indicates that the remote modules will be loaded as global variables.remotes
: This prop defines the remote modules that the current module depends on. It is an object where the keys represent the names of the remote modules, and the values represent the names of the exposed modules from those remote modules.shared
: This prop defines the shared dependencies between the current module and the remote modules. It is an object where the keys represent the names of the shared dependencies, and the values are configuration objects for each shared dependency.singleton
: This prop specifies whether the shared dependency should be a singleton. When set to, only one instance of the shared dependency will be created and shared across all modules. This can help reduce duplication and improve performance.requiredVersion
: This prop specifies the required version of the shared dependency. It ensures that the correct version of the shared dependency is used across all modules.eager
: This prop specifies whether the shared dependency should be eagerly loaded. When set totrue
, the shared dependency will be loaded as soon as possible, even if it is not immediately required by the module.
Below is the customized ModuleFederationPlugin configuration specifically crafted for the React app:
new ModuleFederationPlugin({
name: 'ReactClient',
filename: 'remoteEntry.js',
exposes: {
'./rightSidebar': './src/App.js',
},
shared: {
/*in case you didn't add those as singleton
you may not able to use react hooks*/
'react': { singleton: true, eager: true, requiredVersion: '^18.2.0' },
'react-dom': { singleton: true, eager: true, requiredVersion: '^18.2.0' },
'react-router-dom': { singleton: true, eager: true, requiredVersion: '^6.12.1' },
}
}),
Below is the customized ModuleFederationPlugin configuration for the Angular app:
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const mf = require("@angular-architects/module-federation/webpack");
const share = mf.share;
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(
path.join(__dirname, 'tsconfig.json'),
[/* mapped paths to share */]);
/*note you need to add sharedmapping aliases to resolve
in the webpack configrations from sharedMappings.getAliases()*/
new ModuleFederationPlugin({
name: "AngularClient",
filename: "remoteEntry.js",
exposes: {
'./leftSideBar':'./src/loadApp.ts'
},
shared: share({
"@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
...sharedMappings.getDescriptors()
})
}),
sharedMappings.getPlugin()
SharedMappings
Utility:
SharedMappings
simplifies the process of managing shared dependencies by providing a convenient way to define and map shared libraries.
Integrating Federated Apps into the Host:
seamlessly integrate federated apps into the React host using lazy loading with Suspense, ensuring a smooth user experience.
import React, { Suspense, lazy } from 'react'
const RightSidebarModule = lazy(() => import("./RightSidebar").catch(err => {
console.error(err);
return { default: () => <div>Failed to load module</div> };
}));
const LeftSidebarModule = lazy(() => import("./LeftSidebar").catch(err => {
console.error(err);
return { default: () => <div>Failed to load module</div> };
}));
function App() {
return (
<>
<div className='header'>
<h1>This is Host app (React)</h1>
<h2>Micro host app is integrated below</h2>
</div>
<div >
<div style={{
display: "flex",
flexDirection: "row",
}}>
<Suspense fallback={<span>Loading...</span>}>
<RightSidebarModule />
</Suspense>
<Suspense fallback={<span>Loading...</span>}>
<LeftSidebarModule />
</Suspense>
</div>
</div>
</>
)
}
export default App;
Imports:
- Importing necessary dependencies from React, including
Suspense
andlazy
for handling the lazy loading of components.
- Importing necessary dependencies from React, including
Lazy Loading:
- Lazily loading two components,
RightSidebarModule
andLeftSidebarModule
, using thelazy
function. If there's an import error, a fallback component is provided.
- Lazily loading two components,
Fallback UI:
While the modules are loading, a fallback UI with the text "Loading..." is displayed using the
Suspense
component.Also, a default component if the loading throws an error
Rather than elongating this blog, I'm offering the entire codebase for the application, accessible at https://github.com/massoudsalem/Micro-Frontend-Example. The implementation achieves CSS isolation through the application of Shadow DOM, and the provided example is already equipped with this feature. TypeScript enthusiasts may find value in exploring the FederatedTypesPlugin
form @module-federation/typescript
as an alternative to the traditional ModuleFederationPlugin
. As a final consideration, for enhanced simplicity, utilizing tools like Craco or React App Rewired might be preferable over directly employing Webpack. However, for the sake of clarity in this case, Webpack is employed here.
Subscribe to my newsletter
Read articles from Mohammad Massoud Salem directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Mohammad Massoud Salem
Mohammad Massoud Salem
My goal is to create inspiring web solutions through thoughtful code and user-centered design that enhance the user experience.