Привет! С вами снова Максим, и это заключительная часть трилогии о переезде на MFE. В первой части мы говорили о том, как пришли к распилу, во второй — что подтолкнуло нас к микрофронтам, и вот настала очередь фолбэков.

Упавший конфиг

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

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

Такую ситуацию проще проиллюстрировать:

Запрос за конфиг попадает в nginx и проверяется наличие в кэше. Если в кэше «нет» — выходит ошибка или 404. В противном случае кэш проверяется на валидность, если не валидный — идем за новым файлом. Но прежде кидаем options и проверяем, живо ли приложение, отдающее конфиг. Потому что иногда, чтобы сохранить загрузки и пользователя, мы отдаем данные из протухшего конфига. В таком случае есть риск, что в момент валидации кэша добавится новый фронтенд, который нужно срочно подтянуть
Запрос за конфиг попадает в nginx и проверяется наличие в кэше. Если в кэше «нет» — выходит ошибка или 404. В противном случае кэш проверяется на валидность, если не валидный — идем за новым файлом. Но прежде кидаем options и проверяем, живо ли приложение, отдающее конфиг. Потому что иногда, чтобы сохранить загрузки и пользователя, мы отдаем данные из протухшего конфига. В таком случае есть риск, что в момент валидации кэша добавится новый фронтенд, который нужно срочно подтянуть

Проблему, описанную выше, решает 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 пробы

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, ремоут их слушает, и все работает. Нам не нужно париться о том, в какое приложение мы встраиваемся
Хост-приложение испускает events, ремоут их слушает, и все работает. Нам не нужно париться о том, в какое приложение мы встраиваемся

Хост-приложение де-факто соответствует нашим стандартам, и мы всегда знаем, что личный кабинет и админка испускают одинаковые events, у них одно и то же состояние данных. Да, они могут локально их расширить уже конкретно под себя, но базис у них есть. 

Любое приложение знает, что пользователь имеет определенный тип, и устанавливается в определенный атрибут. Приложения знают, из какого атрибута состоит пользователь, где он лежит, и могут брать нужные им для работы параметры. И если вдруг случится логаут, релогин — через эту шину они поймут, что нужно либо у себя почистить, если они вдруг сохранили себе этого пользователя, либо что-то поменять, перерисоваться.

И еще одна важная вещь: единый state, то есть рутовое состояние нашего приложения, которое мы фактически пошарили между всеми. Возможно, пошарили не совсем красиво и можно использовать dedicated-воркеры, сервис-воркеры и многое другое, чтобы этот конфиг откуда-то подтягивать. Но npm быстро, дешево и сердито решает нашу задачу. Все приложения знают единый набор селекторов, событий и вот этого всего.

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

Все это у нас — библиотека и выглядит примерно так:

Ремоут хочет запросить данные пользователя, идет в библиотеку, берет название селектора. Библиотека знает, что она уже запущена в хосте и получила именно этот instance, знает, где лежит состояние с пользователем. Хост возвращает запрошенное состояние
Ремоут хочет запросить данные пользователя, идет в библиотеку, берет название селектора. Библиотека знает, что она уже запущена в хосте и получила именно этот instance, знает, где лежит состояние с пользователем. Хост возвращает запрошенное состояние

Все работает дешево и сердито, то есть нашу задачу оно решает. Поэтому можно интерфейс админа подключить в личный кабинет. Но он не заведется, потому что там есть ролевые модели и вот это все. И личный кабинет можно просто перенести внутрь админки, которая у нас есть.

Выводы

Микрофронты — Module Federation или какое-то свое решение — это универсальное средство для решения проблем. Какую бы задачу мы ни решали, можно посмотреть, как лучше сделать: монолитно или с применением микрофронтовых решений. 

Но микрофронты — это все-таки не про фреймворк, а про мышление: как декомпозировать приложение так, чтобы была модульность, легкость, все работало быстро и было расшарено между командами.

Можно использовать модульность в монолите: снаружи монолитный, а внутри — красивый модульный проект. И можно спокойно этот проект перевести на микрофронт.

Но в работе с любым приложением, будь то монолит или микрофронт, микросервисные бэки или еще что-то, главное — фантазия. Экспериментируйте.

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

Комментарии (2)


  1. jamak
    15.06.2023 09:30

    Интересно, а сколько человек в core команде ? та что пилит в основном только базу,
    а не feature модули,

    и распределены ли на команды feature модули?


    1. FindYourDream Автор
      15.06.2023 09:30
      +1

      В данный момент в core команде 3 человека. За каждой бизнес командой у нас закреплено в среднем по 2-3 микрофронта которыми занимаются только они.