Информация в чистом виде - это не знание. Настоящий источник знания - это опыт.
Приветствую всех читателей, что забрели на эту страницу. Вероятно, вы тоже как и я, не нашли должной информации по этой теме, поэтому наслаждайтесь, ведь тут будет вся нужная информация для корректной работы 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 порту):
Весь исходный код можете посмотреть на моем гитхабе
На этом данная статья окончена, надеюсь она была полезна для вас!
serginho
Не знаю что там в вебпаке, но в esbuild можно сделать module federation просто заменив импорты в начале страницы на импорты в коде типа:
const someLib = await import('some-lib');