Привет, Хабр!
Микросервисы уже давно стали классикой в бэкенд архитектуре, но в отличие от бэкенда, во фронтенде одновременно можно использовать только один фреймворк. Это порождает неудобства при масштабировании и переиспользовании модулей. Микросервисный подход избавляет от необходимости придерживаться конкретного фреймворка и дает возможность программисту сосредоточиться на конкретном модуле с использованием наиболее удобных для него инструментов. Мы не пропагандируем расширение зоопарка технологий и использование каждого фреймворка необходимо тщательно обдумать, прежде чем внедрять. Подробнее — в статье от разработчика команды BSL gyk007.
Сразу спойлер — уже готов мини фреймворк, который легко позволит вам это сделать.
Начну с того, зачем это нужно
Еще совсем недавно web был простым. Для успешной работы необходимо было знать html, css и популярную на тот момент, библиотеку jquery. На этом знания фронтенд-программиста заканчивались, да и за программистов их особо не считали, так, верстальщики или веб-мастера. Но веб стремительно развивается, интерфейсы становятся все сложнее и сейчас для многих проектов, в которых нет HighLoad и микросервисов (а они в большинстве случаев и не нужны), клиентская часть становится сложнее, чем серверная, и время жизни кода на клиенте обычно меньше, чем на сервере.
Также появляется целая армия фреймворков, которые по сути делают одно и то же, но разными способами. И программисты выбирают свой стек: кто-то React, кто-то Vue и так далее.
Но наше веб-приложение живет долго, а фреймворки и UI-библиотеки меняются быстро. В итоге через пару лет мы получаем легаси, который хочется переписать, но это долго, а бизнес не готов за это платить — приложение же работает.
Я думаю, многие программисты слышали про закон Иглсона: Ваш код, который вы не просматривали 6 или более месяцев, выглядит так, будто его написал кто-то другой
.
С ростом количества новых фич, приложение становится все сложнее и запутаннее. Спустя некоторое время, можно не узнать и свой код, не говоря уже про код других программистов на проекте, в итоге получаем огромный монолит с кучей легаси, который способен уничтожить все желание работать. Эти проблемы можно решить хорошим код ревью, но на это никогда нет времени.
Отдельно стоит затронуть тестирование — чем больше фич и чем больше само приложение, тем дольше его тестировать. А перед каждым релизом нужно выполнять долгий и нудный регресс.
Учитывая все вышесказанное, хотелось бы иметь не один большой монолит, который хочется переписать, а набор микросервисов (назовем их модули, каждый из которых максимально изолирован, чтобы при необходимости мы могли быстро его заменить или написать новый на другом стеке (если, конечно, это нужно). И тестировать только те модули, код которых менялся.
Чтобы решить все проблемы, я написал фреймворк, который легко позволяет делить приложение на модули, причем каждый модуль при необходимости может иметь свой стек (React, Vue, Angular, Webix …. )
Что мы получили
Модули (микросервисы)
Любое веб-приложение можно разбить на модули (хедер, футер, окно с чатом, боковое меню и т.д..
Вот основные их типы:
- Root-модуль — главный модуль, один на все приложение. Отвечает за взаимодействие между модулями.
- Page-модуль — понятно из названия, модуль-страница.
- Layout-модуль — это тоже должно быть понятно, модуль-шаблон для страницы.
- Global-модуль — модуль который можно вызвать из любой страницы нашего приложения (например окно с чатом, или уведомления).
- Embed-модуль — модуль который встраивается в Page-модуль или в Global-модуль.
Все модули устроены однотипно. Они просто встраиваются в DOM дерево. Если наш Page-модуль встраивается в Layout-модуль, то Layout-модуль должен иметь контейнер с id (<div id='content'></div>
), а Page-модуль должен туда монтироваться. Все довольно просто.
Также все модули динамически импортируются, клиент загружает файл с кодом модуля только тогда, когда это ему необходимо.
В итоге мы получаем примерно такую структуру:
|--project_name_dir
|--src
|--modules
|--Root
|--root.js
...
|--Module1
|--module.js
...
|--Module2
|--module.js
...
...
Перед тем как все это заработает, необходимо описать config файл.
Конфиг файл
import Root from 'Root/module';
export default {
// роутинг с помощю history Api или hash.
historyApi: false,
// корневой путь для приложения ('/example/path/').
rootPath: '/',
// класс Root-модуля.
rootModule: Root,
// название модуля главной страницы.
mainModule: 'main',
// названия модуля страницы 404
module404: 'notfound',
// функция для динамического импорта модуля.
// module - название модуля и название директории модуля.
import: async (module) => await import(./modules/${module}/module),
modules: {
auth: {
// Название модуля - названия директории, в которой находится файл с модулем.
module: 'ExampleAuth',
},
// Ключ Page-модуля отвечает за название роута для этой страницы.
main: {
layout: 'ExampleLayoutWebix',
module: 'ExampleWebix',
embed: {
// В этот модуль мы встраиваем ExampleEmbed-модуль.
example: {
module: 'ExampleEmbed',
},
},
},
notfound: {
layout: 'ExampleLayoutWebix',
module: 'ExampleError404',
},
// Глобальный модуль флаг global: true,
globalwnd: {
global: true,
module: 'ExampleGlobalWnd',
embed: {
example: {
module: 'ExampleEmbedGlobal',
},
},
},
globalnotification: {
global: true,
module: 'ExampleNotification',
},
},
};
Наш конфиг файл может иметь дополнительные поля, если это необходимо:
main: {
module: 'PageMain',
layout: 'Layout',
icon: icon,
title: 'Главная Страница',
inMenu: true,
}
Мы можем использовать разные модули для различных условий:
main: {
layout: window.innerWidth < 1000 ? 'Layout_1' : 'Layout_2' ,
module: window.innerWidth < 1000 ? 'Module_1' : 'Module_2 ,
embed: {
example: {
module: window.innerWidth < 1000 ? 'Embed_1' : 'Embed_2' ,
},
},
},
Теперь поговорим об устройстве модуля
Все модули, за исключением Root-модуля, наследуются от класса Onedeck.Module
.
Класс модуля имеет внутри реализацию паттерна одиночка (Singleton) и наблюдатель (Observer).
То есть объект создается только 1 раз и может подписываться и публиковать события.
Пример Модуля:
import Onedeck from 'onedeck';
import App from 'ExampleModule/App.vue';
import Vue from 'vue';
/**
* Class ExampleModule
* module use Vue
*/
export default class ExampleModule extends Onedeck.Module {
// Хук жизненного цикла init.
// Срабатывает при инициализации нашего модуля
init (path, state, queryParam) {
console.log('init', this.constructor.name, path, state, queryParam);
this.VueApp = new Vue(App);
this.eventHandler();
}
// Обработчик событий для данного модуля.
eventHandler () {
this.$$on('onAuth', () => this.$$rout({
path: '/main/',
state: null,
}));
}
// Хук жизненного цикла dispatcher.
// Срабатывает при переходе на url, тут можно описать логику при переходе.
dispatcher (path, state, queryParam) {
console.log('dispatcher', this.constructor.name, path, state, queryParam);
}
// Хук жизненного цикла mounted.
// Срабатывает когда модуль смонтирован в DOM дерево.
mounted (module, layout) {
console.log('mounted', this.constructor.name, module, layout);
}
// Хук жизненного цикла destroy.
// Срабатывает когда нам нужно очистить DOM дерево от нашего модуля
destroy () {
this.$$offAll()
this.VueApp.$destroy();
document.getElementById('root').innerHTML = '';
}
}
Про хуки жизненного цикла модуля вы можете более подробно почитать в документации. Также вы можете почитать в документации про Root-модуль. Его код почти идентичен коду обычного модуля.
Роутинг
Как вы уже заметили, в конфиге мы можем легко переключить вид роутинга с помощью конфига:
// роутинг с помощю history Api или hash
historyApi: false,
Каждый модуль имеет метод $$rout:
import Module from 'Example/module';
// Так как наш модуль реализует паттерн Одиночка (Singleton), мы получим текущий объект модуля.
const module = new Module()
module.$$rout({
// Указываем путь, первый элемент пути - название модуля
path: '/module_name/item/1',
// Данные, которые мы хотим передать по указанному пути:
state: {id: 1, name: 'example'},
})
Далее path
и state
можно получить в хуках жизненного цикла init
и dispatcher
.
Общение между модулями
Общение модулей происходит через события. Каждый модуль может вызывать два типа событий:
- события модуля с помощью метода
$$emit
; - глобальные события с помощью метода
$$gemit
;
События модуля создает сам модуль с помощью методов $$on
или $$onOnce
. А глобальные события создаем Root-модуль также с помощью методов $$on
или $$onOnce
.
Более подробно о событиях вы можете почитать в документации. Также там есть ссылка на github с простым примером приложения.
Выводы
Итак, вместо монолита мы получили набор независимых модулей. Каждый модуль внутри себя содержит мини-приложение, которое можно отдельно собрать и протестировать.
При командной разработке программист может сосредоточится только на коде своего модуля, не вникая в архитектуру приложения и в код остальных модулей.
При необходимости мы можем использовать различные фреймворки в одном приложении.
Теперь нам не нужно переписывать все приложение, мы можем переработать только конкретные модули и можем даже переписать их на другом фреймворке.
Так же мы можем разрабатывать модули заточенные под определенную платформу и в конфиге указывать какой модуль для какой платформы подгружать.
У нас есть методы жизненного цикла модуля, в которых мы можем описывать логику работы в конкретный момент жизненного цикла модуля.
Каждый модуль динамически импортируется при необходимости, что существенно ускоряет первую загрузку.
akuzmin
Использование нескольких фреймворков резко увеличит размер render-blocking resources. Также породит много дубликатов кода. Это сделает первоначальную загрузку сайта намного более долгой, что для современного проекта недопустимо.
www.scientiamobile.com/mobile-site-visitors-abandon-more-than-3-seconds
gyk007
Ни в коем случае мы не призываем использовать различные фреймворки в 1 проекте. Это бывает необходимо только в определенных сценариях. Но возможность быстро сменить фреймворк является плюсом. И этот фреймворк больше подходит для реализации админ панелей, там где не нужно использовать SSR. По поводу скорости первой загрузки, так как все модули динамически импортируются, клиент загружает только необходимый бандл.
akuzmin
Статья ведь о микросервисах во фронтенде. Микросервисы предполагают, что часть функционала дублируется в каждом сервисе, и при загрузке грузятся сразу несколько сервисов, пусть даже в одном бандле. Это делает проект тяжеловесным.
По моему опыту во фронтенде, любые дополнительные уровни абстракции никогда не являются полностью «плюсом». Это всегда компромисс. И чем больше этих абстракций, тем более этот компромис в сторону минуса, чем плюса. Как бы красиво что-то не выглядело с точки зрения архитектуры или креативных идей, во фронтенде они все в конечном счете проходят проверку вопросами производительности. В общем все, что выполняется у пользователя в браузере, должно быть с технической точки зрения как можно более простым — если вы хотите, чтобы он заходил на ваш сайт часто. А тяжеловесные абстракции и новейшие архитектурные идеи лучше оставить там, откуда они пришли и где хорошо работают — в бекенде.
gyk007
Если вам нужно что-то простое и быстрое, нет ничего быстрее статики и этот подход часто оправдан, особенно для лендингов. Но для сложных веб приложений нужно хорошо продумывать архитектуру. Это делается для того, чтобы ваше приложение можно было легко масштабировать, легко добавлять новый функционал, проверять бизнес теории. Иначе, это все может превратиться в ад из кода, и каждый новый разработчик первым делом будет предлагать все переписать. Чтобы избежать дублирование кода, можно вынести свои ui компоненты (инпуты таблицы модальные окна ...) в отдельную библиотеку.
klerick
интересно что скажет бизнес, когда Вы придете и скажите, вот для того чтоб мы избавиться от вендор лока, нам нужна универсальная UI либа, и для этого нам нужно n-ое количество человека часов. Я бы с попкорном понаблюдал за этим)
Когда пишуться приложения, обычно происхоидт так, а давайте что нибудь сделаем посмотрим что будет, сделали, посмотрели. Что дальше?
Бизнес: Надо добавить фичу
Программист: Надо сделать рефаткоринг
Бизнес: Фича важнее.
и так по кругу.
gyk007
вот для этого случая я и создал этот фреймворк. Готовых ui либ много, под любой фреймворк.
Бизнес говорит давайте быстро что-то сделаем — вы ему быстро пишете новый модуль. И вам не нужно думать о том, что вы сейчас испортите все приложение, новый модуль гораздо проще рефачить чем все приложение. Если бизнесу идея не зашла этот модуль можно легко удалить, и не нужно думать что что-то может поломаться.
klerick
готовых Ui либ много, но чтоб они были одинаковые для всех фреймворков я пока знаю только один. А нужно чтоб не только выглядели одинаково компоненты, но нужно чтоб еще чтоб поведение было одинаковое. Я ниже написал, микрофрнтенд это скорей процесс, чем разработка. И этот процесс нужно будет воткнуть в текущую инфраструктуру. Начиная работы с репозиторием, версионирования модулей, инркементальный деплой, процесс сборки. Я все думаю описать все то, с чем я столкнулся и как это решал.
gyk007
Еще раз мы не призываем использовать разные фреймворки и городить зоопарк технологий. Но бывают сценарии когда это необходимо, но это компромисс, и на моей практик такие компромиссы были. Бывает когда хочется перейти на другой фреймворк, или попробовать новый фремворк, так как в мире js они часто появляются. Это можно легко сделать, не переписывать все с 0, а только для новых задач использовать новые инструменты, и со временем переписать старые модули на новые технологии. А если новые технологии не понравились, можно легко от них избавиться.
akuzmin
> Если вам нужно что-то простое и быстрое,
Нет, я имел в виду вообще любые веб-проекты. Любой проект, как бы он сложно не выглядел, во фронтенде он должен быть как можно более производительным, а значит как можно более простым.
> Это делается для того, чтобы ваше приложение можно было легко масштабировать, легко добавлять новый функционал, проверять бизнес теории
Когда на первом месте стоит пользователь этого приложения, а не модные идеи/удобная работа в команде/повышенная легкость масштабирования/принесенный опыт архитектур из java/желание вписать в резюме новый стек и так далее, то идея использовать микросервисы во фронтенде просто не возникнет.
> Чтобы избежать дублирование кода, можно вынести свои ui компоненты (инпуты таблицы модальные окна ...) в отдельную библиотеку.
Во фронтенде каждый микросервис будет иметь свои зависимости, у каждой зависимости будет своя версия и так далее. Если выносить общие зависимости в отдельную библиотеку, то каждая команда должна будет постоянно это обсуждать — например, перед апдейтом какого-то плагина из общей библиотеки с версии 1.0.17 до 1.0.18, и соотвественно вносить изменения в каждый из своих сервисов. Это нивелирует идею микросервисов (параллельная разработка и уменьшение зависимостей между командами), и в итоге только добавляет излишнюю сложность.
gyk007
> Производительность зависит не от данного фреймворка, а от реализации определенных модулей и компонентов. Данный фремворк не работает с DOM деревом. Он просто создает и уничтожает модули.
>Еще раз — каждый модуль собирается в отдельный бандл и подгружается только тогда когда пользователь этот модуль использует.
> Если вам нужно поменять версию библиотеки, в любом случае вам нужно рефачить весь проект. Какой бы вы подход не использовали.
akuzmin
Производительность зависит не от вашего фреймворка, а от самого решения использовать микросервисы, так как это всегда новые абстракции + дублированный код
Подлючаемые модули будут работать с дом-деревом. Но даже это не столь важно. Ресурсы, блокирующие рендеринг, не обязательно работают с DOM-деревом.
Еще раз — в каждом модуле будет дублированный код, это неизбежно при использовании микросервисов
Посмотрите на свою папку node_modules, загляните в файл package-lock. Каждый плагин имеет версию. Есть громадное количество причин, почему нужно сделать апгрейд того или иного плагина. Часто это происходит автоматически, в зависимости от проекта. Ну и если это будет общая библиотека для всех микросервисов, то внесение любого нового плагина / неизбежные изменения в существующих все равно нужно будет согласовывать с каждым сервисом в отдельности.
gyk007
> Еще раз) Производительность зависит от реализации модуля и от фремворка (Vue React ...) Данный фрйемврок заменяем вам роутер. И отвечает за создание и уничтожения модуля.
> Проблема версий никуда не уйдет. Если вы используете 1 версию и решили ее поменять при любом подходе нужно эту проблему решать во всем приложении.
> Дублирование кода — это уже как вы решите. Если вы делаете полностью независимый модуль, который можно переиспользовать в другом проекте, тогда лучше сделать его максимально изолированный (даже со своими внутренними ui компонентами) зато потом вы его легко вставите в любой проект, подключите к api и все заведется. Если это не требуется, то можно использовать либо готовую либо написать свою ui библиотеку. И дублирование кода у вас сведется к минимуму или к 0. На моей практике дублирование кода не было. Каждый модуль отвечает за сваю бизнес логику, и даже работает с разными микросервисами на бэке.
akuzmin
Я не говорил, что проблема версий уйдет, если не использовать микросервисы — я говорил, что эту проблему придется решать для каждого сервиса отдельно. Это создает дополнительную сложность и нивелирует плюсы микросервисов как параллельной разработки.
В который раз говорю, это не так. Микросервисы как подход очень способствуют дублированию кода. Это неизбежно. Идея не нова (https://micro-frontends.org/), и то, что это добавляет абстракций и дубликатов, предполагается прямо в самом подходе микросервисов.
gyk007
> Объем работы по писку и замене уязвимых мест зависит не от подхода (микросервис, монолит), а от количества кода и тех мест где используется старая библиотека.
> То что в микросеврисах есть дублирование кода — это понятно, но в данном случае это можно решить и минимизировать эту проблему.
klerick
то то я думал что все сервисы которыми я пользуюсь грузятся по несколько минут)
Я пришел к тому что на данный момент, продуктовые проекты, уже давно на скорость рендеринга забили. Главное чтоб красиво было)
gyk007
Значит это плохие сервисы, программисты не думали над архитектурой, и как в каких случаях доставлять контент пользователю.
klerick
я вас умоляю))) не всегда программист может сделать хорошо, даже если очень хочет. если руководство, мягко сказать, другого взгляда) Классика ведь, ты говоришь нужно сделать вот так, а тебе, давай сделаем тут костыль, тк нужно показать красивые графики релизов. Руководству важно, что бы была красивая картинка. А как там работает, ведь у руководства крутой мак.
gyk007
Ну так тогда может и не нужно париться по поводу производительности)) если руководству и так норм) Мы же разрабатываем продукт не для себя, а для конкретных пользователей) если пользователи руководство, а у них крутые маки, можно сделать максимально просто и не думать об производительности )
klerick
так в том говне разбираться нам) и когда собираешь проект, и получаеться ~50 метров билд в зипе, становиться грустно, а сделать то не чего не можешь)
gyk007
Для этого и можно использовать наш фреймворк. Новая хотелка — новый модуль. Каждый модуль собирается в отдельный бандл. Понравилась бизнес идея, быстро переработали модуль, сделали максимально все круто, не понравилась — просто удалили.
klerick
я не сколько не против микрофронта, а даже за, я к тому что в реальности достаточно сложно продвинуть. я сам сейчас именно этой задачей и занимаюсь, но тут скорей повезло с руководством. Проблема микрофрнтенда, это то что нет каких то бестпрактикс, нет готовый решений, тк микрофрнтенд, это скорей процесс, чем разработка. И вот тут начинаются проблемы, которые вот сейчас их решаю.
gyk007
Можете посмотреть наш фреймворк, и даже участвовать в развитии) Я открыт к критике.