Module Federation using ViteJS

Module Federation using ViteJS

Step into the Micro-frontend architecture

ยท

8 min read

For backend developers, the microservice architecture represents a software development approach composed of small, independent services communicating over well-defined APIs. A similar concept for frontend applications is Micro-Frontends. This architectural pattern shares similarities and differences, akin to Microservices, specifically designed for front-end development. One crucial implementation tool enabling this architecture is Module Federation.

What is Module Federation?

Module federation is a concept where different builds come together to make one application. These separate builds act like containers and can expose and consume code between builds, creating a single, unified application. They are also termed as remote modules.

Local modules are standard modules within the current build, while remote modules, not part of the current build, are loaded at runtime from a remote container. Loading remote modules operates as an asynchronous operation.

Module Federation is unidirectional, involving one host while allowing an application to consume multiple remote builds. Each build acts as a container and can consume other builds as containers. This way, each build can access any other exposed module by loading it from its container.

Module Federation was introduced by Zack Jackson, a Webpack maintainer, and became a flagship feature of Webpack 5 in 2020. It serves as a plugin offering flexibility in project development. While Webpack was a popular bundling tool previously used in the Create React App, CRA was deprecated in early 2023, and Vite.js gained popularity as the Next Generation Frontend Tooling. Astro is built on top of Vite.

Today, we'll delve into Module Federation using the Vite Plugin, an excellent tool created by Originjs to develop micro-frontend applications with Vite.


Host and Remote Applications

When using Module Federation, two scopes come into play:

Host: This application typically serves as the primary or container application responsible for integrating and orchestrating multiple micro-frontends or modules. It's the central hub that imports and consumes functionalities or components from "remote" applications.

Remote: These applications are standalone modules or micro-frontends exposing certain functionalities, components, or features. They are self-contained entities developed, deployed, and maintained independently. They provide functionalities that can be consumed by the "host" application or other remotes via Module Federation.

Let's understand these terminologies with the help of an example:

Suppose we have a remote application exposing the Header.tsx file. We've also developed two different host applications โ€” a video listing application and a blog page. Both these applications require a common header component.

Remote Component - Header.tsx

To understand creating and exposing the Header component as a remote application, let's utilize Vite's react + typescript template:

# npm 7+, extra double-dash is needed:
npm create vite@latest remote-header -- --template react-ts

# yarn
yarn create vite remote-header --template react-ts

The above command creates a new react application. Now, let's create the Header component:

// ./src/components/Header.tsx
import React from 'react';
import './Header.css'; 

interface HeaderProps {
  title: string;
  logo?: string;
}

const Header: React.FC<HeaderProps> = ({ logo, title }) => {
  return (
    <header className="navbar">
      <div className="logo-container">{logo?<img src={logo} className="logo react" alt="React logo" />:<small className='logo-text'>Logo</small>}</div>
      <h1 className="title">{title}</h1>
      <div className="login-btn-container">
        <button className="login-btn">Login</button>
      </div>
    </header>
  );
};

export default Header;

The header component accepts the title and logo as props, granting consuming applications the liberty to provide custom title and logo options.

To make this header component truly remote, we expose them using the @originjs/vite-plugin-federation plugin with the Vite config file.

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import federation from "@originjs/vite-plugin-federation";

export default defineConfig({
  plugins: [react(), federation({
    name:"header_app", // any name of your choice
    filename: "remoteHeader.js",
    exposes:{
      "./Header":"./src/components/Header"
      // expose other components if required
    },
    shared: ["react","react-dom"]
  })],
  build: {
    target: "esnext",
    cssCodeSplit: false,
  },
})

Ensure to remember the filename value within this configuration. It represents the manifest for all exposed modules. This value is further referenced within the host application. In this example, only the header component is exposed, although additional components can be added. Share any libraries the module depends on. In this case, the Header component shares React and React-DOM that is included in the shared group.

Finally, a few tweaks are made to the configuration. The code transpiles to ESNext, enabling the use of modern JavaScript features. Additionally, CSS file code splitting is disabled, consolidating all styles within the main bundle.

At this point, let us test how the header component looks.

// Testing the rendering of header component - App.tsx

function App() {

  return (
    <>
    <Header logo={reactLogo} title='Header Component'/> {/* Header with logo */}
    <div className='spacing'/>
    <Header  title='Header Component'/> {/* Header without logo */}
    </>
  )
}

// package.json

"scripts":{
    "dev": "vite --port 5001 --strictPort",
    "build": "tsc && vite build",
    "preview": "vite preview --port 5001 --strictPort"
}

Running yarn dev presents the output. We have a default logo shown if the user doesn't pass any icon.

To create a local build for this component, make sure to run a build first and then preview the component. Observe that we have restricted this application to be deployed on port 5001.

yarn build && yarn preview

Upon execution, the main bundle, containing shared libraries and the exposed header component, becomes accessible at http://localhost:5001/assets/remoteHeader.js. This file is not generated when using the yarn dev command.

๐Ÿ’ก
Observe that all the data is within the assets directory. This remoteHeader file and all of the bundles it references are just JavaScript files. Hence, they should be treated and deployed like assets.

Host Application with Videos.tsx Component

Now that our remote application is ready, let's create the host application and consume the Header component. We'll develop a simple application displaying a list of video cards with title and author details.

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import federation from "@originjs/vite-plugin-federation";

export default defineConfig({
  plugins: [react(), federation({
    name:"video_app",
    remotes:{
      headerApp: "http://localhost:5001/assets/remoteHeader.js",
      // consume other remote components
    },
    shared:["react","react-dom"]
  })],
  build: {
    target: "esnext",
    cssCodeSplit: false,
  },
})

In the Vite configuration, use the @originjs/vite-plugin-federation plugin to define headerApp as the remote points to the served location. Also, specify every shared library the host shares with the remote, ensuring uniformity across applications.

We can now extract and render the Header component as follows:

// App.tsx
import React from 'react';
import VideoList from './components/VideoList';
import { dataVideos } from './utils/sampleData';
import customLogo from './assets/react.svg'
import Header from "headerApp/Header";

const App: React.FC = () => {

  return (
    <div>
      <Header logo={customLogo} title="My Videos"/>
      <VideoList videos={dataVideos} />
    </div>
  );
};

export default App;

The Header component is imported from headerApp/Header, accepting a custom logo and title string. The video list is rendered within the <VideoList> component.


Consuming Remote in a Webpack Application

Now that we have learned how to use module federation in Vite applications, let's understand how this remote component can be consumed by a host application created using Webpack.

Host Component - Blogs.tsx (Webpack Application)

// webpack.config.js

// ...rest of the config details

config.target: "es2020",
  experiments: {
    outputModule: true,
  },

config.plugins: [
    new ModuleFederationPlugin({
      name: "host_wp_blogs",
      library: { type: "module" },
      filename: "remoteHeader.js",
      remotes: {
        headerModule: "http://localhost:5001/assets/remoteHeader.js",
      },
      exposes: {},
      shared: {
        ...deps,
        react: {
          singleton: true,
          requiredVersion: deps.react,
        },
        "react-dom": {
          singleton: true,
          requiredVersion: deps["react-dom"],
        },
      },
    }),
    new HtmlWebPackPlugin({
      template: "./index.ejs",
      inject: false,
    }),
  ],

The remote component remoteHeader.js has been consumed within the Webpack's ModuleFederationPlugin. Note that we have named this remote as headerModule. Considering that the remote application is built with ViteJS, include library: { type: "module" } in the module federation settings as Vite works on ECMAScript modules. Additionally, set up config.target to support modules. These changes ensure the proper linkage between the remote application built with ViteJS and the Webpack setup.

import React from "react";
import ReactDOM from "react-dom";

import "./index.css";
import BlogList from "./components/BlogList";
import { dataBlogs } from "./utils/sampleData";
import logo from "./assets/react.svg";
import HeaderModule from "headerModule/Header";
const Header = HeaderModule.default;

const App = () => (
  <div className="container">
    <Header logo={logo} title="My Blogs" />
    <BlogList blogs={dataBlogs} />
  </div>
);
ReactDOM.render(<App />, document.getElementById("app"));

In this scenario, the Header is imported as a module, accessing the default export assigned to a variable named Header.


Further options for extending the use of Module Federation

What an enriching learning experience! We've successfully created a remote module and integrated it into host applications. This example serves as a foundation for various extensions and adaptations.

In this scenario, the Video listing application (host) consumes two distinct remote components - Header and Subscribe Button. Interestingly, the same button component is utilized by yet another application.

Going a step further:

The header component serves a dual role as both a remote and a host application. It functions as a remote component for the Videos and Blogs application, while also serving as a host to consume the remote Login component.

Numerous possibilities exist for expanding the utilization of module federation in real-world scenarios. I would be interested in knowing the specifics of your implementation. Please share them in the comments section.


Conclusion

In this exploration of Micro-frontend Architecture through Module Federation using ViteJS, we've delved into a groundbreaking approach. Understanding the intricate dynamics between host and remote applications, we've unlocked the potential for seamless integration and autonomy.

This journey has shown how breaking down tasks into separate, cohesive parts can create strong, unified applications. As we wrap up, it's clear that the potential for innovative and scalable front-end development in Micro-frontend Architecture knows no bounds.

Thank you for joining me on this exploration today. Remember, continuous learning and adaptation are key in the ever-evolving landscape of software development.

Feel free to refer to my Github repo and show your support by starring it.

I hope you were able to take away something new and useful from this article! If you did, please drop a like on this post. ๐Ÿ™‚

You can connect with me on Twitter, LinkedIn, and GitHub โœจ

Peace โœŒ๏ธ

References