DEV Community

Long Huynh
Long Huynh

Posted on

Building a Micro Frontend Architecture with Vue 3, Vite and Module Federation

Micro frontend architecture is a powerful way to build scalable and modular applications. Instead of having a monolithic frontend, we split it into multiple independent micro frontends (MFEs) that can be developed, deployed, and maintained separately. In this guide, we will walk through a Vue 3 based micro frontend setup using Vite, Module Federation, and Vue Router to create a flexible and scalable system.

This setup is useful when each micro frontend requires its own Vue instance, allowing different teams to work independently while maintaining separation. It also ensures good performance by loading only one additional instance of Vue.

We use Module Federation Runtime and a Route Manifest to dynamically fetch and load MFEs at runtime, reducing initial load times and improving flexibility.

What We Will Be Using

We will be going through this) sample project and see how we can create a host application that dynamically renders micro frontends, each with its own Vue instance.

The project consist of these dependencies:

  • Vue 3 – The frontend framework for both the host and MFEs
  • Vite – A fast build tool that supports Module Federation
  • Vue Router – To handle navigation between micro frontends
  • Module Federation Plugin – For dynamically loading MFEs

Setting Up the Host Application

The host application is responsible for orchestrating the micro frontends, handling routing, and dynamically loading them as needed.

Vite Configuration for the Host

In the host Vite configuration, we don't need much beyond specifying the name, filename, and shared dependencies.

packages/host/vite.config.js

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { federation } from '@module-federation/vite';
import { fileURLToPath, URL } from 'node:url';

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'host',
      filename: 'remoteEntry.js', // Entry point for the host's own remote modules
      shared: ['vue'], // Ensure shared dependencies are listed
    }),
  ],
  server: {
    port: 5010,
  },
});
Enter fullscreen mode Exit fullscreen mode

Initializing Remotes at Runtime

Instead of defining remotes directly in the Vite config, we initialize them at runtime using the Module Federation Runtime.

In the plugin, we need to pass all the remotes that will be fetched at runtime. In a more advanced setup, we could fetch the remote manifest dynamically and initialize them as needed. However, for this example, the remotes are pre-defined in the route manifest.

packages/host/src/plugins/federationPlugin.js

import { init, loadRemote as _loadRemote } from '@module-federation/runtime';

export const federationPlugin = {
  install: (_, remotes) => {
    const mfRemotes = remotes.map((remote) => ({
      name: remote.module,
      entry: remote.entry,
      type: 'module',
    }));

    init({
      name: 'host',
      remotes: mfRemotes,
    });
  },
};

export default federationPlugin;

export const useModuleFederation = () => {
  const loadRemote = async (module) => {
    const loadedModule = await _loadRemote(`${module}/entry`);
    return {
      name: module,
      mount: loadedModule.mount,
      unmount: loadedModule.unmount,
    };
  };
  return { loadRemote };
};
Enter fullscreen mode Exit fullscreen mode

Defining the Route Manifest

The Route Manifest serves as a centralized registry for defining remotes, providing a structured and scalable way to manage micro frontend routes. Instead of manually specifying routes, this manifest allows the system to dynamically determine where to fetch and load MFEs, ensuring clear separation of concerns and easier updates as new micro frontends are introduced. It is crucial to correctly define the module name (e.g., app1) and the entry path (e.g., entry: 'http://localhost:5011/remoteEntry.js') in the remote property to ensure proper remote fetching and integration.

We define two properties here: one that specifies the route details and another that indicates where to fetch and load the corresponding micro frontend.

packages/host/src/router/routeManifest.js

export const routeManifest = [
  {
    route: {
      path: '/app-1',
      name: 'App1',
      meta: { requiresAuth: false },
    },
    remote: { 
      module: 'app1', 
      entry: 'http://localhost:5011/remoteEntry.js' 
    }
  }
];
Enter fullscreen mode Exit fullscreen mode

Setting Up Vue Router in the Host

Now that our route manifest is in place, we can configure the router to handle application routes dynamically. Instead of manually defining which micro frontend to load, the router reads from the manifest and directs all MFE routes to Loader.vue. This allows the system to fetch and mount the correct micro frontend at runtime.

Here, we map the route properties from the manifest to create our routes. If needed, we can extend the manifest to include additional properties like metadata for enhanced route handling.

packages/host/src/router/index.js

import { createRouter, createWebHistory } from 'vue-router';
import routeManifest from '../routeManifest';

const getRoutes = () => {
  return routeManifest.map((mfe) => ({
    path: mfe.route.path,
    name: mfe.route.name,
    meta: mfe.route.meta,
    component: () => import('@/components/Loader.vue'),
  }));
};

const routes = getRoutes();

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;
Enter fullscreen mode Exit fullscreen mode

The Loader Component

The Loader.vue component is responsible for dynamically mounting and unmounting micro frontends (MFEs) as users navigate through the application. It ensures that only one instance of each micro frontend is active at any time, optimizing performance and preventing memory issues.

At its core, Loader.vue consists of a simple div element that serves as a container for dynamically mounted micro frontends.

packages/host/src/components/Loader.vue

<template>
  <div ref="containerRef"></div>
</template>
Enter fullscreen mode Exit fullscreen mode

To ensure the correct micro frontend is loaded based on navigation, Loader.vue listens for route changes, identifies the appropriate micro frontend, and dynamically loads it while unmounting the previous one. This keeps the application responsive and efficient.

Setting Up the App 1 Application

Now that we have the host application configured, let's set up app1, our first micro frontend. This application will be loaded dynamically by the host at runtime using Module Federation.

Vite Configuration

To enable Module Federation, we need to configure vite.config.js for app1. This will allow it to expose its components and functionality to the host application.

In our route manifest, we have defined the remote configuration, which should correspond to app1. As shown here, the name is app1, and it is configured to be served on port 5011. This means that when app1 is running, the host application can dynamically access its remoteEntry.js file at http://localhost:5011/remoteEntry.js, enabling seamless integration between the host and the micro frontend.

packages/app-1/vite.config.js

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { federation } from '@module-federation/vite';

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'app1',
      filename: 'remoteEntry.js',
      exposes: {
        './entry': './src/entry.js',
      },
      shared: ['vue'],
    }),
  ],
  server: {
    port: 5011,
  },
});
Enter fullscreen mode Exit fullscreen mode

Explanation of the Configuration

  • name: 'app1' – Identifies this micro frontend as app1.

  • filename: 'remoteEntry.js' – Defines the entry point for the remote module.

  • exposes: { './entry': './src/entry.js' } – Exposes the entry.js file so the host can import and mount it.

  • shared: ['vue'] – Ensures Vue is shared to prevent multiple instances.

  • server.port: 5011 – Runs app1 on port 5011.

Entry File for App 1

The entry file is responsible for mounting the micro frontend. It provides the necessary lifecycle methods (mount and unmount) so that the host application can dynamically load and unload app1.

packages/app-1/src/entry.js

import { createApp } from 'vue';
import App from './App.vue';

let appInstance = null;

export function mount(container) {
  appInstance = createApp(App);
  appInstance.mount(container);
}

export function unmount() {
  if (appInstance) {
    appInstance.unmount();
    appInstance = null;
  }
}
Enter fullscreen mode Exit fullscreen mode

Running Both The Host and App 1

To run both application we can go to each directory and run npm run dev. This will start the host on port 5010, and app1 on port 5011, making it accessible for the host to load dynamically at runtime.

Conclusion

This guide walks through setting up a Vue 3-based micro frontend architecture using Vite and Module Federation. We covered:

  1. Setting up the host app with Vite config and Route Manifest

  2. Configuring Vue Router to dynamically load MFEs

  3. Using Module Federation Runtime Plugin to initialize remotes dynamically

By initializing remotes at runtime instead of defining them in the Vite config, we gain more flexibility in managing MFEs and reduce unnecessary dependencies in the host application.

Check out the full GitHub Repository) for a working example.

Top comments (0)