Alfa Brain

Создаем динамические модули используя Webpack Module Federation


Превью статьи Создаем динамические модули используя Webpack Module Federation

Изучая технологию Webpack Module Federation я задался следующим вопросом: Как подключить удаленный модуль (remote module), не во время сборки, а во время работы нашего JS приложения?

Я воспринимаю remote модуль как коробку, которую можно перенести в любое место и распаковать ее содержимое, и в нашем случае это компоненты React. Моей же задачей стало понять, как запаковать компонент в коробку, а затем положить эту коробку в более крупную и перенести все вместе за один раз?

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

Сперва я использовал <a href="https://gist.github.com/ScriptedAlchemy/3a24008ef60adc47fad1af7d3299a063" target="_blank">способ</a> от самого автора WMF - <a href="https://gist.github.com/ScriptedAlchemy">Zack Jackson</a>. Данный способ показался мне громоздким, но все же работал. Работал до того момента, пока я не попробовал загружать удаленный модуль, внутри другого загружаемого удаленного модуля. Это привело к огромному количеству (более 200) повторных рендеров этих компонентов. Причем, рендер не был циклом и все же заканчивался в какой-то момент. К сожалению я так и не нашел причину такого странного поведения. Но, к счастью мне удалось найти другое решение.

Самым странным для меня, оказалось то, что данный функционал оказался как будто спрятан и о нем нигде не было написано. Теперь я готов поделиться тем что мне удалось "откопать".

Шаги описанные в данной статье

  • Настройка host приложения
  • Динамически загружать скрипт манифеста из удаленного модуля
  • Загрузить компонент из области общего доступа webpack (shared scope)
  • Воспользоваться remote компонентом на host приложении
  • Конфигурации remote приложения
  • Результат
<img src="https://storage.yandexcloud.net/alfa-code-public-bucket/Blog/Images/dynamic-wmf-plan.svg" />

Настройка host приложения

Используйте ModuleFederationPlugin в файле webpack.config.js приложения, которое вы хотите использовать для загружаемых модулей.

Обратите внимание, что запись remotes теперь является пустым объектом. Туда ничего не нужно прописывать. Это единственное изменение, которое вам нужно в конфигурации webpack на хосте.

Если все загружаемые модули будут динамическими (например вы не будете иметь общих библиотек в секции shared), можно вообще удалить плагин ModuleFederationPlugin из настроек хоста. Да, это меня тоже удивило - но это работает - проверено!

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); // добавьте его в вашу конфигурацию webpack plugins: [ new ModuleFederationPlugin({ name: 'hostApp', remotes: { }, shared: { react: { requiredVersion: false, singleton: true, }, }, }), ],

Загружаем манифест динамически

Теперь нам необходимо загрузить манифест remote приложения. Если упростить, то манифест описывает какие удаленные компоненты и как можно загрузить с удаленного сервера. Я создал простой хук используя нативные хуки React для этого. Этот хук создаст элемент script, используя API браузера. Это позволяет динамически загрузить манифест. Хук так же защищает от повторной загрузки данного скрипта.

import React from "react"; const useDynamicScript = (args) => { const [ready, setReady] = React.useState(false); const [failed, setFailed] = React.useState(false); React.useEffect(() => { if (!args.url) { return; } const element = document.createElement("script"); element.src = args.url; element.type = "text/javascript"; element.async = true; setReady(false); setFailed(false); element.onload = () => { console.log(`Dynamic Script Loaded: ${args.url}`); setReady(true); }; element.onerror = () => { console.error(`Dynamic Script Error: ${args.url}`); setReady(false); setFailed(true); }; document.head.appendChild(element); return () => { console.log(`Dynamic Script Removed: ${args.url}`); document.head.removeChild(element); }; }, [args.url]); return { ready, failed }; }; export default useDynamicScript;

Загружаем компонент из области общего доступа webpack (shared scope webpack)

Тут происходит немного магии Webpack Module Federation. Что бы разобраться как это работает, нужно основательно погрузиться в реализацию WMF. Мы этого делать не будем, но скажу что данная технология построена с использованием понятия контейнера. С помощью функции loadComponent мы создаем специальный объект контейнера и загружаем в него remote модуль (наш удаленный компонент).

Чуть ниже при помощи компонента ModuleLoader мы подключаем манифест испльзуя хук useDynamicScript. Далее используем loadComponent вместе с React.lazy для ленивой загрузки.

import React, { Suspense } from "react"; import { useDynamicScript } from '../hooks/use-dynamic-script'; function loadComponent(scope, module) { return async () => { // Эта строчка инициализирует область общего доступа. 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; }; } export function ModuleLoader(props) { const { url, scope, module, ...rest } = props; const { ready, failed } = useDynamicScript(url); if (!module) { return <h2>Не указана система</h2>; } if (!ready) { return <h2>Загрузка динамического скрипта: {url}</h2>; } if (failed) { return <h2>Не удалось загрузить динамический скрипт: {url}</h2>; } const Component = React.lazy( loadComponent(scope, module) ); return ( <Suspense fallback="Loading Module"> <Component {...rest} /> </Suspense> ); };

Используем remote компонент на хосте

Теперь когда мы сделали всю подготовительную работу, наконец можем использовать удаленный компонент. Параметров url, scope, module - удаленного сервера я пропишу тут на месте строкой, но вы можете передавать эти параметры в props (например из конфига приложения) чтобы сделать приложение дествистельно универсальным.

url - адрес удаленного (remote) сервера (вместе с путем до манифеста) - в моем случае он запускается на порту 8082 scope - имя удаленного сервера, из конфига webpack module - название удаленного модуля. Оно должно быть в том же формате, что и в webpack конфигурации remote приложения в секции exposes.

import React, { Suspense, useEffect, useState } from 'react'; import ModuleLoader from './ModuleLoader'; function App() { const url = 'http://localhost:8082/remoteEntry.js'; const scope = 'remoteApp'; const module = './RemoteComponent1'; return ( <> <Suspense fallback={'Loading . . . '}> <ModuleLoader url={remote.url} scope={remote.scope} module={remote.module} /> </Suspense> </> ); } export default App;

Обзор настройки WMF на remote сервере

В конфигурации webpack удаленного модуля:

  • Имя удаленного модуля: remoteApp
  • Открываем наружу компонент с именем: ./RemoteComponent1
plugins: [ new ModuleFederationPlugin({ name: 'remoteApp', filename: 'remoteEntry.js', exposes: { "./RemoteComponent1": "./src/components/RemoteComponent1.js", }, }),

🤯 Результат 🤯

Что же мы получаем в итоге? А то что, теперь мы можем динамически определять сервер, откуда мы будем загружать remote компоненты. В любом месте приложения мы можем определить remote компонент. Более того, этот способ поддерживает вложенные remote компоненты. То есть если загружаемый компонент содержит в самом себе загружаемый компонент - это тоже вполне работает. Дополнительных перерендеров не происходит.

<img src="https://storage.yandexcloud.net/alfa-code-public-bucket/Blog/Images/dwmf.gif" />

Я подготовил два репозитория (host и remote прилложения), чтобы потренироваться с динамическими remote компонентами. Инструкция по пользованию в README.md

  • HOST приложение - https://github.com/Hydrock/wmf-host-dynamic-remote.git
  • REMOTE приложение - https://github.com/Hydrock/wmf-remote-dynamic-remote.git

При написании данной статью я использовал статью <a href="https://dev.to/omher/lets-dynamic-remote-modules-with-webpack-module-federation-2b9m" target="_blank">Let's Dynamic Remote modules with Webpack Module Federation</a>

Не забудь написать комментарий! Спасибо!

<img src="https://i.giphy.com/media/ciwAC8vb2YlC758Wcq/giphy-downsized.gif" />
Поделиться: