Dynamic remote import of Module Federation component on Vue 3

Pure information is not knowledge. The real source of knowledge is experience.

Greetings to all readers that wandered into this page. Probably, you, like me, have not found the proper information on this topic, so enjoy, because there will be all the information you need for the runtime import to work correctly!

A little background on why this article was written

Immersed in work with Module Federation, I encountered such a problem as the lack of information for advanced developers. Most of the information that I met was either about technology such as React or Angular. But examples from vue as such, I didn’t find it, only the simplest ones, which, of course, doesn’t work for advanced ones 🙂 Therefore, I had to figure it out on my own and after thousands of trial and error, I finally completed this task and am ready to tell you what is so magical behind the dynamic import to Vue.

Briefly about Module Federation

Module Federation is an application development approach introduced by the web standard that allows you to divide an application into separate modules that can be developed, deployed and connected independently of each other. It allows you to combine different modules and applications to create scalable and flexible architectures.

Basic principles Module Federation:

  1. Module Independence: Each module is a separate independent application that can be developed and deployed separately from other modules.

  2. Dynamic loading of modules: Modules can be loaded and connected dynamically at runtime. This allows efficient use of resources and reduces the initial load of the application.

  3. Exchange of data and functionality: Modules can communicate and provide their functionality to other modules. This allows you to create flexible and extensible applications.

  4. Dependency Management: Module Federation allows you to explicitly manage dependencies between modules. Each module can specify which modules and versions it requires in order to function.

Module Federation is especially useful in a microservice architecture and distributed development environment where different teams can independently develop and plug their modules into a common application.

In the context of front-end development, Module Federation has become widely used in conjunction with tools such as webpack to create scalable and flexible client-side microservice architectures.

Let’s write Host and Remote applications step by step

The idea is the following:
Host – shares its Content component and it will lie on port 3002. Next, launch the Remote application, wait until the user enters the desired port into the input, then load the component, if one exists. Profit!

A bit of configuration:
1) webpack.config.js – there is nothing to describe in principle, the basic structure for the module federation plugin

...
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
    }),
    new ModuleFederationPlugin({
      name: 'home',
      filename: 'remoteEntry.js',
      exposes: {
        './Content': './src/components/Content',
      },
      shared: {
        vue: {
          singleton: true,
        },
      },
    }),
...
});

2) content.vue:

<template>
  <div style="color: #d9c1e4;">{{ title }}</div>
</template>
<script>
export default {
  data() {
    return {
      title: "Remote content component",
    };
  },
};
</script>

3) app.vue

<template>
  <main class="main">
    <h3>Host App</h3>
    <Content />
  </main>
</template>

<script>
import { ref, defineAsyncComponent } from "vue";
export default {
  components: {
    Content: defineAsyncComponent(() => import("./components/Content")),
  },
  setup() {
    const count = ref(0);
    const inc = () => {
      count.value++;
    };

    return {
      count,
      inc,
    };
  },
};
</script>

<style>
/* Немного стилей */
@import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap');

img {
  width: 200px;
}
h1 {
  font-family: Arial, Helvetica, sans-serif;
}

html,body {
  margin: 0;
}

h3 {
  margin: 0;
  color:#d9c1e4;
}

.main{
  height: 100vh;
  background: gray;
  display: flex;
  flex-direction: column;
  justify-content: center;
  font-family: 'Montserrat', sans-serif;
  align-items: center;
  color: #fff;
}
</style>

4) Settings for package.json:

"scripts": {
    "start": "webpack-cli serve",
    "serve": "serve dist -p 3002",
    "build": "webpack --mode production",
    "clean": "rm -rf dist"
  },

With this, our host is ready to use. You just need to pick up port 3002 and process it properly.

Now the configuration for the Remote application:

1) webpack.config.js:

...
    new ModuleFederationPlugin({
      name: 'layout',
      filename: 'remoteEntry.js',
      exposes: {},
      shared: {
        vue: {
          singleton: true,
        },
      },
    }),
...

2) layout.vue. Here I will analyze a little more, because. this component contains the key functions for the program to work. What is the current algorithm?

  • There is an input with a variable attached to it port

  • By entering the port, we can click on the button that launches the function that picks up that manifest on the entered port

  • We are trying to create a script from this manifest and connect it to our application

  • As soon as the script has loaded, we can take the object from there and attach it to the dynamic component

3) package.json:

 "scripts": {
    "start": "webpack-cli serve",
    "serve": "serve dist -p 3001",
    "build": "webpack --mode production",
    "clean": "rm -rf dist"
  },

Let’s move on to the code:

The input form looks like this:

<div class="component">
    <p style="font-size:22px; margin-bottom: 5px">Layout App</p>
    <div class="form">
    <label>Enter port for loading</label>
    <input type="text" v-model="port">
    <button @click="getRemoteComponent">Get remote component</button>
</div>

Basically, it remains to write a function getRemoteComponent and done. Let’s describe the body of the function:

// Для начала зададим конфигурацию для запроса 
const uiApplication = {
   protocol: 'http',
   host: 'localhost',
   port: this.port,
   fileName: 'remoteEntry.js'
}
// Теперь построим ссылку
const remoteURL = `${uiApplication.protocol}://${uiApplication.host}:${uiApplication.port}/${uiApplication.fileName}`;

Next, you need to create a script that will connect to the application:

const moduleScope="home" // Переменные для дальнейшей конфигурации
const moduleName="Content"

const element = document.createElement('script');
element.type="text/javascript";
element.async = true;
element.src = remoteURL;

If an error occurs, we will handle it as follows:

element.onerror = () => {
    alert(`Port ${this.port} doesn't have any content! Try another`)
}

If the script has successfully loaded, then we can process it, but writing it will require one more function, which is in the documentation webpack‘a:

async loadModule(scope, module) {
    await __webpack_init_sharing__('default');
    const container = window[scope];
    await container.init(__webpack_share_scopes__.default);
    const factory = await window[scope].get(module);
    const Module = factory();
    return Module;
  }

This function returns an object of the form:

Return value of loadModule

Return value of loadModule

It can be seen with the naked eye that this is some kind of thing related to the component, but what to do with it? The answer is simple – pass this object to a dynamic component from Vue and it will magically collect this component!

Now let’s get back to script processing:

element.onload = () => {
    const remoteComponent = this.loadModule(moduleScope, `./${moduleName}`)
    remoteComponent.then(res => {
      console.log(res.default);
      this.dynamicComponent = res.default; 
    })
};
document.head.appendChild(element);

That’s all! Our work on this is over, it remains only to attribute this object to the component as follows:

<div class="component">
    <p style="font-size:22px; margin-bottom: 5px">Remote App</p>
    <component :is="dynamicComponent"></component>
</div>

So the problem is solved, we can rejoice 🙂
Final component code layout.vue:

<template>
  <div class="main">
    <div class="component">
      <p style="font-size:22px; margin-bottom: 5px">Layout App</p>
      <div class="form">
        <label>Enter port for loading</label>
        <input type="text" v-model="port">
        <button @click="getRemoteComponent">Get remote component</button>
      </div>
    </div>
    <div class="component">
      <p style="font-size:22px; margin-bottom: 5px">Remote App</p>
      <component :is="dynamicComponent"></component>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      port: null,
      dynamicComponent: null
    }
  },
  methods: {
    getRemoteComponent() {
      console.log(this.port, '<- Подгружаем по порту')

      // Можно конфигурировать любые параметры динамически
      const uiApplication = {
        protocol: 'http',
        host: 'localhost',
        port: this.port,
        fileName: 'remoteEntry.js'
      }

      const remoteURL = `${uiApplication.protocol}://${uiApplication.host}:${uiApplication.port}/${uiApplication.fileName}`;
      console.log(remoteURL)

      const moduleScope="home"
      const moduleName="Content"
      const element = document.createElement('script');
      element.type="text/javascript";
      element.async = true;
      element.src = remoteURL;

      element.onload = () => {
        const remoteComponent = this.loadModule(moduleScope, `./${moduleName}`)
        remoteComponent.then(res => {
          console.log(res.default);
          this.dynamicComponent = res.default;
        })
      };

      element.onerror = () => {
        alert(`Port ${this.port} doesn't have any content! Try another`)
      }

      document.head.appendChild(element);
    },
    async loadModule(scope, module) {
      await __webpack_init_sharing__('default');
      const container = window[scope];
      await container.init(__webpack_share_scopes__.default);
      const factory = await window[scope].get(module);
      const Module = factory();
      return Module;
    }
  }
};
</script>

<style>
@import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap');

* {
  font-family: 'Montserrat', sans-serif;
  color:#fff;
}

body, p {
  margin: 0;
}

.main {
  height: 100vh;
  display: flex;
  background: gray;
}

.component {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  border: 2px solid #ffff;
  padding: 5px;
  border-radius: 10px;
  width: 100%;
}

.form {
  display: flex;
  max-width: 300px;
  flex-direction: column;
}

input {
  margin: 10px 0;
  color:black;
}

button {
  color: black;
}

</style>

The result is the following (let me remind you that the host distributes the component on port 3002):

Topic ending

Topic ending

You can see the entire source code on my github

This concludes this article, I hope it was useful to you!

Similar Posts

Leave a Reply

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