Unisupport

React-Vue Micro-Frontend Application Using Webpack 5 Module Federation

This is a short guide to getting started with a micro-frontend application. The article focuses more on the webpack configuration and startup code for building and running the app. We will be using Nx to architect, test, and build the application code. We will have three micro-frontend apps:

  • Container App (Host) — Responsible for loading other micro-frontend apps.
  • Auth App (Remote) — Covering simple authentication flows
  • Dashboard App (Remote) — Showing dashboard for user

Container and Auth apps will be built using React whereas the Portal app uses Vue.js. If you want to directly jump to code, visit this Github Repo

First, let’s look briefly at what micro frontend and webpack module federation are.

What are Micro-frontends ??

To keep it simple and brief — “A collection of small apps that are developed independently but can be dynamically plugged into the main app”

It is a design approach in which a monolithic frontend app is decomposed into individual, semi-independent “micro-apps” working loosely together.

Webpack 5 Module Federation — A Game Changer

The following article explains Webpack Module Federation and how it helps in designing Micro-frontend architecture. It is written by Zack Jackson, Maintainer of Webpack and Co-Creator of Module Federation. We are using Nx 13 to generate the application structure and boilerplate code.

You can follow the Nx official documentation for commands and available options.

Step 1: Generate Nx Workspace

Generate an Nx workspace with React preset, follows the steps on the console and it will automatically create an application and other necessary configuration files

npx create-nx-workspace@latest --preset=react

Step 2: Add micro-frontend apps

Add an Auth and Dashboard app using an Nx generate application utility

Run the following commands:

npx nx g application auth
npx nx g application dashboard

Step 3: Custom Webpack configuration for host and remote app

Nx manages the webpack configuration by itself to run the project, however we will extend the webpack configuration file and add module federation configuration to it.

Container webpack configuration file looks like this

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const { dependencies } = require('../../../package.json');

module.exports = (config, context) => {
  config.plugins.push(
    new ModuleFederationPlugin({
      name: 'container',
      remotes: {
        auth: 'auth@//localhost:8082/remoteEntry.js',
        dashboard: 'dashboard@//localhost:8083/remoteEntry.js',
      },
      shared: {
        ...dependencies,
      },
    })
  );
  config.output.publicPath = 'http://localhost:8081/';
  config.module.rules = [
    {
      test: /\.m?js/,
      resolve: {
        fullySpecified: false,
      },
    },
    {
      test: /\.(js|tsx|ts)$/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: [
            '@babel/preset-react',
            '@babel/preset-env',
            '@babel/preset-typescript',
          ],
          plugins: ['@babel/plugin-transform-runtime'],
        },
      },
    },
  ];

  return config;
};

This file resides on the following path in the GitHub repo

apps/container/config/webpack.dev.js

Other micro-frontend apps configuration

Auth App Configuration (React App)

apps/auth/config/webpack.dev.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const { dependencies } = require('../../../package.json');

module.exports = (config, context) => {
  config.context = process.cwd();
  config.plugins.push(
    new ModuleFederationPlugin({
      name: 'auth',
      filename: 'remoteEntry.js',
      exposes: {
        './AuthApp': 'apps/auth/src/bootstrap',
      },
      shared: {
        ...dependencies,
      },
    })
  );
  config.optimization.runtimeChunk = false;
  config.output = {
    uniqueName: 'auth',
    publicPath: 'http://localhost:8082/',
    clean: true,
  };
  config.module.rules = [
    {
      test: /\.m?js/,
      resolve: {
        fullySpecified: false,
      },
    },
    {
      test: /\.(js|tsx|ts)$/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: [
            '@babel/preset-react',
            '@babel/preset-env',
            '@babel/preset-typescript',
          ],
          plugins: ['@babel/plugin-transform-runtime'],
        },
      },
    },
  ];

  return config;
};

Dashboard App Configuration (Vue App)

apps/dashboard/config/webpack.dev.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const { VueLoaderPlugin } = require('vue-loader');
const { dependencies } = require('../../../package.json');

module.exports = (config, context) => {
  config.context = process.cwd();
  config.plugins.push(
    new ModuleFederationPlugin({
      name: 'dashboard',
      filename: 'remoteEntry.js',
      exposes: {
        './DashboardApp': 'apps/dashboard/src/bootstrap',
      },
      shared: {
        ...dependencies,
      },
    }),
    new VueLoaderPlugin()
  );
  config.optimization.runtimeChunk = false;
  config.output = {
    uniqueName: 'dashboard',
    publicPath: 'http://localhost:8083/',
    filename: '[name][contenthash].js',
    clean: true,
  };
  config.devServer.headers = { 'Access-Control-Allow-Origin': '*' };
  config.resolve.extensions = ['.vue', '.js'];
  config.module.rules = [
    {
      test: /\.(png|jpe?g|gif|woff|svg|eot|ttf)$/i,
      use: [{ loader: 'file-loader' }],
    },
    {
      test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
      use: ['file-loader'],
    },
    {
      test: /\.vue$/,
      use: 'vue-loader',
    },
    {
      test: /\.scss|\.css$/,
      use: ['style-loader', 'css-loader', 'sass-loader'],
    },
  ];

  return config;
};

The next step is to point our custom webpack configuration file for each app. You can add this setting in the build options of the project.json file.

blog.png

Step 4: Application code

You can add code to each app and design it with respect to your use case.

To keep it simple, I have added simple auth flows — SignIn and Signup screens in auth app, whereas some charts and statistics in the dashboard app.

In addition to that, managing the root level routing in the container app — which will determine when and where to load other micro-frontends and app-specific routing in each app entry file “app.tsx”

Step 5: Run apps separately

The beauty of micro-frontend is that we can run each app separately and also through a container/shell app.

We can run auth app like this

npx nx run auth:serve

Auth app running at following URL

[http://localhost:8082/auth/signin](http://localhost:8082/auth/signin)

For dashboard app

npx nx run dashboard:serve

Access the app at http://localhost:8083/dahsboard

Step 6: Run all apps using Container

We can run multiple projects simultaneously using the nx run-many command.

nx run-many --parallel --maxParallel=3 --target=serve --projects=container,auth,dashboard

Access the container by hitting the following URL

http://localhost:8081/

Here, the container will dynamically load other micro-frontend apps which is app and dashboard app in our case on run time.

Next Items

This is a very simple example of getting started and running micro-frontend application, but there are a lot of items to set up for making production ready.

Following are the items:

  • Production setup for Mirco-frontend app
  • Dockerized application
  • CI/CD pipeline

Github Repo

You can check out the code, configurations, and other stuff in this GitHub repo.

Leave a Comment

Your email address will not be published. Required fields are marked *