Микровселенная безумия, или Как устроены микрофронтенды в Dodo

e4b3a04f71f39c7eca24ab78d0ad75e8.png

«Микрофронтенды в компании, которая доставляет пиццу? Серьёзно? Зачем? Да и куда? У вас же всего лишь приложенька с каталогом и заказом товара. Какие ещё микрофронтенды?»

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

На самом деле не всё так просто

Вся работа компании построена на огромной системе Dodo IS. Подробнее можно про неё прочитать в статье нашего СТО Паши Притчина. Если вкратце, то это монолит, который состоит из большого количества сервисов. Но командам разработчиков, как правило, удобнее работать с микросервисами, чем с большим монолитом. Отсюда и решение отпиливать их.

Процесс пошёл, сервисы начали отпиливаться, но появился другой вопрос:, а что делать с UI?

Практически у каждого сервиса есть своя админка. Админки для нескольких сервисов часто лежат рядом, поэтому поначалу весь UI оставляли в самом монолите. Но такой подход терял все профиты от использования микросервисов, так как приходилось менять код сначала в микросервисе, а потом ещё и в монолите. Работа увеличилась бы вдвое. Плюс этот сервис размазывался, не становился таким самостоятельным и зависел бы от кода извне. Ну и писать end-to-end гораздо удобнее, когда фронт и бэк находятся рядом.

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

Практически все b2b интерфейсы у нас были написаны на Razor и jQuery. Но очень хотелось идти в ногу со временем, писать новый фронт на современных решениях и по мере возможности переписывать старые на новый стек. Микрофронтенды как раз позволяют писать себя на разных технологиях и разводить полнейший зоопарк.

Короче говоря, мы хотели:

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

  • быстро и независимо деплоить каждый интерфейс;

  • иметь возможность писать на различных фреймворках, использовать любые сборщики и т.д.

Мы поискали варианты решений наших потребностей и пришли к микрофронтендам.

Нельзя просто взять и сделать микрофронтенды

Ну для начала, конечно же, мы посмотрели на Iframe. Самый простой для реализации вариант: вставляем в него наше приложение и всё.




    
    
    
    Document


    












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

На тот момент уже много кто использовал подход с Single-spa. Это удобный фреймворк для создания приложения, которое может объединять в себе дочерние фронтенд-приложения (appshell далее). Выглядит базовый конфиг совсем несложным:

import { registerApplication, start } from 'single-spa'

registerApplication(
    'sharedApp',
    () => import('./sharedApp.js'),
    location => location.pathname.startsWith('/sharedApp'),
)

start()

Регистрируем приложение sharedApp, импортируем и показываем его по маршруту /sharedApp.

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

Но у нас же ещё есть сами микрофронтенды, и их тоже нужно как-то готовить. Экосистема Single-spa позволяет нам сделать и это. У неё есть библиотеки для создания микрофронтендов на разных фреймворках. Например, single-spa-react.

import singleSpaReact from 'single-spa-react';

const App = () => 
SharedApp
export const { bootstrap, mount, unmount } = singleSpaReact({ React, ReactDOMClient, rootComponent: App, errorBoundary: err => { return
I Knew You Were Trouble
}, })

Оборачиваем приложение в метод singleSpaReact и экспортируем методы жизненного цикла, необходимые Single-spa для корректной работы.

Соберём наш микрофронтенд каким-либо сборщиком, подсунем его в appshell и всё, живём счастливо? (Нет)

Наши микрофронтенды деплоятся и достаются из blob-storage. Соответственно, импортировать их через относительные пути (с текущего хоста) не получится. Нужно запрашивать их динамически и лениво.

Мы не знаем заранее, где будут храниться бандлы, но хотим их импортировать не по путям, а просто по названиям. С этим может помочь Importmap. Это скрипт с картами импортов, у которых ключи в роли идентификатора (название приложения) и значениями в роли относительных или абсолютных путей на его физическое расположение (какой-нибудь blob-storage, например, Azure).

	

C ними, к сожалению, тогда и до сих пор сохраняются некоторые проблемы. Их поддержку всё ещё не полностью реализовали во всех современных браузерах (например, Safari на iOS). Множественные importmap не поддерживаются в современных браузерах (например, в Chrome). Также есть проблема с external importmap (когда importmap подтягиваются через src тега script).

И что же делать в таком случае?

На помощь пришёл SystemJS. Он позволяет использовать вышеупомянутые importmap, только в формате systemjs-importmap, который поддерживают большинство браузеров.

Получается, appshell будет выглядеть таким образом:

import { registerApplication, start } from 'single-spa'

registerApplication(
    'sharedApp',
    () => import('mySharedApp'),
    location => location.pathname.startsWith('/sharedApp'),
)

start()

Кажется, теперь всё хорошо. (Нет)

Так как мы используем для полифила importmap SystemJS, мы также будем использовать его для загрузки модулей.

Перепишем импорт микрофронтенда через SystemJS, и наш appshell будет выглядеть так:

import { registerApplication, start } from 'single-spa'

registerApplication(
    'sharedApp',
    () => System.import("mySharedApp"),
    location => location.pathname.startsWith('/sharedApp'),
)

start()

И вот тут пазл сошёлся.

© Habrahabr.ru