Микросервисы во фронтенде

от автора

image

Привет, Хабр!

Микросервисы уже давно стали классикой в бэкенд архитектуре, но в отличие от бэкенда, во фронтенде одновременно можно использовать только один фреймворк. Это порождает неудобства при масштабировании и переиспользовании модулей. Микросервисный подход избавляет от необходимости придерживаться конкретного фреймворка и дает возможность программисту сосредоточиться на конкретном модуле с использованием наиболее удобных для него инструментов. Мы не пропагандируем расширение зоопарка технологий и использование каждого фреймворка необходимо тщательно обдумать, прежде чем внедрять. Подробнее — в статье от разработчика команды 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 с простым примером приложения.

Выводы

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

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

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

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

Так же мы можем разрабатывать модули заточенные под определенную платформу и в конфиге указывать какой модуль для какой платформы подгружать.

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

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

ссылка на оригинал статьи https://habr.com/ru/post/518664/