Share Dependencies Between Micro FE Apps

HowardHoward
6 min read

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?

  1. Container fetches Products remoteEntry.js file

  2. Container fetches Cart remoteEntry.js file

  3. Container notice that BOTH require Faker module

  4. Container can choose to load ONLY ONE COPY from either Cart or Product

  5. SINGLE COPY OF faker is made available to BOTH Cart and Product

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

1
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.