Статьи DexSys

Как готовить микрофронтенды в Webpack 5

Как готовить микрофронтенды в Webpack 5
Всем привет, я фронтенд-разработчик.
На моём комментарии про микрофронтенды набралось целых три лайка, поэтому я решил написать статью с описанием всех шишек, что наш стрим набил и набивает в результате внедрения микрофронтендов.
Начнём с того, что ребята с Хабра уже писали про Module Federation, так что, моя статья - это не что-то уникальное и прорывное. Скорее, это шишки, костыли и велосипеды, которые полезно знать тем, кто собирается использовать данную технологию.
Причина
Причина, по которой решено было внедрять микросервисный подход на фронте, довольно простая - много команд, а проект один, нужно было как-то разделить зоны ответственности и распараллелить разработку. Как раз в тот момент, мне на глаза попался доклад Павла Черторогова про Webpack 5 Module Federation. Честно, это перевернуло моё видение современных веб-приложений. Я очень вдохновился и начал изучать и крутить эту технологию, чтобы понять, можно ли применить это в нашем проекте. Оказалось, всё что нужно, это дописать несколько строк в конфиг Webpack, создать пару компонентов-хелперов, и... всё завелось.
Настройка
Итак, что же нужно сделать, чтобы запустить микрофронтенды на базе сборки Webpack 5?
Для начала, убедитесь, что используете Webpack пятой версии, потому что Module Federation там поддерживается из коробки.
Настройка shell-приложения
Так как, до внедрения микрофронтендов, у нас уже было действующее приложение, решено было использовать его в качестве точки входа и оболочки для подключения других микрофронтендов. Для сборки использовался Webpack версии 4.4 и при обновлении до 5 версии возникли небольшие проблемы с некоторыми плагинами. К счастью, это решилось простым поднятием версий плагинов.
Чтобы создать контейнер на базе сборки Webpack и при помощи этого контейнера иметь возможность импортировать ресурсы с удаленных хостов добавляем в Webpack-конфиг следующий код:
Теперь нам нужно забутстрапить точку входа в наше приложение, чтобы оно запускалось асинхронно, для этого создаем файл bootstrap.tsx и кладем туда содержимое файла index.tsx
А в index.tsx вызываем этот самый bootstrap
В общем то всё, в таком виде уже можно импортировать ваши микрофронтенды - они указываются в объекте remotes в формате <name>@<адрес хоста>/<filename>. Импортировать модуль подключенный таким образом можно как обычный компонент, так , как будто он находится в локальной папке.
Но нам такая конфигурация не подходит, ведь на момент сборки приложения мы ещё не знаем откуда будем брать микрофронтенд, к счастью, есть готовое решение, поэтому возьмем код из примера для динамических хостов, так как наше приложение написано на React, то оформим хэлпер в виде React-компонента LazyService:
Хук useDynamicScript нужен нам, чтобы в рантайме прикреплять загруженный скрипт к нашему html-документу.
loadComponent это обращение к Webpack-контейнеру, по сути - обычный динамический импорт.
Ну и напоследок опишем тип для нашего микросервиса, дженерик нужен для того, чтобы правильно работала типизация пропсов.
  • url - имя хоста + имя контейнера (например, http://localhost:3002/widgets.js), с которого мы хотим подтянуть модуль
  • scope - параметр name, который мы укажем в удаленном конфиге ModuleFederationPlugin
  • module - имя модуля, который мы хотим подтянуть
  • props - опциональный параметр, если вдруг наш микросервис требует пропсы, нужно их типизировать
Вызов компонента LazyService происходит следующим образом:
В общем-то, по коду видно, что мы можем динамически переключать наши модули, а основной url хранить, например, в конфиге.
Так, с shell-приложением вроде разобрались, теперь нужно откуда-то брать наши модули.
Настройка shell-приложения
Для начала проделываем все те же манипуляции что и в shell-приложении и убеждаемся, что версия Webpack => 5
Настраиваем ModuleFederationPlugin, но уже со своими параметрами, эти параметры указываем при подключении модуля в основное приложение.
В объекте exposes указываем те модули, которые мы ходим отдать наружу, точку входа в приложение так же нужно забутстрапить. Если в микрофронтенде нам не нужны модули с других хостов, то компонент LazyService тут не нужен.
Вот и всё, получен работающий прототип микрофронтенда.
Выглядит круто, работает тоже круто. Общие зависимости не грузятся повторно, версии библиотек рулятся плагином, можно динамически переключать модули, в общем, сказка. Если копать глубже, то это очень гибкая технология, можно использовать её не только с React и JavaScript, но и со всем, что переваривает Webpack, то есть теоретически можно подружить части приложения написанные на разных фреймворках, это конечно не очень хорошо, но сделать так можно. Можно собрать модули и положить на CDN, можно использовать контейнер как общую библиотеку компонентов для нескольких приложений. Возможностей реально много.
Что мы получаем?
Независимую разработку и развёртывание
Каждый микрофронтенд может и должен находиться в отдельном репозитории, это позволят вести независимую разработку, развёртывание и тестирование. Команды не будут завязаны на релизный цикл, а в проекте можно будет использовать те инструменты, которые удобны конкретной команде. Но с этим нужно быть осторожнее, желательно не выходить за пределы стека проекта, чтобы вынести большинство бибилиотек в общий скоуп и не грузить в свой микрофронтенд множество вендорных чанков.
Повышенную отказоустойчивость
Так как приложение теперь состоит из нескольких частей, которые расположены на разных хостах, то при недоступности одного микрофронтенда, основной функционал продолжить работать. Конечно, если shell-приложение будет недоступно, то Module Federation тут не поможет. Ещё можно продублировать критичные микрофронтенды на несколько хостов расположенных в разных локациях и подключать их в порядке приоритета, таким образом, если один хост будет недоступен, подключится следующий и так по порядку.
Проблемы
Когда удалось запустить это в нашем проекте я был доволен, нет, очень доволен, но это длилось недолго, после того как началась реальная работа над микрофронтендами, начали всплывать наши любимые подводные камни, а теперь поговорим он них подробнее.
Потеря контекстов в React-компонентах
Как только понадобилось работать с контекстом библиотеки react-router, то возникли проблемы, при попытке использовать в микрофронтенде хук useLocation, например, приложение вылетало с ошибкой.
Для взаимодействия с бэкендом мы используем Apollo, и хотелось, чтобы ApolloClient объявлялся только единожды в shell-приложении. Но при попытке из микрофронтенда просто использовать хук useQuery, в рантайме приложение вылетало с такой же ошибкой как и для useLocation.
Экспериментальным путём было выяснено, для того чтобы контексты правильно работали, нужно в микрофронтендах использовать версию npm-пакета не выше, чем в shell-приложение, так что за этим нужно внимательно следить.
Дублирование UI-компонентов в shell-приложении и микрофронтенде
Так как разработка ведётся разными командами, есть шанс, что разработчики напишут компоненты с одинаковым функционалом и в shell-приложении и в микрофронтенде. Чтобы этого избежать, есть несколько решений:
  • Выносить UI-компоненты в отдельный npm-пакет и использовать его как shared-модуль
  • "Делиться" компонентами через ModuleFederationPlugin
В принципе, у обоих подходов есть свои плюсы, но мы выбрали первый, потому что так удобнее и прозрачнее управлять библиотекой компонентов. Да и саму технологию Module Federation хотелось использовать как механизм для построения микрофронтендов, а не аналог npm.
Типизация
Если вы ведёте разработку на TypeScript, могут возникнуть сложности с типизацией микрофронтендов, потому что в Module Federation пока нет механизма, с помощью которого можно было бы передавать типы на другой хост. В нашем продукте нам пока не приходилось типизировать микрофронтенды - мы стараемся реализовывать их так, чтобы для работы им не требовались пропсы. Но если вам нужно типизировать свой микрофронтенд, то придётся либо расписывать .d.ts файлы руками, либо написать какой-то свой инструмент для сбора деклараций типов с других микрофронтендов.
Ещё есть инструмент emp-tune-dts-plugin, но про него я ничего не могу сказать, так как сам им не пользовался.
Заключение:
Пока что выглядит так, что переход на Webpack 5 Module Federation решает проблему, которая стояла перед нашим стримом, а именно - разделение зоны ответственности и распараллеливание разработки. При этом, нет больших накладных расходов при разработке, а настройка довольно проста даже для тех, кто не знаком с этой технологией.
Минусы у этого подхода конечно же есть, накладные расходы для развертывания зоопарка микрофронтендов будут значительно выше, чем для монолита. Если над вашим приложением работает одна-две команды и оно не такое большое, то наверное не стоит делить его на микросервисы.
Но для нашей конкретной проблемы, это решение подошло хорошо, посмотрим, как оно покажет себя в будущем, технология развивается и уже появляются фреймворки и библиотеки, которые под капотом используют Module Federation.
Полезные ссылки:
Умное