Изучая технологию 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
./RemoteComponent1
plugins: [ 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" />