Динамический импорт remote компонента Module Federation на Vue 3

от автора

Информация в чистом виде — это не знание. Настоящий источник знания — это опыт.

Приветствую всех читателей, что забрели на эту страницу. Вероятно, вы тоже как и я, не нашли должной информации по этой теме, поэтому наслаждайтесь, ведь тут будет вся нужная информация для корректной работы runtime импорта!

Небольшая предыстория, для чего написана эта статья

Погрузившись в работу с Module Federation, я столкнулся с такой проблемой, как отсутствие информации для продвинутых разработчиков. Большинство информации, что я встречал была либо про такую технологию как React, либо Angular. Но примеров с Vue как таковых я не нашел, только самые простые, что конечно же, для продвинутых не катит 🙂 Поэтому пришлось разобраться во всем самому и спустя тысячи проб и ошибок, я наконец то доделал данную задачу и готов рассказать, что же такого волшебного кроется за динамическим импортом во Vue.

Вкратце о Module Federation

Module Federation — это подход в разработке приложений, представленный веб-стандартом, который позволяет разделить приложение на отдельные модули, которые могут быть разработаны, развернуты и подключены независимо друг от друга. Он позволяет комбинировать различные модули и приложения, создавая масштабируемые и гибкие архитектуры.

Основные принципы Module Federation:

  1. Независимость модулей: Каждый модуль является отдельным независимым приложением, которое может быть разработано и развернуто отдельно от других модулей.

  2. Динамическая загрузка модулей: Модули могут быть загружены и подключены динамически во время выполнения. Это позволяет эффективно использовать ресурсы и уменьшает начальную загрузку приложения.

  3. Обмен данными и функциональностью: Модули могут обмениваться данными и предоставлять свою функциональность другим модулям. Это позволяет создавать гибкие и расширяемые приложения.

  4. Управление зависимостями: Module Federation позволяет явно управлять зависимостями между модулями. Каждый модуль может указывать, какие модули и версии он требует для своей работы.

Module Federation особенно полезен в среде микросервисной архитектуры и распределенной разработки, где разные команды могут независимо разрабатывать и подключать свои модули к общему приложению.

В контексте фронтенд-разработки, Module Federation стал широко используемым в связке с инструментами, такими как webpack, для создания масштабируемых и гибких микросервисных архитектур на стороне клиента.

Поэтапно напишем Host и Remote приложения

Задумка следующая:
Host — делится своим компонентом Content и будет лежать он на порту 3002. Далее запускаем Remote приложение, ждем пока пользователь введет нужный порт в инпут, далее подгружаем компонент, если такой существует. Profit!

Немного конфигурации:
1) webpack.config.js — описывать в принципе нечего, базовая структура для 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) Настройки для package.json:

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

На этом наш хост готов к использованию. Нужно только подхватить 3002 порт и должным образом обработать.

Теперь конфигурация для Remote приложения:

1) webpack.config.js:

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

2) Layout.vue. Тут я разберу немного подробнее, т.к. в этом компоненте находятся ключевые функции для работы программы. В чем заключается алгоритм на данный момент:

  • Есть инпут, с привязанной к нему переменной port

  • Введя порт можем нажать на кнопку, по которой запускается функция, подхватывающая тот manifest по введенному порту

  • Пытаемся создать скрипт из этого манифеста и подсоединить его к нашему приложению

  • Как только скрипт загрузился — можем забрать оттуда объект и прикрутить его к динамическому компоненту

3) package.json:

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

Перейдем к коду:

Форма с инпутом выглядит следующим образом:

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

В принципе, осталось написать функцию getRemoteComponent и готово. Опишем тело функции:

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

Далее, нужно создать скрипт, который будет подключаться к приложению:

const moduleScope = 'home' // Переменные для дальнейшей конфигурации const moduleName = 'Content'  const element = document.createElement('script'); element.type = 'text/javascript'; element.async = true; element.src = remoteURL;

Если произошла ошибка, обработаем ее следующим образом:

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

Если же скрипт успешно загрузился, то можем его обработать, но для его написания потребуется еще одна функция, которая есть в документации 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;   }

Данная функция возвращает объект вида:

Возвращенное значение loadModule

Возвращенное значение loadModule

Невооруженным глазом видно, что это какая то штука, относящаяся к компоненту, но вот что с ней делать? Ответ прост — передадим этот объект динамическому компоненту из Vue и оно магическим образом соберет этот компонент!

Теперь все таки вернемся к обработке скрипта:

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

Вот и все! Наша работа на этом закончена, осталось только приписать этот объект в компонент следующим образом:

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

Таким образом задача решена, можем радоваться 🙂
Итоговый код компонента 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> 

Результат получился следующий (напомню, что хост раздает компонент по 3002 порту):

Финал темы

Финал темы

Весь исходный код можете посмотреть на моем гитхабе

На этом данная статья окончена, надеюсь она была полезна для вас!


ссылка на оригинал статьи https://habr.com/ru/articles/747384/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *