Привет! С вами снова Максим, и это заключительная часть трилогии о переезде на MFE. В первой части мы говорили о том, как пришли к распилу, во второй — что подтолкнуло нас к микрофронтам, и вот настала очередь фолбэков.
Упавший конфиг
В прошлой части мы добавляли конфигурацию нашего приложения и перенесли ее из локального хранения на отдельный сервис. Но что делать, если сервис, раздающий эту конфигурацию, будет недоступен?
Первый фолбэк — это конфигурация. В случае недоступности раздающего сервиса нам нужно из какого-то кэша отдать конфиг, даже если кэш будет протухшим. Иначе конечный пользователь столкнется с проблемой.
Такую ситуацию проще проиллюстрировать:
Проблему, описанную выше, решает CDN. Если в нашем nginx нет кэша, он точно есть у CDN, но мы дополнительно сохраняем у себя в локальном кэше nginx. С конфигом все понятно: код мы здесь даже не писали и не притронулись к нему.
Сломанный переход
Внутри приложения нам нужны фолбэки на загрузку микрофронта или хотя бы фолбэки переходов между страницами внутри приложений.
Module Federation имеет мало отношения и к микрофронтам, просто фолбэки нужны всегда, если вы боретесь за доступность ваших приложений. Здесь мы добавили их именно в таком контексте.
Еще переходы между приложениями могут затрагивать плановые работы на ремоутах: релизим какой-то функционал и он может привести к down time. Поэтому нужно повесить плашечку, то есть отследить, живо ли приложение, пускать ли на него трафик.
this.router.events.pipe(takeUntil(this.destroy$)).subscribe((event: Event) => {
switch (true) {
case event instanceof NavigationError: {
/*
Do some logic
.....
*/
this.router.navigate(['redirect', 'error']);
break;
}
}
});
Любому роутеру нужно добавить обработчик на редирект. У нас в Ангуляре есть Router Events, мы ловим среди событий ошибочное и либо показываем нотификацию, либо навигируем отдельную ошибку, если понимаем что микрофронт полностью недоступен. Но главное — мы говорим с пользователем: не показываем ему белый экран, а сообщаем, что есть проблема, но можно пользоваться остальными приложениями, все будет хорошо, а это мы починим.
Работы на проде
Health check — это проба, которая говорит, живо наше приложение в данный момент или нет
На фронте health check используется редко, а в мире бэкенда — достаточно часто.
Поэтому добавляем на nginx проверку maintenance mode. Если мы релизим приложение и знаем, что будет down time, то переключаем галочку maintenance mode, чтобы перед этим down time увести трафик из приложения.
Многие проекты запущены не в одном экземпляре, а минимум в двух-трех для поддержания стабильности. Вывели один экземпляр через maintenance mode, трафик через него перестал приходить и релизим без аффекта на клиентов.
Если maintenance mode не включен, просто проверяем живость сервиса через health check. Сервис мертв — 503, мы не можем принимать трафик. Живой — 200, ок, все хорошо. Описание нужно где-то хранить, для этого есть великолепный конфиг, который мы можем расширить:
{
"remoteEntry": "http://localhost:4201/remoteEntry.js",
"remoteName": "main",
"exposedModule": "./Module",
"displayName": "navigation.main",
"routePath": "main",
"ngModuleName": "RemoteEntryModule",
"health": {
"url": "/remote/main/health",
"ttl": "5000",
"scheduler": "10"
}
В конфиг добавляем новый объект с указанием, куда ходить по конкретному микрофронту, какая максимальная задержка должна быть. Думаю, не стоит ждать ответа health check дольше 60 секунд. Если сервис вам не ответил за 5 секунд — кажется, уже есть проблемы, можно его глушить. В нашем примере мы проверяем живость приложения раз в 10 секунд, с ожиданием ответа в течение 5 секунд.
Со стороны приложения наш хост может вычитать этот конфиг, запросить health check у приложения и обработать ответ.
В тот момент, когда мы создаем алерт и понимаем, что приложение недоступно, можем обработать эту ситуацию. Например, убрать недоступное приложение из навигации или повесить плашку, что сейчас ведутся технические работы, — чтобы пользователи видели предупреждение о возможных проблемах.
Кейс простой и удобный. Но нужно смотреть, хотите ли вы показывать пользователю, что у вас работы или же сбой. Или вы принимаете этот риск, что они пойдут туда, попадут, что-нибудь сломают или сломаются.
Ремоут в ремоуте
Однажды к нам пришла очень добрая тетя-продакт, позвала всю команду фронтенда и сказала: «Ребята, у меня есть задача, от которой вы будете просто без ума».
У нас есть два проекта: личный кабинет и админка. Думаю, у многих имеются такие приложения. И там, и там у нас микрофронтенды.
Появилась задача, по которой админ должен при определенной роли в админке видеть пользовательские операции и что-то с ними делать: возвращать, подтверждать и многое-многое другое. Но часть функциональности они не должны видеть, то есть нужны определенные роли.
Сели думать, как будет выглядеть наше приложение.
Как можно решить эту задачу? Ctrl+C, Ctrl+V — перенесли, допилили. Но нам нужно фактически зеркалирование. Если мы внесли изменения в личный кабинет, оно должно автоматически отразиться в админке. Если же мы Ctrl+C, Ctrl+V — появляется второй проект, в котором нужно не забыть это допилить.
После этой истории у нас появляется ситуация: есть хост, внутри которого ремоут и внутри ремоута еще один ремоут.
Кодируем на успех
В загрузчик конфига добавляем историю с сохранением конфига в session storage. У нас используется специальный ключ configuration storage key. И это отсылочка к той первой истории, когда мы делили на локальные npm-библиотеки все, что должно быть вынесено общее:
private resolveConfig(): Observable<Microfrontend[]> {
return this.httpClient
.get<Microfrontend[]>('/assets/config/mf/config.json')
.pipe(tap(mfConfig => sessionStorage.setItem(CONFIGURATION_STORAGE_KEY, JSON.stringify(mfConfig))));
}
Появилась NPM-библиотека для токенов, конфигов и для состояния наших приложений. О ней я чуть позже расскажу. Мы решили использовать именно микрофронтендовый подход не только в одном приложении, а зациклить все хосты, которые у нас есть, на определенный базис требований, которым они должны соответствовать.
Пишем еще один загрузчик:
export function loadRemoteRoute(remoteName: RemoteNames) {
return new Promise((resolve, reject) => {
const mfConfig = sessionStorage.getItem(CONFIGURATION_STORAGE_KEY);
if (!mfConfig) {
reject(new Error('CONFIG not provided'));
}
const remotes: Microfrontend[] = JSON.parse(mfConfig as string);
const requiredRemote = remotes.find((remote: Microfrontend) => remote.remoteName === remoteName);
if (!requiredRemote) {
reject(new Error(`Remote with provided name ${remoteName}, not found`));
}
resolve(
loadRemoteModule(requiredRemote as Microfrontend).then(m => m[(requiredRemote as Microfrontend).ngModuleName]),
);
});
}
Мы фактически делаем обертку на функцию, которую нам поставляет Module Federation внутри проекта. По нашему storage key внутри другого приложения в другой библиотеке находим конфиг, подгружаем его:
Здесь мы не можем использовать ангуляровский DI, потому что используется это все в момент, когда до DI еще дело не дошло. Поэтому берем из конфига, парсим и находим нужное приложение:
У нас везде есть список приложений — это enum, который мы передаем.
Если нашли — отлично, работаем дальше. Мы вызываем Module Federation функцию и подгружаем наше приложение либо выдаем ошибку:
В целом функция «загрузчик» отлично работает, и мы идем по стандартному для фреймворка пути — без каких-либо костылей и велосипедов, то есть у нас есть массив объектов, в котором описаны наши пути. В одном из этих дочерних приложений указываем, что появляется новый путь. Как только пользователь перейдет по нему, произойдет вызов функции, которая все сделает за тебя:
{
path: ’operations',
loadChildren: () => loadRemoteRoute(‘operations'),
},
Функция сходит на отдельный хост, заберет оттуда название ремоута, его файлики и прогрузит наш микрофронт там, где нужно.
У нас все получилось, все работает.
Шина данных
Почему мы взяли страницу личного кабинета, вставили ее в админку, и оно не сломалось?
Все потому, что у нас есть шина данных. Все приложения, которые мы разрабатываем, должны иметь у себя свой фича state, в котором они хранят только свою бизнес-логику и данные. И они знают о том, что они куда-то встраиваются, и у этого есть рутовое состояние.
Рутовое состояние между всеми приложениями сделано двумя способами:
1. Паблишим npm-библиотеку: подключаем, указываем, какой рутовый state взять из этой библиотеки.
2. Единые DTO для описания типов состояния.
Наше хост-приложение при старте будет исполнять роль продюсера — возьмет из библиотеки поток и обозначит, что в этот поток загружаются все события: по загрузке пользователя, по загрузке конфига при изменении языка.
Ремоут-приложение знает, что есть очередь, на которую стоит подписаться, и каких событий он ждет. Поэтому, когда события исполнятся, он их обработает. Выглядит это примерно так:
Хост-приложение де-факто соответствует нашим стандартам, и мы всегда знаем, что личный кабинет и админка испускают одинаковые events, у них одно и то же состояние данных. Да, они могут локально их расширить уже конкретно под себя, но базис у них есть.
Любое приложение знает, что пользователь имеет определенный тип, и устанавливается в определенный атрибут. Приложения знают, из какого атрибута состоит пользователь, где он лежит, и могут брать нужные им для работы параметры. И если вдруг случится логаут, релогин — через эту шину они поймут, что нужно либо у себя почистить, если они вдруг сохранили себе этого пользователя, либо что-то поменять, перерисоваться.
И еще одна важная вещь: единый state, то есть рутовое состояние нашего приложения, которое мы фактически пошарили между всеми. Возможно, пошарили не совсем красиво и можно использовать dedicated-воркеры, сервис-воркеры и многое другое, чтобы этот конфиг откуда-то подтягивать. Но npm быстро, дешево и сердито решает нашу задачу. Все приложения знают единый набор селекторов, событий и вот этого всего.
Если мое приложение просит информацию по пользователю через селектор, в любом хост-приложении будет информация по пользователю, чтобы этот селектор отработал. У нас нет проблем с подключением тех или иных страниц в разные приложения. Поэтому любую функциональность личного кабинета можно без каких-либо проблем добавить в наше приложение через добавление в конфиг и подключение где-то в роутере.
Все это у нас — библиотека и выглядит примерно так:
Все работает дешево и сердито, то есть нашу задачу оно решает. Поэтому можно интерфейс админа подключить в личный кабинет. Но он не заведется, потому что там есть ролевые модели и вот это все. И личный кабинет можно просто перенести внутрь админки, которая у нас есть.
Выводы
Микрофронты — Module Federation или какое-то свое решение — это универсальное средство для решения проблем. Какую бы задачу мы ни решали, можно посмотреть, как лучше сделать: монолитно или с применением микрофронтовых решений.
Но микрофронты — это все-таки не про фреймворк, а про мышление: как декомпозировать приложение так, чтобы была модульность, легкость, все работало быстро и было расшарено между командами.
Можно использовать модульность в монолите: снаружи монолитный, а внутри — красивый модульный проект. И можно спокойно этот проект перевести на микрофронт.
Но в работе с любым приложением, будь то монолит или микрофронт, микросервисные бэки или еще что-то, главное — фантазия. Экспериментируйте.
Если кажется странной идея пошарить state, запихнуть это все еще куда-то — попробуйте. Потому что только попробовав вы поймете, тяжело вам это дается или легко, какие плюсы и минусы вы от этого получите. Поэтому не бойтесь экспериментировать.
jamak
Интересно, а сколько человек в core команде ? та что пилит в основном только базу,
а не feature модули,
и распределены ли на команды feature модули?
FindYourDream Автор
В данный момент в core команде 3 человека. За каждой бизнес командой у нас закреплено в среднем по 2-3 микрофронта которыми занимаются только они.