Share Dependencies Between Micro FE Apps
Let's continue with our micro frontend project.
Now is the time to setup the cart
application.
Please run the same commands as what we did with the products
Here are the different
cart/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
mode: "development",
devServer: {
port: 8082,
},
plugins: [
new ModuleFederationPlugin({
name: 'cart',
filename: 'remoteEntry.js',
exposes: {
'./CartShow': './src/index'
}
}),
new HtmlWebpackPlugin({
template: './public/index.html'
})
]
}
cart/src/index.js
import faker from "faker";
const cartText = `<div>You have ${faker.random.number()} items in your cart</div>`;
document.querySelector('#cart-dev').innerHTML = cartText
cart/public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cart</title>
</head>
<body>
<div id="cart-dev"></div>
</body>
</html>
Inside the container
let's add some updates to integrate the cart
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Container</title>
</head>
<body>
<div id="dev-products"></div>
<div id="cart-dev"></div>
</body>
</html>
bootstrap.js
import 'products/ProductsIndex';
import 'cart/CartShow';
console.log("Hi from Container");
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
mode: "development",
devServer: {
port: 8080,
},
plugins: [
new ModuleFederationPlugin({
name: 'container',
remotes: {
products: 'products@http://localhost:8081/remoteEntry.js',
cart: 'cart@http://localhost:8082/remoteEntry.js'
}
}),
new HtmlWebpackPlugin({
template: './public/index.html'
})
]
}
Done.
Let's get back to the main point of this article by looking at the problem we have now.
We have to load 2 files called vendors-node_modules_faker...
In order to run our Container
application.
Because both products
and cart
import the faker
module, now they both have to load their own copy of it.
This is definitely NOT GOOD. Especially because the faker
module itself is very large (2.9MB). Can we do it better?
We want products
and cart
use the same copy of faker
How can we achieve that?
Container
fetchesProducts
remoteEntry.js fileContainer
fetchesCart
remoteEntry.js fileContainer
notice that BOTH requireFaker
moduleContainer
can choose to load ONLY ONE COPY from eitherCart
orProduct
SINGLE COPY OF
faker
is made available to BOTHCart
andProduct
Of course, we don't have to do all of this ourselves.
We just need to change the Webpack configuration like the following
product/webpack.config.js
...
new ModuleFederationPlugin({
name: 'products',
filename: 'remoteEntry.js',
exposes: {
'./ProductsIndex': './src/index'
},
// ADD THIS LINE
shared: ['faker']
}),
...
cart/webpack.config.js
...
new ModuleFederationPlugin({
name: 'cart',
filename: 'remoteEntry.js',
exposes: {
'./CartShow': './src/index'
},
// ADD THIS LINE
shared: ['faker']
}),
...
Now restart the server of all 3 apps.
We only have to load 1 copy of faker
now. Yay!
But... we have a problem.
The product
is running on isolation at localhost:8081
have a trouble
Here is the caveat
When we mark a module as a shared module. This causes the module default to be asynchronously loaded.
That's why we're having this error when running product
alone.
At products/src/index.js
we try to make use of faker
when it's not ready yet.
Use Async Script loading to fix the error.
Now, please navigate to products/src
and create a bootstrap.js
file.
We used this pattern before to asynchronously load things.
It also gives Webpack the opportunity to figure out what all of our different files actually need to run successfully.
src/bootstrap.js
import faker from 'faker';
let products = '';
for (let i = 0; i < 5; i++) {
const name = faker.commerce.productName();
products += `<div>${name}</div>`
}
document.querySelector("#dev-products").innerHTML = products;
src/index.js
import('./bootstrap')
Everything in Products
back to work as expected.
Let's repeat the process for our cart
as well. I will not do it here because it will take this article a bit longer.
Shared Module Version?
If 2 app Cart
and Product
have slightly different versions of faker
.
The Module Federation plugin will still just load 1 copy of faker
.
If one version is way higher or lower than the other, it will load 2 distinct copies.
Singleton Loading
faker
is the module we can load them several times in the browser without any error.
But not every shared module working exactly the same.
For instance, if we try to load 2 copies (or more) react
- you will get an error.
In this scenario, we just want to load exactly 1 copy.
We can change the Webpack configuration
shared: {
react: {
singleton: true,
}
}
By doing this, the browser will load exactly one copy of react
No matter the versions of sub-applications.
Of course, you will receive an error message like Unsatisfied version...
if the difference in dependencies between sub-apps is too high.
More Refactoring
Right now we have some problems.
In the product/src/bootstrap.js
and cart/src/bootstrap.js
document.querySelector("#dev-products").innerHTML = products;
document.querySelector('#cart-dev').innerHTML = cartText
We're now assuming that we have a container with id cart-dev
and dev-products
In fact, it's fine when Cart
and Products
teams running their application during development in isolation.
To better illustrate, if inside the Container
app, in development
or production
we cannot find any elements with the exact same id
as the Cart
and Products
team defined inside their codebase? It's an issue.
That's why we should do refactoring and make sure it worked both for the 2 below contexts.
Firstly, if we run products/src/bootstrap.js
in development
in ISOLATION.
We're using the products/public/index.html
file, therefore, definitely has an element with id
of dev-products
. Similarly for the cart/public/index.html
with an element with id
of cart-dev
In this case, we immediately render our app into that element.
Please make these changes to your projects
products/src/bootstrap.js
import faker from 'faker';
function productMount(el) {
let products = '';
for (let i = 0; i < 5; i++) {
const name = faker.commerce.productName();
products += `<div>${name}</div>`
};
el.innerHTML = products;
}
// Context 1: Running the product in isolation in development
if (process.env.NODE_ENV === 'development') {
const el = document.querySelector('#dev-products');
// ASUMMING our container doesnt have an element with id 'dev-products'
if (el) {
productMount(el);
}
}
// Context 2: Running this file in Development or production through CONTAINER (HOST) app
export {productMount}
Here we encapsulate the logic of rendering into its own function productMount
If we running product
application alone, we can just query the root element, and pass it into productMount(el)
as an argument.
Secondly, we're running these bootstrap.js
files in development
and production
through the Container
(HOST)
There is NO guarantee that an element with id cart-dev
or dev-products
will be there.
We don't want to render the application immediately.
In this case, we're exposing the productMount
for the Container
to decide the target element (In the HOST) it should be rendered into.
Let's make more changes
In products/webpack.config.js
new ModuleFederationPlugin({
name: 'products',
filename: 'remoteEntry.js',
exposes: {
// CHANGE THIS LINE
'./ProductsIndex': './src/bootstrap'
},
shared: ['faker']
}),
Previously, we exposed the entire index.js
file, this file will asynchronously load entire content of the bootstrap.js
Now, we're exposing export {productMount}
from bootstrap.js
, that's why here we want to expose this file.
Let's change the id
of target elements to render Cart
and Products
content inside container/public/index.html
to differentiate them.
<body>
<div id="my-products"></div>
<div id="my-cart"></div>
</body>
Now, inside the container/src/bootstrap.js
import {productMount} from 'products/ProductsIndex';
import 'cart/CartShow';
productMount(document.getElementById('my-products'));
Yay! We're done with the Product
.
Please repeat the same with the Cart
In cart/webpack.config.js
new ModuleFederationPlugin({
name: 'cart',
filename: 'remoteEntry.js',
exposes: {
'./CartShow': './src/bootstrap'
},
shared: ['faker']
}),
cart/src/bootstrap.js
import faker from 'faker';
function cartMount(el) {
const cartText = `<div>You have ${faker.random.number()} items in your cart</div>`;
el.innerHTML = cartText;
}
if (process.env.NODE_ENV === 'development') {
const el = document.querySelector('#cart-dev');
if (el) cartMount(el);
}
export {cartMount}
container/src/bootstrap.js
import {productMount} from 'products/ProductsIndex';
import {cartMount} from 'cart/CartShow';
productMount(document.getElementById('my-products'));
cartMount(document.getElementById('my-cart'))
Our application worked fine after refactoring.
That's it for this article! Thanks for reading.
__
Image source: Unsplash.com
Reference source: Stephen Grider
Subscribe to my newsletter
Read articles from Howard directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Howard
Howard
🙋♂️ I'm a Software Engineer, Voracious Reader, Writer (tech, productivity, mindset), LOVE Fitness, Piano, Running.💻 Started coding professionally in 2020 as a full-time Frontend engineer. ⚗️ I make things in the messy world of JS, Computer Science, and Software Engineering more digestible. Or I would like to say “Distilled” 📹 Documenting my learning journey, and working experience along the way. Share how I learn and build my personal projects.