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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <iframe src="./mySharedApp/index.html"></iframe>
</body>
</html>

Проблемы сразу же возникают, когда подключаешь приложение с другого хоста. Тут вопросы и с 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 = () => <div>SharedApp</div>

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

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

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

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

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

	<script type="importmap">
		{
			"imports": {
			   "mySharedApp": "https://storage/mySharedApp.432dfgh45.js",
			   "mySharedApp2": "https://storage/mySharedApp2.123fsd4g.js",
			}
		}
	</script>

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

<script type="importmap" src="http://localhost:4000/importmap.json">
</script>

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

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

<script type="systemjs-importmap">
	{
	  "imports": {
	    "mySharedApp": "https://storage/mySharedApp.432dfgh45.js",
	    "mySharedApp2": "https://storage/mySharedApp2.123fsd4g.js",
	  }
	}
</script>

Получается, 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()

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

Источник: https://habr.com/ru/company/dododev/blog/712320/


Интересные статьи

Интересные статьи

Кто бы что ни говорил, но я считаю, что изобретение велосипедов — штука полезная. Использование готовых библиотек и фреймворков, конечно, хорошо, но порой стоит их отложить и создать ...
Каждый лишний элемент на сайте — это кнопка «Не купить», каждая непонятность или трудность, с которой сталкивается клиент — это крестик, закрывающий в браузере вкладку с вашим интернет-магазином.
В 1С Битрикс есть специальные сущности под названием “Информационные блоки, сокращенно (инфоблоки)“, я думаю каждый с ними знаком, но не каждый понимает, что это такое и для чего они нужны
Если вы последние лет десять следите за обновлениями «коробочной версии» Битрикса (не 24), то давно уже заметили, что обновляется только модуль магазина и его окружение. Все остальные модули как ...
В «1С-Битрикс» считают: современный интернет-магазин должен быть визуально привлекательным, адаптированным для просмотра с мобильных устройств и максимально персонализированным с помощью технологии Бо...