Информация в чистом виде - это не знание. Настоящий источник знания - это опыт.
Приветствую всех читателей, что забрели на эту страницу. Вероятно, вы тоже как и я, не нашли должной информации по этой теме, поэтому наслаждайтесь, ведь тут будет вся нужная информация для корректной работы runtime импорта!
Небольшая предыстория, для чего написана эта статья
Погрузившись в работу с Module Federation, я столкнулся с такой проблемой, как отсутствие информации для продвинутых разработчиков. Большинство информации, что я встречал была либо про такую технологию как React, либо Angular. Но примеров с Vue как таковых я не нашел, только самые простые, что конечно же, для продвинутых не катит :) Поэтому пришлось разобраться во всем самому и спустя тысячи проб и ошибок, я наконец то доделал данную задачу и готов рассказать, что же такого волшебного кроется за динамическим импортом во Vue.
Вкратце о Module Federation
Module Federation - это подход в разработке приложений, представленный веб-стандартом, который позволяет разделить приложение на отдельные модули, которые могут быть разработаны, развернуты и подключены независимо друг от друга. Он позволяет комбинировать различные модули и приложения, создавая масштабируемые и гибкие архитектуры.
Основные принципы Module Federation:
Независимость модулей: Каждый модуль является отдельным независимым приложением, которое может быть разработано и развернуто отдельно от других модулей.
Динамическая загрузка модулей: Модули могут быть загружены и подключены динамически во время выполнения. Это позволяет эффективно использовать ресурсы и уменьшает начальную загрузку приложения.
Обмен данными и функциональностью: Модули могут обмениваться данными и предоставлять свою функциональность другим модулям. Это позволяет создавать гибкие и расширяемые приложения.
Управление зависимостями: 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; }
Данная функция возвращает объект вида:

Невооруженным глазом видно, что это какая то штука, относящаяся к компоненту, но вот что с ней делать? Ответ прост - передадим этот объект динамическому компоненту из 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 порту):

Весь исходный код можете посмотреть на моем гитхабе
На этом данная статья окончена, надеюсь она была полезна для вас!
