
Всем привет. Меня зовут Михаил, я — фронтенд-разработчик в Лиге Цифровой Экономики.
В последнее время я пробую себя в должности руководителя направления фронтенд-разработки, однако я хочу с вами поделиться опытом разработки приложения с применением Webpack Module Federation, о том, какие задачи приходилось решать и проблемы, которые возникли на этом пути. Не буду вдаваться в теорию о микрофронтах и module federation, об этом уже много написано и предполагается, что вы знакомы с базовой настройкой. Мы же поговорим о самом «вкусном», некоторые моменты будут опущены в целях сосредоточения на деталях.
Вместо предисловия
На момент написания статьи (21.01.2022) проект находится в предрелизном состоянии и была уже выпущена альфа-версия, которую клиент передал своим потенциальным покупателям. Проект сам по себе с точки зрения бизнеса представляет собой коробочное решение, в виде АРМ системного администратора со следующими возможностями:
-
Граф топологии доменов и сайтов
-
Удаленный рабочий стол прямо в браузере (noVNC)
-
Установка ОС, ПО на машины пользователей по сети
-
Управление пользователями, принтерами и др.
-
Подсистема оповещений через WebSocket и многое другое.
Однако, необходимо вернуться примерно на год назад.
Как все начиналось
Был обычный рабочий день, я тогда был еще рядовым разработчиком и трудился над другим проектом, который был на Vue. Как говорится, ничего не предвещало беды. Ко мне подошел тимлид и сказал: «Заканчивай задачи и как будешь готов, мне надо будет с тобой поговорить. Меня обдало холодным потом, потому как я только переехал из другого города и вышел с удаленки. В действительности, все оказалось несколько проще:
– Итак, Миш, у нас новый проект. На React. Я хочу перевести тебя туда.
– Да без проблем, — обрадовался я. – А что за проект?
– Проект специфический, я всех деталей не знаю, я только готовил прототип. Тебе надо будет поговорить с архитектором. А, да, там будут микрофронты. В общем, чуть позже более детально обсудим, я тебе пока дам доступы и кину приглашения в телеге в чаты нужные.
Тут меня холодным потом обдало во второй раз. Дело в том, что в свободное время я с друзьями пишу пет-проект, если вкратце, то игровой сервер для GTA V на движке, в котором крутится Chromium Embedded Framework, а фронты (мы свои писали на React) выводятся как куча iframe один над другим и иногда возникала проблема шаринга состояния. По сути, очень похоже на те же микрофронты. Вспомнив всю боль, с которой я столкнулся в пете, я немного приуныл и стал ждать деталей.
От лирики переходим к деталям. Какие вводные я получил:
-
Я один фронтендер на проекте.
-
У заказчика есть MVP, необходимо «просто его доработать» (с)
-
Фронт разделяется на ядро и дочерние приложения. Заказчик хочет в любой момент добавлять/удалять приложения без больших работ по фронту.
-
У каждого приложения есть манифест – некоторая метаинформация + навигация 2 уровня и ниже. Этот манифест ядро запрашивает у сервера и тем самым отображает доступные модули системы
-
У заказчика есть UI-kit
Как вы понимаете, некоторые пункты с течением проекта сильно изменились. Так, например, постепенно команда фронта в пике достигала 5 человек, а от MVP не осталось почти и следа.
Когда я получил MVP на руки, я полез «под капот» где я обнаружил самописный механизм подключения микрофронтов, который достаточно сильно связан со структурой манифеста приложения, а так же Babel и Redux Toolkit. Остальное было не так интересно.
По структуре MVP был просто монолитом, где в папке pages лежали отдельно микрофронты и все это через хитрую таску поднималось на нескольких локальных серверах – сервер ядра, проксирующий и заодно еще сервер с микрофронтом (в dev и watch режимах). Ядро стучало в проксирующий сервер в режиме разработки, который ходил локально в папку с микрофронтом и отдавал его. Выглядело все довольно странно, учитывая предпочтения заказчика и очевидной задачей от тимлида стал распил этого добра на отдельные репозитории.
Окончив всю эту работу, начался цикл разработки, но ситуация выше меня коробила. Список проблем, которые возникли в самом начале:
-
Для рабочего ноутбука поднимать несколько серверов стало трудоемкой задачей. Добавляем сюда открытый браузер, Figma, Spotify и по уровню шума можно подумать, что у меня на столе мини Байконур
-
Приложение монтировалось, но оставляло за собой кучу мусора, к тому же еще и не всегда корректно размонтировалось. Особенно остро вопрос мусора стал тогда, когда я заметил, что стили 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. Именно тут я лично застрял надолго.
Микроинфаркт №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. Давайте посмотрим на опции в конфиге:
-
eager – говорит о том, что данный модуль необходимо «жадно» потреблять при старте приложения. Без него микрофронт отвалится.
-
singleton – указывает, разрешено ли иметь более одного инстанса модуля в окне
-
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, и не только:
-
Пересмотреть способ организации работы со стейтами. Сейчас у нас все очень похоже на dependency injection, но как-то криво, на мой взгляд. Если двигаться по этому пути, то опробовать, например, Inversify. Если посмотреть под другим углом, то попробовать событийно-ориентированный стейт-менеджер. При поиске я наткнулся на Storeon, выглядит интересно, но никаких демок я не делал. Или же вообще общение между микрофронтами вынести в worker, хотя на первый взгляд выглядит как оверхед, но в пет проекте есть нечто подобное.
-
Подготовить shared-библиотеки. И не одну. И сразу. Дело в том, что, работая над микрофронтами, все-равно что-то может повторяться в дочерних приложениях и лучше сразу уносить это в одну точку, т.к. в начале проекта мы не задумывались ни о переиспользовании логики, ни о переиспользовании компонентов, не смотря на ui kit. И так же стоит учесть, что лучше отделить логику от ui, потому что можно столкнуться с неожиданным поведением, напимер, «отвалы» контекстов, на которые многие жалуются, но с таким я не сталкивался. И в целом, лично мне нравится подход dumb components, не раз себя оправдывал.
-
Абстракции. Как только появилась мысль сделать абстракцию – надо её делать, а не откладывать на потом. Естественно, в пределах разумного. К сожалению, я сначала думал отделаться «малой» кровью, пришлось писать сразу 3 и быстро их интегрировать.
-
Оптимизация приложения. Да, преждевременная оптимизация – плохо. Но тогда не будет сюрпризов, например, с чанками, которые постигли меня. Необходимо даже на промежуточных этапах гонять бенчмарки и по возможности оптимизировать то, что есть. Особенно Core Web Vitals, потому что проход напрямую в точку, где происходит монтирование микрофронта может стать очень долгой операцией, и, если вовремя не отследить просадку производительности, можно получить ситуацию необходимости очень трудоемкого рефактора. Сетевые метрики тоже еще никто не отменял.
-
Делать приложения абсолютно независимыми. Module Federation прекрасно позволяет это сделать — разрабатывать и использовать приложения отдельно и независимо очень круто! Но тут вырастают накладные расходы на Developer Experience, т.к. у меня была ситуация с dead code в продакшн сборке, и, каюсь, не везде удалось его убрать в силу разных причин. Так же, могут вырасти расходы на CI/CD и инфрастуктуру, но мы обошлись простым деплоем каждого приложения отдельно через Jenkins.
-
Тесты. Просто потому, что было прямое указание. Мы тесты не писали. Как оказалось в последствии, очень зря и я очень жалею, что и тут я думал, что «пронесет».
-
Репозитории. Следить за 11 репозиториями фронта в какой-то момент становится очень сложно. Что с этим делать — пока не ясно, но взгляд устремляется в сторону монорепозитория.
На этом у меня все, всем спасибо за внимание и успехов!
ссылка на оригинал статьи https://habr.com/ru/articles/650401/
Добавить комментарий