Mateu: інший погляд на мікрофронтенди

pic

Фото Xavi Cabrera на Unsplash

Вступ

Протягом моєї кар'єри мені довелося проєктувати та створювати кілька великих і складних систем. Однією з ключових стратегій для вирішення цих задач було розбивання їх на менші, більш керовані компоненти, при цьому процес створення інтерфейсів користувача (UI) робився максимально безшовним.

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

Для демонстрації цього методу ми розглянемо простий випадок: створення системи бронювання, де інтерфейс складається з кількох мікрофронтендів.

Що таке мікрофронтенди?

Мікрофронтенд — це модульний компонент інтерфейсу користувача, який можна вбудовувати в більший інтерфейс та розгортати незалежно один від одного. Оновлення мікрофронтенду не потребує змін або перезавантаження основного інтерфейсу.

Крім можливості повторного використання, особливо цінним є те, що мікрофронтенди дозволяють командам повністю володіти своїми функціями, від розробки до розгортання. Замість того, щоб одна центральна команда фронтенду відповідала за весь інтерфейс і споживала REST-ендпоінти від різних бекенд-сервісів, кожна команда може керувати та доставляти повну функціональність, включаючи як фронтенд, так і бекенд компоненти. Для того, щоб додати нову функцію до вашого додатку, потрібно лише вбудувати відповідний мікрофронтенд.

Мікрофронтенди дають змогу командам розробників зменшити залежності та мінімізувати бюрократію, що допомагає усунути вузькі місця та оптимізувати процес розробки.

Що таке Mateu?

Mateu — це бібліотека для Java, яка призначена для створення бекенд-орієнтованих інтерфейсів користувача. Використовуючи прості анотації та інтерфейси, Mateu пропонує домен-специфічну мову (DSL), що дозволяє визначати інтерфейс користувача за допомогою простих класів Java, в той час як її runtime піклується про його функціональність.

Frontend-частина Mateu — це фактично веб-компонент, що взаємодіє з простим REST API. Цей веб-компонент надсилає запити до API і відображає отримані відповіді. Оскільки фронтенд і бекенд повністю розділені, можна використовувати різні реалізації фронтенду та технології бекенду, якщо вони відповідають вимогам API-контракту.

На даний момент єдиним доступним бекендом є Java, але я планую розширити бібліотеки для C# та Go в майбутньому (сподіваюся, до 2025 року).

Frontend-компонент Mateu базується на Vaadin design system, що забезпечує елегантний і послідовний користувацький досвід.

Проблематика

Нашим завданням є створення простої системи бронювання. Зокрема, ми хочемо побудувати платформу для колл-центру, що дозволяє керувати бронюваннями (включаючи створення нових бронювань, зміни, скасування), виставленням рахунків та оплатою.

Нижче наведена схема, яка висвітлює наші функціональні вимоги та показує потік користувача:

pic

Потік користувача / вайрфрейм

З точки зору нефункціональних вимог, інтерфейс користувача, представлений для користувачів колл-центру, повинен бути єдиним, навіть якщо кожна команда розробників буде відповідати за власні інтерфейси. Крім того, у наших командах немає розробників фронтенду.

Наша кінцева мета — створити інтерфейс, який інтегрує меню та екрани з різних підсистем (або мікросервісів, у нашому випадку). Ці екрани також будуть включати компоненти з інших підсистем. Ми хочемо уникнути використання Iframe та забезпечити безшовну інтеграцію по всій системі. Крім того, ми не хочемо мати кілька вебсайтів з різними URL, навіть якщо вони підключені через одноразовий вхід (SSO) і використовують одну й ту саму дизайн-систему.
Натомість ми хочемо один URL з єдиним, інтегрованим меню.

Простір рішень

Для нашої системи бронювання припустимо, що ми визначили два окремих піддомені: бронювання та фінанси. Кожен піддомен відповідає за окремий обмежений контекст, де концепти мають сталі значення в межах цього контексту.

pic

Наша схема контекстів

Контекст бронювання охоплює всі аспекти життєвого циклу бронювання, в той час як контекст фінансів зосереджений на виставленні рахунків та оплаті. Для полегшення комунікації між ними обидва контексти мають спільну кодову базу, зокрема визначення подій.

Мікросервіси

Для кожного обмеженого контексту ми створимо мікросервіс, яким керуватиме окрема команда, відповідальна за весь набір функцій — як фронтенд, так і бекенд.

Загалом ми визначимо п’ять мікросервісів:

  1. Один мікросервіс для кореневого інтерфейсу користувача.
  2. Один мікросервіс для контексту бронювання, який надаватиме свої власні компоненти UI.
  3. Один мікросервіс для контексту фінансів, який надаватиме свої власні компоненти UI.
  4. Один мікросервіс для API Gateway, який маршрутизуватиме запити до відповідного мікросервісу.
  5. Один мікросервіс для збирання даних в запитну базу даних, яка агрегує дані з різних мікросервісів.

Інфраструктура

Наступна схема допомагає нам візуалізувати кінцеву картину:

pic

Оглядова схема

Коли користувач вводить URL нашої системи в браузері, запит спочатку проходить через CDN (наприклад, Cloudflare), потім потрапляє до балансувальника навантаження, який направляє його до одного з наших вузлів Kubernetes. Звідти запит проксіюється до API Gateway, який, врешті-решт, передає його до відповідного мікросервісу. Лише початковий HTTP-запит з браузера повертає HTML, тоді як усі наступні запити — це виклики REST API, які здійснюються веб-компонентами Mateu.

Бази даних

Кожен мікросервіс має свою базу даних, і, додатково, ми дозволяємо мікросервісу бронювання виконувати запити до спеціалізованої бази даних для читання, яка агрегує дані з обох мікросервісів: бронювання та фінансів. Зв'язок між мікросервісами та їх базами даних ілюструється на наступній схемі:

pic

Потік даних між мікросервісами

Мікросервіси бронювання та фінанси читають і записують у свої власні бази даних, при цьому генерують події в загальний Kafka-топік. Водночас мікросервіс CQRS-sink споживає події з мікросервісів бронювання та фінанси, щоб заповнити базу даних для читання. Ця база даних для читання потім використовується мікросервісом бронювання для побудови списку бронювань, що містить як оперативні, так і фінансові дані. З іншого боку, мікросервіс фінансів слухає події, що виникають у мікросервісі бронювання, для оновлення своєї бази даних.

Те, що ми вибрали різні системи управління базами даних для кожного мікросервісу, підкреслює автономність кожного мікросервісу.

Проєкт

Для побудови нашої системи ми створимо монорепозиторій, і структура проєкту виглядатиме наступним чином:

pic

Структура проєкту

Тут нема багато що пояснювати. Кожен мікросервіс має свою власну папку, яка видна на кореневому рівні.

Від цього моменту ми відкладемо бізнес-логіку та деталі реалізації піддоменів і зосередимося виключно на розробці інтерфейсу користувача, зокрема на аспекті композиції.

Інтерфейс користувача

Наш інтерфейс користувача матиме меню з різних обмежених контекстів: одне, пов'язане з життєвим циклом бронювання, і інше, пов'язане з фінансами.

Існує кілька способів визначити кореневий інтерфейс користувача, але оскільки у наших командах немає розробників фронтенду, ми будемо використовувати Mateu для цієї мети.
Ми оберемо складання на стороні клієнта (замість складання на рівні edge чи сервера), оскільки це найпростіший підхід для інтеграції різних мікрофронтендів. Крім того, ми не покладаємося на залежності Maven для складання макро UI. Натомість ми хочемо справжні мікрофронтенди, які можна розгортати незалежно один від одного разом із відповідними мікросервісами, на їх власний розсуд. Цей підхід також допомагає уникнути необхідності створювати API, спеціально призначені для підтримки UI.

Тепер давайте подивимося, як UI визначається в різних мікросервісах.

UI бронювання

UI бронювання створюється для того, щоб представити меню та різні компоненти, визначені та керовані мікросервісом бронювання.

Наступний код можна використовувати для визначення UI бронювання:

 // ЦЕ Є В МІКРОСЕРВІСІ БРОНЮВАННЯ  

class BookingMenu {  

 @MenuOption // (2)  
 BookingsCrud bookings;  

}  

@MateuUI("/booking") // (1)  
public class BookingUI {  

 @Submenu  
 BookingMenu booking; // "booking" буде ідентифікатором меню  

}

Отже, нам потрібно лише визначити клас, де ми:

  1. Оголошуємо його як UI та прив’язуємо до маршруту “/booking”.
  2. Додаємо функціональність CRUD для бронювань до меню.

Щоб поділитися меню бронювання, ми можемо відкрити UI бронювання, вибрати пункт меню і взяти частину URL, яка нас цікавить.

pic

UI бронювання після вибору одного з пунктів меню бронювань

На скріншоті вище ми повинні зосередитися на URL (https://article2.mateu.io/booking#bookingbookings_). Ключова частина URL — booking#booking, оскільки ми будемо споживати меню з того самого домену. Усередині booking#booking перша частина ідентифікує UI бронювання, а друга частина — меню.

UI фінансів

UI фінансів створюється для того, щоб представити меню та різні компоненти, визначені та керовані мікросервісом фінансів.

Ось код для визначення UI фінансів:

 // ЦЕ Є В МІКРОСЕРВІСІ ФІНАНСІВ  

class FinancialMenu {  

 @MenuOption  
 InvoiceCrud invoices; // (2)  

 @MenuOption  
 PaymentCrud payments; // (3)  

}  

@MateuUI("/financial") // (1)  
public class FinancialUI {  

 @Submenu  
 FinancialMenu financial; // "financial" буде ідентифікатором меню  

}

Знову ж таки, нам потрібно лише визначити клас, де ми:

  1. Оголошуємо його як UI та прив’язуємо до маршруту “/financial”.
  2. Додаємо CRUD для рахунків до меню.
  3. Додаємо CRUD для платежів до меню.

Щоб поділитися меню фінансів, ми можемо відкрити UI фінансів, вибрати пункт меню і взяти частину URL, яка нас цікавить.

pic

UI бронювання після вибору одного з пунктів меню бронювань

На скріншоті вище ми повинні зосередитися на URL (https://article2.mateu.io/financial#financialinvoices_). Ключова частина URL — financial#financial, оскільки ми будемо споживати меню з того самого домену. Усередині financial#financial перша частина ідентифікує UI фінансів, а друга частина — меню.

UI оболонки

Єдина мета нашого UI оболонки — це надання домашньої сторінки та агрегування меню з різних мікросервісів.

Для цього ми вже визначили UI в кожному мікросервісі (Бронювання та Фінанси), кожен з яких має своє меню.
Тепер, в окремому мікросервісі, ми визначимо кореневий (або оболонковий) UI, який буде агрегувати ці меню.

Використовуючи Mateu, нам потрібно всього лише один клас Java для визначення кореневого UI:

 // ЦЕ Є В МІКРОСЕРВІСІ КОРЕНЕВОГО UI  

@Title("Home")  
public class Home { // контент домашньої сторінки  
 @RawContent  
 String content = """  


Привіт! :)  
Це система бронювання, побудована за допомогою Mateu!
       """;   }   @MateuUI("") // (1)   @PageTitle("Система бронювання Mateu")   @Title("Home")   @AppTitle("Моя система бронювання")   @FavIcon("/mateu-favicon.png")   public class ShellUI extends Home implements HasLogo {       @MenuOption(remote=true) // (2)    String booking = "booking#booking";        @MenuOption(remote=true) // (3)    String financial = "financial#financial";        // ...      } ```  

В цьому коді ми просто:

1. Оголошуємо його як UI та асоціюємо з маршрутом `/`.
2. Додаємо меню бронювання, яке буде динамічно завантажене браузером під час виконання з URL `booking#booking`.
3. Додаємо меню фінансів, яке буде динамічно завантажене браузером під час виконання з URL `financial#financial`.

Зверніть увагу, що ми створили клас `Home` для визначення контенту домашньої сторінки, а клас `ShellUI` відповідає за визначення меню, дотримуючись принципу розділення обов'язків.

Вищенаведений код призведе до наступного контенту, що буде відображений, коли користувач відкриє вебсайт:

                                            ```  

Зверніть увагу, що для вбудовування нашого UI в будь-яку HTML-сторінку потрібен лише один скрипт (той, що з src="/dist/assets/mateu.js") та теги mateu-ui.

HTML вище буде відображатися наступним чином:

pic

Скріншот домашньої сторінки

Як будується меню?

Ця діаграма послідовностей показує, як меню програми динамічно конструюється під час виконання:

pic

Діаграма послідовності побудови меню

Діаграма вище підкреслює, що це складання на стороні клієнта. Це веб-компонент Mateu, який працює в браузері, запитує необхідну інформацію з різних мікросервісів і потім конструює меню.

Як показано на наступних скріншотах, користувач сприймає меню як єдине, об'єднане ціле, хоча підменю визначаються різними мікросервісами.

pic

Меню, що надходить з мікросервісу бронювання

pic

Меню, що надходить з мікросервісу фінансів

Зверніть увагу, що коли будь-який з цих пунктів меню вибирається, мікрофронтенд з відповідного мікросервісу відображається користувачу. Додаючи ці меню до нашого додатку, ми ефективно підключаємо всі базові компоненти.

pic

Crud для бронювань у кореневому додатку

Перегляди, що містять мікрофронтенди

Тепер давайте подивимося, як створити перегляд, що включає мікрофронтенд, визначений в іншому мікросервісі.

Ми використаємо перегляд бронювання як приклад складеного перегляду, який включає мікрофронтенди з інших піддоменів.
Перегляд бронювання відображатиме інформацію безпосередньо з бронювання, а також фінансове резюме у вигляді мікрофронтенду з фінансового обмеженого контексту.

Фінансовий Мікрофронтенд Резюме

Фінансовий мікрофронтенд резюме буде визначений за допомогою класу Java в фінансовому мікросервісі, як показано нижче:

 // ЦЕ Є В ФІНАНСОВОМУ МІКРОСЕРВІСІ  

@MateuUI("/financial/bookingreport")  
@Service  
@Title("")  
public class BookingReport implements ConsumesContextData {  

 //...  

 @Output@Money  
 double value;  
 @Output@Money  
 double invoiced;  
 @Output@Money  
 double paid;  
 @Output@Money@Bold  
 double pending;  

 //...  

 @Button(type = ActionType.Secondary, target = ActionTarget.NewModal)  
 @Width("100%")  
 Callable createInvoice;  
 @Button(type = ActionType.Secondary, target = ActionTarget.NewModal)  
 @Width("100%")  
 Callable registerPayment;  

 /...  

 private void load() {  
 var data = getBookingReportDataUseCase  
 .handle(new GetBookingReportDataRequest(id));  
 this.value = data.value();  
 this.invoiced = data.invoiced();  
 this.paid = data.paid();  
 this.pending = data.value() - data.paid();  
 }  

 @Override  
 public void consume(Map context,   
 ServerHttpRequest serverHttpRequest) {  
 id = (String) context.getOrDefault("bookingId", "");  
 load();  
 }  
}

Код вище визначає форму, яка відображає фінансову інформацію для бронювання, а нижче ви можете побачити, як вона виглядатиме для користувача, коли буде вбудована в UI.

pic

Фінансова інформація по бронюванню в мікрофронтенді

ID бронювання отримується з даних контексту, які, як ми побачимо пізніше, будуть встановлені в BookingView, коли ми визначимо об'єкт MicroFrontend. Також зверніть увагу на маршрут, вказаний в анотації @MateuUI (/financial/bookingreport), який буде використаний пізніше, коли ми будемо визначати об'єкт MicroFrontend.

Додавання Фінансового Мікрофронтенду Резюме до Перегляду Бронювання

Наш код для перегляду бронювання виглядатиме приблизно так:

 // ЦЕ Є В МІКРОСЕРВІСІ БРОНЮВАННЯ  

@Title("")  
@Service  
@Scope("prototype")  
class BookingInfoSection implements HasStatus {  

 //...  

 @NotEmpty  
 String leadName;  
 @NotEmpty  
 String service;  
 @NotNull  
 LocalDate serviceStartDate;  
 @NotNull  
 LocalDate serviceEndDate;  
 @NotNull  
 BigDecimal value;  



 @MainAction // ...  
 BookingsCrud back() {  
 // ...  
 }  

 @MainAction // ...  
 Mono cancelBooking(BigDecimal newValue) {  
 // ...  
 }  

 @MainAction  
 Mono save() {  
 // ...  
 }  

 //...  
}  


@Service  
@Scope("prototype")  
public class BookingView implements Container {  

 //...  

 @HorizontalLayouted  
 List content;  

 //...  

 public Mono load(String id) {  
 this.id = id;  

 var financialSection = ;  

 main = new MyHorizontalLayout(bookingInfoSection, financialSection);  

 return findBookingUseCase.handle(  
 new FindBookingRequest(new BookingId(id)))  
 .map(i -> {  

 content = List.of(  

 new BookingInfoSection(  
 i.id(),  
 i.leadName(),  
 // ...   
 ),  

 new MicroFrontend(  
 "/financial/bookingreport",  
 Map.of("bookingId", id))  

 );  

 // ...  

 return i;  
 }).then(Mono.just(this));  
 }  

 //...  

}

У цьому фрагменті коду ми визначаємо клас Java під назвою BookingView, який містить два компоненти, розташовані в горизонтальному лейауті. Перший компонент, BookingInfoSection, є формою, яка дозволяє переглядати та редагувати деталі бронювання. Другий компонент, визначений за допомогою MicroFrontend, представляє мікрофронтенд, що надходить з фінансового мікросервісу, як ми бачили раніше. Зверніть увагу, що значення, яке використовується при визначенні мікрофронтенду, є відносним URL для фінансового резюме: /financial/bookingreport.
Додатково, ID бронювання передається через дані контексту.

pic

Мікрофронтенд перегляду бронювання

В результаті, коли користувач переходить до перегляду деталей бронювання, він сприймає це як єдиний, об'єднаний вигляд, хоча насправді це композиція різних мікрофронтендів з різних мікросервісів.

pic

Мікрофронтенди бронювання та фінансів разом

Зверніть увагу, що кнопки Створити рахунок та Зареєструвати платіж праворуч відкриватимуть модальні вікна та виконуватимуть дії, повністю контрольовані фінансовим мікросервісом, тоді як кнопки Скасувати бронювання та Зберегти зліва визначаються та управляються мікросервісом бронювання.

Давайте переглянемо, як це все працює

Як згадувалося раніше, середовище виконання Mateu — це фактично веб-компонент, що споживає простий REST API. Існують два основні веб-компоненти в Mateu на стороні фронтенду: mateu-ui та mateu-ux. Обидва компоненти приймають базовий URL для API як атрибут, що дозволяє підключати їх до різних кінцевих точок, сумісних з API.

Важливо зазначити, що це завжди екземпляри одного і того ж веб-компонента, які рендерять різний контент, залежно від даних, що отримуються з REST API, що надаються різними мікросервісами.

Як ми бачили, веб-компонент mateu-ui включає логіку для отримання віддалених меню, що дозволяє об'єднувати різні підменю з різних кінцевих точок, наданих різними мікросервісами. З іншого боку, веб-компонент mateu-ux може містити інші компоненти mateu-ux, кожен з яких має різні базові URL API, тим самим дозволяючи комбінувати кілька мікрофронтендів.

pic

Огляд мікрофронтендів

Зверніть увагу, що бекенди Mateu не зберігають стан, що робить їх ідеальним вибором для нашої архітектури мікросервісів. Такий підхід дозволяє запускати кілька реплік кожного поду з можливістю перезапуску в будь-який час без впливу на функціональність системи.

Примітка: Зверніть увагу на Cross-Origin Resource Sharing (CORS). Оскільки ми використовуємо композицію на стороні клієнта, браузер блокуватиме будь-які HTTP-запити до URL-адрес за межами нашого домену. Якщо вам потрібно отримувати мікрофронтенди з різних доменів, вам необхідно додати відповідні заголовки Access-Control-Allow-Origin до кінцевих точок.

Висновок

Ми успішно створили складний UI, розподілений по кількох мікрофронтендах, кожен з яких потенційно керується різними командами бекенду, які володіють своїми функціями від початку до кінця, з незалежними темпами розгортання.

Важливо відзначити мінімальну кількість коду, яку ми написали. Ми не використовували HTML або JavaScript і не писали жодних API для підтримки нашого UI.

Додатково, фронтенди Mateu — це, по суті, просто звичайні веб-компоненти. Ви можете вбудовувати їх куди завгодно — чи то в просту HTML-сторінку, Jamstack-додаток, побудований за допомогою React, Vue, Angular тощо, чи навіть в сайт на WordPress — як справжні мікрофронтенди.

Подяки

Щира подяка моїй дружині, Антонії, за рецензування цієї статті та зроблення її зрозумілою і доступною для всіх.

Джерела

Перекладено з: Mateu: a different approach to micro frontends

Leave a Reply

Your email address will not be published. Required fields are marked *