Pull to refresh

Мой опыт с Webpack 5 Module Federation

Level of difficultyMedium
Reading time11 min
Views17K

P.S. с очень большим опозданием прикладываю ссылку на запись митапа, который является расширенной версией статьи

Всем привет. Меня зовут Михаил, я - фронтенд-разработчик в Лиге Цифровой Экономики.

В последнее время я пробую себя в должности руководителя направления фронтенд-разработки, однако я хочу с вами поделиться опытом разработки приложения с применением Webpack Module Federation, о том, какие задачи приходилось решать и проблемы, которые возникли на этом пути. Не буду вдаваться в теорию о микрофронтах и module federation, об этом уже много написано и предполагается, что вы знакомы с базовой настройкой. Мы же поговорим о самом «вкусном», некоторые моменты будут опущены в целях сосредоточения на деталях.

Вместо предисловия

На момент написания статьи (21.01.2022) проект находится в предрелизном состоянии и была уже выпущена альфа-версия, которую клиент передал своим потенциальным покупателям. Проект сам по себе с точки зрения бизнеса представляет собой коробочное решение, в виде АРМ системного администратора со следующими возможностями:

  • Граф топологии доменов и сайтов

  • Удаленный рабочий стол прямо в браузере (noVNC)

  • Установка ОС, ПО на машины пользователей по сети

  • Управление пользователями, принтерами и др.

  • Подсистема оповещений через WebSocket и многое другое.

Однако, необходимо вернуться примерно на год назад.

Как все начиналось

Был обычный рабочий день, я тогда был еще рядовым разработчиком и трудился над другим проектом, который был на Vue. Как говорится, ничего не предвещало беды. Ко мне подошел тимлид и сказал: «Заканчивай задачи и как будешь готов, мне надо будет с тобой поговорить. Меня обдало холодным потом, потому как я только переехал из другого города и вышел с удаленки. В действительности, все оказалось несколько проще:

Итак, Миш, у нас новый проект. На React. Я хочу перевести тебя туда.

Да без проблем, - обрадовался я. – А что за проект?

Проект специфический, я всех деталей не знаю, я только готовил прототип. Тебе надо будет поговорить с архитектором. А, да, там будут микрофронты. В общем, чуть позже более детально обсудим, я тебе пока дам доступы и кину приглашения в телеге в чаты нужные.

Когда услышал про микрофронтенды
Когда услышал про микрофронтенды

Тут меня холодным потом обдало во второй раз. Дело в том, что в свободное время я с друзьями пишу пет-проект, если вкратце, то игровой сервер для GTA V на движке, в котором крутится Chromium Embedded Framework, а фронты (мы свои писали на React) выводятся как куча iframe один над другим и иногда возникала проблема шаринга состояния. По сути, очень похоже на те же микрофронты. Вспомнив всю боль, с которой я столкнулся в пете, я немного приуныл и стал ждать деталей.

От лирики переходим к деталям. Какие вводные я получил:

  1. Я один фронтендер на проекте.

  2. У заказчика есть MVP, необходимо «просто его доработать» (с)

  3. Фронт разделяется на ядро и дочерние приложения. Заказчик хочет в любой момент добавлять/удалять приложения без больших работ по фронту.

  4. У каждого приложения есть манифест – некоторая метаинформация + навигация 2 уровня и ниже. Этот манифест ядро запрашивает у сервера и тем самым отображает доступные модули системы

  5. У заказчика есть UI-kit

Как вы понимаете, некоторые пункты с течением проекта сильно изменились. Так, например, постепенно команда фронта в пике достигала 5 человек, а от MVP не осталось почти и следа.

Когда я получил MVP на руки, я полез «под капот» где я обнаружил самописный механизм подключения микрофронтов, который достаточно сильно связан со структурой манифеста приложения, а так же Babel и Redux Toolkit. Остальное было не так интересно.

Схема страницы. Header и Sidebar находятся в Core, Microfrontend - зона для дочернего приложения
Схема страницы. Header и Sidebar находятся в Core, Microfrontend - зона для дочернего приложения

По структуре MVP был просто монолитом, где в папке pages лежали отдельно микрофронты и все это через хитрую таску поднималось на нескольких локальных серверах – сервер ядра, проксирующий и заодно еще сервер с микрофронтом (в dev и watch режимах). Ядро стучало в проксирующий сервер в режиме разработки, который ходил локально в папку с микрофронтом и отдавал его. Выглядело все довольно странно, учитывая предпочтения заказчика и очевидной задачей от тимлида стал распил этого добра на отдельные репозитории.

Окончив всю эту работу, начался цикл разработки, но ситуация выше меня коробила. Список проблем, которые возникли в самом начале:

  1. Для рабочего ноутбука поднимать несколько серверов стало трудоемкой задачей. Добавляем сюда открытый браузер, Figma, Spotify и по уровню шума можно подумать, что у меня на столе мини Байконур

  2. Приложение монтировалось, но оставляло за собой кучу мусора, к тому же еще и не всегда корректно размонтировалось. Особенно остро вопрос мусора стал тогда, когда я заметил, что стили UI kit подключается банально тегом <style> в шапку и никак не удаляется и такая ситуация постоянно повторяется для каждого микрофронта

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

Микроинфаркты от микрофронтов

Этот раздел я хочу посвятить тем нервным моментам, которые пришлось пережить.

Поиски по теме микрофронтов дали неоднозначные результаты. С одной стороны, была куча информации о теории. С другой - почти ничего о практике. Тогда я наивно полагал, что уже сформированы лучшие практики, но реальность оказалась плачевной. Естественно, тогда чуть ли не из каждого угла трубили о Module Federation, и я думал быстро запрыгнуть в паровоз хайпа и умчаться в светлое будущее, которое умные дяди и тети проложили до меня. Отдельно отмечу, что Single SPA тогда странным образом прошел мимо меня, но Module Federation сильно меня зацепил, т.к. в нем я увидел едва ли не решение всех своих проблем:

  • Можно избавиться от самописного механизма, который тяжело поддерживать

  • Можно избавиться от дополнительного локального сервера для разработки

  • В локальной разработке я сильно ближе становлюсь к реальным условиям работы

  • Сильно привлекла фича шаринга пакетов между микрофронтами, потому что эта мысль пришла едва ли не сразу и на обсуждениях с тимлидом пришли как раз к выводу о том, что было бы неплохо определять наличие того же React на странице.

Естественно, сразу же перейти на Module Federation не получилось. Почти потеряв надежду, т.к. я потратил 3 недели (точнее выходных по субботам и воскресеньям) на адаптирование приложений и не получалось ничего. Работал и прототип тимлида на своем механизме и примеры с гитхаба Module Federation, но именно мой проект не работал. Я долго анализировал сначала код, потом конфиги вебпака и не найдя очевидных ответов я попросту решил с 0 переписать приложения. И… оно заработало!

Микроинфаркт №1. Настройка Webpack

Итак, что нужно для настройки ModuleFederation. Выводом из примеров и статей стало то, что нужно всего лишь добавить ModuleFederationPlugin:

const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
    }),
  ],
};

И в дочернем приложении:

const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'app2',
      filename: 'remoteEntry.js',
      exposes: {
        './Widget': './src/Widget',
      },
    }),
  ],
};

Казалось бы, что тут не так? Да все тут так, но проблема оказалась всего лишь в одной строке и не в этом месте:

PublicPath в output критичеcки важен!
PublicPath в output критичеcки важен!

Это одна из проблем, с которой пришлось столкнуться и это проблема потому, что нигде не была указана важность publicPath в output. Именно тут я лично застрял надолго.

Мои попытки разобраться с Module Federation
Мои попытки разобраться с Module Federation

Микроинфаркт №2. Загрузка дочерних приложений

Первая проблема решена – микрофронты заводятся, все хорошо. Кроме одного: необходимо вручную прописывать все приложения. По сути, в этом ничего плохого нет, но это никак не коррелирует с требованиями ТЗ, да и в последствии все дописывать руками тоже лень.

Вторая проблема заключалась в том, что все-таки надо делать загрузку динамически, учитывая манифест. Сам манифест выглядит следующим образом:

{
  "id": "someapp",
  "moduleName": "SomeApp",
  "entrypoint": "someapp.js",
  "menuItem": {
    "path": "/foo",
    "label": "Бла-бла",
    "description": "какое-то описание",
    "icon": "hi",
    "options": [
      {
        "label": "2 уровень навигации",
        "path": "/bar"
      }
    ]
  },
  "routerPath": "/app",
  "weight": 1
}

Это уже конечная вариация манифеста любого дочернего приложения и все-бы ничего, но в конце марта прошлого года не было никаких подсказок о том, как делать динамику. В разделе advanced api был пример, вокруг которого я начал придумывать свой велосипед, но вместо колес получались квадраты. И примерно в конце апреля, когда я уже во второй раз думал бросить все, я наткнулся на статью, из которой честно стырил позаимствовал и механизм динамической загрузки (части кода можно найти на гитхабе и документации webpack), и хук по загрузке скриптом, который полностью разрешил проблему «мусора» в <head>.

Микроинфаркт №3. Состояние

Redux. Как много было о нем написано и сказано, и, наверняка мне достанется в комментариях за слова ниже, но все-таки я осмелюсь. Очевидна идея объединения сторов приложений и недолго копая я наткнулся на Redux Reducer Injection Example. Вроде бы все классно, но тут возникли самые большие сложности с адаптацией.

Я в попытках объединить состояния приложений
Я в попытках объединить состояния приложений

Во-первых, инжектить редьюсер в хост приложение мне показалось странным в том смысле, что необходим корректный механизм очищения «хвостов» от дочернего приложения при размонтировании. Мне в голову хороший вариант так и не пришел

Во-вторых, к этому моменту начался этап интеграции с беком и стал вопрос о полноценном переходе на саги. Я, если честно, не сторонник ради одной библиотеки тянуть еще 10, когда можно обойтись одной.

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

В-четвертых, а почему бы просто не передавать стор сверху вниз, а не снизу вверх? А также вообще объединить их, одновременно оставив независимыми.

В-пятых, концептуально каждое приложение содержит отдельный набор сущностей (компьютер, группа компьютеров, ПО, домен, сайт и т.д.) и они нигде и никак не пересекаются, имея свое независимое состояние. Представляю перед собой огромного вида стор, сразу же захотелось отделить котлеты от мух, т.к. это попросту Домены.

Ответ нашелся сразу в виде MobX. Он полностью удовлетворяет всем потребностям, не тянет за собой шлейф из библиотек, великолепно работает с промисами и генераторами, а так же прямо в документации говорит, как объединить сторы. Забегая вперед, скажу, что при расширении команды вхождение в MobX заняло несколько дней для джунов, что сэкономило много времени на дистанции.

Микроинфаркт №4. Роутер, роутинг и history

На самом деле это не столько проблема Module Federation, сколько, наверняка, я ошибся в проектировании и пересборке приложения из MVP. Например, у меня напрочь отказался работать BrowserRouter при монтировании дочернего приложения, при этом HashRouter работает как надо. И из-за некоторых ошибок при построении роутов пришлось держать 2 разные версии history для хоста и дочернего приложения из-за проблем энкодирования кириллицы в URL и передачи некоторых параметров в Django. Если у кого есть мысли не этот счет, был бы рад услышать какой вы используете роутер.

Микроинфаркт №5. Шаринг модулей

Не думаю, что станет откровением тот факт, что шаринг модулей – это киллер-фича Module Federation. Давайте посмотрим на опции в конфиге:

  1. eager – говорит о том, что данный модуль необходимо «жадно» потреблять при старте приложения. Без него микрофронт отвалится.

  2. singleton – указывает, разрешено ли иметь более одного инстанса модуля в окне

  3. requiredVersion – сравнение версии инстанса модуля в окне. При одинаковых модулях, но разных версиях отдается предпочтение более свежему.    

В целом, мой конфиг для дочернего приложения выглядит так:

const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('../package.json').dependencies;
const { moduleName, entrypoint } = require('../src/static/app-manifest.json');

module.exports = function (isProduction) {
  return {
    plugins: [
      new ModuleFederationPlugin({
        name: moduleName,
        filename: entrypoint,
        exposes: {
          [`./${moduleName}`]: `./src/${moduleName}`,
        },
        shared: {
          react: {
            requiredVersion: deps.react,
            eager: !isProduction,
            singleton: !isProduction
          },
          axios: {
            requiredVersion: deps.axios,
            eager: !isProduction,
            singleton: !isProduction
          },
          mobx: {
            requiredVersion: deps.mobx,
            eager: !isProduction,
            singleton: !isProduction
          },
        },
      }),
    ],
  };
};

От конфига хоста он отличается лишь наличием флага isProduction и в таком виде его легко тиражировать по другим приложениям.

Однако по неизвестной причине не все модули шарятся. Некоторые крашат приложение. Другие могу некорректно инстанцироваться или грузятся целиком. Например, у нас только в одном приложении есть мигрирующий баг, когда не инстанцируется i18n и из-за этого не подтягивается локализация, при этом, перезагрузив страницу, проблема пропадает. Странно то, что все приложения имеют одинаковый конфиг и точку входа, различия только в нейминге некоторых несущественных элементов. Ответ на это может быть утверждение Вадима Малютина из его доклада для HolyJS. Я же данную проблему не решил и просто отключил потребление модуля для данного приложения.

Другая проблема – поддержание актуального состояния пакетов. Т.к. у нас UI kit поставляется заказчиком, возникают некоторые проблемы с обновлением. Так, например, т.к. потребитель у нас дочернее приложение, бывало часто, что если версия библиотеки не совпадала с хостом, то у нас отваливались почти все стили, т.к. в классах менялись хеши.

Еще одна проблема – плохая подготовка библиотек. Не хочется говорить об этом открыто, но стоит отметить, что при неправильной организации библиотеки она может вывалиться в remoteEntry целиком и полностью вместо того, чтобы упасть в свой чанк. И тут вы можете потерять еще одно преимущество – вместо того чтобы тянуть по сети небольшой файл, вы тянете огромный чанк. Так, например, при переходе на Module Federation мною было замечена уменьшение траффика на дочернее приложение с 500 на 40кб в ранних версиях (к сожалению, скриншоты потерял, придется верить на слово и не только мне), но однажды ситуация изменилась в худшую сторону.

Микроинфаркт №6. Code splitting

И, наверное, последняя проблема, с которой я столкнулся – разбивка кода на чанки. Так, на очередном цикле работы, я решил заняться оптимизацией приложения и под нож у меня попал babel, т.к. не давал адекватно протестировать приложение в условиях плохого соединения. «Скачивание» 20мб девелоперских ассетов по 3g как раз хватит выпить пару кружек кофе и поэтому на его место пришел esbuild, который параллельно вытеснил собой и Terser. Но, когда я прописал чанки вручную, все дочерние приложения упали с ошибкой. Я грешил на esbuild, но оказалось, что Module Federation не дружит с такой оптимизацией, т.к. сплитит самостоятельно. Решилась ли эта проблема на сегодняшний день я не знаю.

Вместо выводов

Обычно, в конце статьи делаются какие-либо выводы о технологии и т.д. Я не хочу повторяться о том, насколько крут Module Federation, я не скажу ничего нового. Он действительно настолько хорош, как о нем говорят.

Скорее, я дам самому себе несколько советов, если придется работать с Module Federation, и не только:

  1. Пересмотреть способ организации работы со стейтами. Сейчас у нас все очень похоже на dependency injection, но как-то криво, на мой взгляд. Если двигаться по этому пути, то опробовать, например, Inversify. Если посмотреть под другим углом, то попробовать событийно-ориентированный стейт-менеджер. При поиске я наткнулся на Storeon, выглядит интересно, но никаких демок я не делал. Или же вообще общение между микрофронтами вынести в worker, хотя на первый взгляд выглядит как оверхед, но в пет проекте есть нечто подобное.

  2. Подготовить shared-библиотеки. И не одну. И сразу. Дело в том, что, работая над микрофронтами, все-равно что-то может повторяться в дочерних приложениях и лучше сразу уносить это в одну точку, т.к. в начале проекта мы не задумывались ни о переиспользовании логики, ни о переиспользовании компонентов, не смотря на ui kit. И так же стоит учесть, что лучше отделить логику от ui, потому что можно столкнуться с неожиданным поведением, напимер, «отвалы» контекстов, на которые многие жалуются, но с таким я не сталкивался. И в целом, лично мне нравится подход dumb components, не раз себя оправдывал.

  3. Абстракции. Как только появилась мысль сделать абстракцию – надо её делать, а не откладывать на потом. Естественно, в пределах разумного. К сожалению, я сначала думал отделаться «малой» кровью, пришлось писать сразу 3 и быстро их интегрировать.

  4. Оптимизация приложения. Да, преждевременная оптимизация – плохо. Но тогда не будет сюрпризов, например, с чанками, которые постигли меня. Необходимо даже на промежуточных этапах гонять бенчмарки и по возможности оптимизировать то, что есть. Особенно Core Web Vitals, потому что проход напрямую в точку, где происходит монтирование микрофронта может стать очень долгой операцией, и, если вовремя не отследить просадку производительности, можно получить ситуацию необходимости очень трудоемкого рефактора. Сетевые метрики тоже еще никто не отменял.

  5. Делать приложения абсолютно независимыми. Module Federation прекрасно позволяет это сделать - разрабатывать и использовать приложения отдельно и независимо очень круто! Но тут вырастают накладные расходы на Developer Experience, т.к. у меня была ситуация с dead code в продакшн сборке, и, каюсь, не везде удалось его убрать в силу разных причин. Так же, могут вырасти расходы на CI/CD и инфрастуктуру, но мы обошлись простым деплоем каждого приложения отдельно через Jenkins.

  6. Тесты. Просто потому, что было прямое указание. Мы тесты не писали. Как оказалось в последствии, очень зря и я очень жалею, что и тут я думал, что «пронесет».

  7. Репозитории. Следить за 11 репозиториями фронта в какой-то момент становится очень сложно. Что с этим делать - пока не ясно, но взгляд устремляется в сторону монорепозитория.

На этом у меня все, всем спасибо за внимание и успехов!

Tags:
Hubs:
Total votes 22: ↑22 and ↓0+22
Comments22

Articles