Изучая технологию 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 приложенияИспользуйте 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> ); };
Теперь когда мы сделали всю подготовительную работу, наконец можем использовать удаленный компонент.
Параметров 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;
remote сервереВ конфигурации webpack удаленного модуля:
remoteApp./RemoteComponent1plugins: [ new ModuleFederationPlugin({ name: 'remoteApp', filename: 'remoteEntry.js', exposes: { "./RemoteComponent1": "./src/components/RemoteComponent1.js", }, }),
Что же мы получаем в итоге? А то что, теперь мы можем динамически определять сервер, откуда мы будем загружать remote компоненты. В любом месте приложения мы можем определить remote компонент. Более того, этот способ поддерживает вложенные remote компоненты. То есть если загружаемый компонент содержит в самом себе загружаемый компонент - это тоже вполне работает. Дополнительных перерендеров не происходит.
Я подготовил два репозитория (host и remote прилложения), чтобы потренироваться с динамическими remote компонентами. Инструкция по пользованию в README.md
HOST приложение - https://github.com/Hydrock/wmf-host-dynamic-remote.gitREMOTE приложение - 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" />