too FAT SPA


Микросервисная архитектура уже давно де-факто стала стандартом при разработке больших и сложных систем. Она имеет целый ряд преимуществ: это и строгое деление на модули, и слабая связность, и устойчивость к сбоям, и постепенность выхода в продакшн, и независимое версионирование компонентов.

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

Сегодня я расскажу вам, как мы делали микросервисный фронт в нашем SaaS-решении и с какими проблемами столкнулись.

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


Изначально разработка в нашей компании выглядела так: есть много команд, занимающихся разработкой микросервисов, каждый из которых публикует свой API. И есть отдельная команда, которая занимается разработкой SPA для конечного пользователя, используя API разных микросервисов. При таком подходе все работает: разработчики микросервисов знают все об их реализации, а разработчики SPA знают все тонкости пользовательских взаимодействий. Но появилась проблема: теперь каждый фронтендер должен знать все тонкости всех микросервисов. Микросервисов становится все больше, фронтендеров становится все больше — и Agile начинает разваливаться, так как появляется специализация внутри команды, то есть исчезают взаимозаменяемость и универсальность.

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

  • Все модули разнородные, со своей спецификой. Для каждого модуля лучше подходят свои технологии. При этом выбор технологий — трудновыполнимая задача в условиях SPA.
  • Так как приложение SPA (а в современном мире это означает компиляцию в единый бандл или как минимум сборку), то одновременно могут делаться только выдачи всего приложения. Риск каждой выдачи растет.
  • Все сложнее заниматься управлением зависимостями. Разным модулям нужны разные (возможно, специфичные) версии зависимостей. Кто-то не готов перейти на обновленный API зависимости, а кто-то не может сделать фичу из-за баги в старой ветке зависимости.
  • Из-за второго пункта релизный цикл у всех модулей должен быть синхронизирован. Все ждут отстающих.

Режем фронтенд


Наступил момент накопления критической массы, и фронтенд решили разделить на… фронтендные микросервисы. Давайте определим, что такое фронтендный микросервис:

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

Но мы пошли дальше и ввели еще один уровень деления.

Понятие фрагмента


Фрагментом мы называем некий бандл, состоящий из js + css + дескриптора развертывания. По сути, это независимая часть UI, которая должна выполнять набор правил разработки, для того чтобы его можно было использовать в общем SPA. Например, все стили должны быть максимально специфичны для фрагмента. Никаких попыток прямого взаимодействия с другими фрагментами быть не должно. Необходимо иметь специальный метод, которому можно передать DOM-элемент, где фрагмент должен отрисоваться.

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

Подобный подход позволяет разместить два приложения, написанных на разных фреймворках, на одной странице. Также это дает возможность написать универсальный код, который позволит динамически подгружать нужные фрагменты на страницу, инициализировать их и управлять жизненным циклом. Для большинства современных фреймворков достаточно соблюдать «правила гигиены», чтобы это стало возможным.

В тех случаях, когда фрагмент не имеет возможности «сожительствовать» с другими на одной странице, есть fallback-сценарий, при котором мы отрисовываем фрагмент в iframe (решение сопутствующих проблем остается за рамками данной статьи).

Все, что нужно сделать разработчику, желающему использовать существующий фрагмент на странице, — это:

  1. Подключить скрипт микросервисной платформы на страницу.
    <script src="//{URL to static cache service}/api/v1/mui-platform/muiPlatform.js"></script>

  2. Вызвать метод добавления фрагмента на страницу.

    window.MUI.createFragment(
        // fragment name
        "hello-label",
    
        // fragment model
        {
            text: "HelloLabelFragment text from run time"
        },
    
        // fragment position
        {
            selector: ".hello-label-placeholder",
            position: "afterend"
        })
        .then(callback);
    


Также для общения фрагментов между собой есть шина, построенная на Observable и rxjs. Написана она на NativeJS. Кроме того, в SDK поставляются обертки для разных фреймворков, которые помогают использовать эту шину нативно. Пример для Angular 6 — утилитный метод, возвращающий rxjs/Observable:

import {fromEvent} from "@netcracker/mui-platform/angular2-factory/modules/shared/utils/event-utils"

fromEvent("<event-name>");
fromEvent(EventClassType);

Кроме того, платформа предоставляет набор сервисов, которые часто используются разными фрагментами и являются базовыми в нашей инфраструктуре. Это такие сервисы, как локализация/интернационализация, авторизационный сервис, работа с кросс-доменными куками, local storage и многое другое. Для их использования в SDK также поставляются обертки для разных фреймворков.

Объединяем фронтенд


Для примера можем рассмотреть такой подход в SPA админки (она объединяет разные возможные настройки с разных микросервисов). Содержимое каждой закладки мы можем сделать отдельным фрагментом, поставлять и разрабатывать который будет каждый микросервис по отдельности. Благодаря этому мы можем сделать простую «шапку», которая будет показывать соответствующий микросервис при клике на закладку.

image

Развиваем идею фрагмента


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

И тут нам тоже помогают фрагменты! Так как все, что нужно фрагменту, — это DOM-элемент для отрисовки, мы выдаем любому микросервису глобальный API, через который он может разместить любой фрагмент внутри своего DOM-дерева. Для этого достаточно передать ID фрагмента и контейнер, в котором ему надо отрисоваться. Остальное сделается само!
Теперь мы можем строить «матрешку» любого уровня вложенности и переиспользовать целые куски UI без необходимости поддержки в нескольких местах.

Часто бывает так, что на одной странице находятся несколько фрагментов, которые должны менять свое состояние при изменении неких общих данных на странице. Для этого у них есть глобальная (NativeJS) шина событий, через которую они могут общаться и реагировать на изменения.

image

Общие сервисы


В микросервисной архитектуре неизбежно появляются центральные сервисы, данные из которых нужны всем остальным. Например, сервис локализации, который хранит переводы. Если каждый микросервис в отдельности начнет лазить за этими данными на сервер, мы получим просто вал запросов при инициализации.

Для решения этой проблемы мы разработали реализации NativeJS сервисов, которые предоставляют доступ к таким данным. Это дало возможность не делать лишних запросов и кешировать данные. В некоторых случаях — даже заранее выводить такие данные на страницу в HTML, чтобы совсем избавиться от запросов.

Кроме того, были разработаны обертки над нашими сервисами для разных фреймворков с целью сделать их использование очень естественным (DI, фиксированный интерфейс).

Плюсы фронтендных микросервисов


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

  • очень четко разделенные зоны ответственности;
  • независимые выдачи: каждый фрагмент может иметь свой релизный цикл;
  • повышение стабильности решения в целом, так как выдача отдельных фрагментов не влияет на другие;
  • возможность легко откатывать фичи, выкатывать их на аудиторию частично;
  • фрагмент легко помещается в голове каждого разработчика, что приводит к реальной
взаимозаменяемости членов команды; кроме того, каждый фронтендер может глубже понять все тонкости взаимодействия с соответствующим бэкендом.

Решение с микросерисным фронтендом выглядит неплохо. Ведь теперь каждый фрагмент (микросервис) может сам решать, как деплоиться: нужен ли просто nginx для раздачи статики, полноценный middleware для агрегации запросов к бэкам или поддержки websockets либо еще какая-нибудь специфика в виде бинарного протокола передачи данных внутри http. Кроме того, фрагменты могут сами выбирать способы сборки, методы оптимизации и прочее.

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


Никогда нельзя обойтись без ложки дегтя.

  • Взаимодействие между фрагментами невозможно обеспечить стандартными ламповыми методами (DI, например).
  • Как быть с общими зависимостями? Ведь размер приложения будет расти как на дрожжах, если их не выносить из фрагментов.
  • За роутинг в конечном приложении все равно должен отвечать кто-то один.
  • Что делать, если один из фрагментов недоступен / не может отрисоваться.
  • Неясно, что делать с тем, что разные микросервисы могут находиться на разных доменах.

Заключение


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

К сожалению, в одной статье очень сложно осветить весь спектр проблем и решений, которые можно встретить на пути повторения такой архитектуры. Но для нас плюсы явно перевешивают минусы. Если Хабр проявит интерес к раскрытию подробностей реализации этого подхода, мы обязательно напишем продолжение!

Комментарии (50)


  1. BugM
    21.08.2018 13:51
    +1

    Всегда помните!
    Вам не нужны микросервисы если вся команда умещается за одним большим столом.


    1. SirEdvin
      21.08.2018 15:07

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


      1. BugM
        21.08.2018 15:14

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


        1. APXEOLOG
          21.08.2018 16:10

          А если вам нужно чтобы отдельные компоненты масштабировались независимо, но вы умещаетесь за одним столом, вам все равно нужно писать монолит?


          1. ShimON Автор
            21.08.2018 16:18

            Думаю, что вся проблема в понимании термина «монолит». Монолит — это не когда у тебя только один сервер с только одним аппликейшеном на нем. Монолит также может быть построен на сервисной архитектуре. Микросервисная архитектура — является частным примером сервисной. Так вот речь о том, что именно микросервисная архитектура не подходит для маленьких команд. Бенефитов меньше.
            Эта архитектура предполагает достаточно мелкое деление, которое не разумно для маленьких команд в связи с увеличивающейся стоимостью поддержки инфраструктуры.
            При этом сервисную (макросервисную) архитектуру никто использовать не запрещает по необходимости. Тут всегда вопрос компромисса между стоимостью поддеркжи, производительностью и масштабируемостью.


          1. BugM
            21.08.2018 17:14

            Компоненты != Микросервисы

            Микросервисы это очень большое количество формальных интерфейсов и накладных расходов на шины и прочее. Это все выражается в человекочасах и соответственно деньгах на разработку.
            В небольших компаниях эти деньги не окупятся никогда. Любую проблему можно решить эффективнее.


      1. nsinreal
        21.08.2018 18:51

        Нет, маленькая команда не сможет сделать сложный продукт, если слово «сложный» имеет смысл «большой». Так вот, маленькая команда не может сделать большой продукт, потому что ресурсы лимитированы.

        А сложный в плане «сложный» — да, может, но это не имеет отношения к любому механизму обеспечения модульности.


    1. ShimON Автор
      21.08.2018 15:10

      Просто для справки — в Netcracker работает более 3000 разработчиков. Среди них, думаю, не менее 100 чистых фронтендеров.


      1. BugM
        21.08.2018 15:13

        С вашими объемами все понятно. Я для читателей написал.

        Попытка сделать микросервисы в командах по 5-10 человек приводит к такому ужасу… Надо предупредить.


      1. nsinreal
        21.08.2018 18:52

        Вы бы такие вещи писали бы как варнинг в начале статье, это очень бы помогло миру.


  1. maxzh83
    21.08.2018 13:51

    По настроению в статье показалось, что «делаем, потому что можем», но, возможно, просто показалось. А что с производительностью? Я имею в виду насколько медленнее приложение, обросшее оверхедом и внутренними взаимодействиями, стало работать в браузере? В случае с бэком, это все скрыто множеством серверов, а тут это все вываливается клиенту в браузер.


    1. ShimON Автор
      21.08.2018 15:12

      Спасибо большое за коммент!
      Делаем, потому, что размеры решения очень большие и количество команд разработки растет как на дрожжах. Так что скорее показалось.

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


      1. recoder
        21.08.2018 19:50

        размеры решения очень большие и количество команд разработки растет как на дрожжах
        Паттерн «решение организационных проблем техническими способами». :)


        1. ShimON Автор
          21.08.2018 20:02

          Ну не скажи. Мы наоборот решили организационную проблему в бекенде (сильная связанность, боттл нек команды, отсутствие ответственного за функционал) методом деления на микросервисы. А дальше уже решили ту же самую проблему во фронтенде. Технический способ давно известен.


  1. Demetros
    21.08.2018 14:22

    Жаль, что не освещены подробности взаимодействия фрагментов между собой. Например, нам нужно вызвать «метод» (у фрагментов есть внешнее API?) из другого фрагмента, дождаться его асинхронного выполнения и получить результат. Или как решается к примеру проблема, когда сообщение публикуется, но не все подписчики еще инициализировались.


    1. ShimON Автор
      21.08.2018 15:16

      Никаких попыток прямого взаимодействия с другими фрагментами быть не должно.
      Также для общения фрагментов между собой есть шина, построенная на Observable и rxjs.

      У фрагмента нет публичного API. Фрагмент != компонент. Это не читсая вью — это прямо самостоятельный кусок UI, который самодостаточен и ему не нужно ничего из вне. Поэтому таким фрагментам очень редко нужно взаимодействие с внешним миром. Если оно нужно — делаем через EventBus.
      Или как решается к примеру проблема, когда сообщение публикуется, но не все подписчики еще инициализировались.

      Эта задача легко решается с помощью Observable. Используется специальный тип, который хранит состояние и подписчиков на его изменение.


      1. Demetros
        21.08.2018 16:32

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

        Первое, что приходит в голову — это какой-то вырожденный частный случай. Например, собрать главную страницу яндекса из кусочков, которые поставляют микросервисы. И даже в этом случае, в реальном мире чаще всего возникают потребности как-то взаимодействовать между кусочками.
        Вероятно, я не прав и вы меня переубедите.


        1. ShimON Автор
          21.08.2018 17:05

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

          Я не сказал, что взаимодействие полностью исключается. Мы исходим из того, что прямое взаимодействие скорее говорит о неверном делении.


        1. ShimON Автор
          21.08.2018 17:09

          Еще замечание по поводу яндекса. Так и есть, они строят страницу из кусочков, поставляемых разными микросервисами. Вот только они пошли чуть дальше — у них есть серверный рендеринг и сборка страницы происходит риал тайм. Подобный подход есть и у других компаний. Есть даже опенсорс решение на посмотреть Project Mosaic


  1. Fomich
    21.08.2018 15:07

    Я правильно понял, что у вас получилось подобие Custom Elements (https://developer.mozilla.org/)?


    1. ShimON Автор
      21.08.2018 15:09

      Нет, не совсем. Web Components — это способ изоляции частей UI и это крутой подход, чтобы не заморачиваться не конфликты стилей, конфликты библиотек и прочее. Как только мы сможем его использовать в полную силу — обязательно наши фрагменты будут строиться на них. Пока же это все только планы и разрозненные имплементации в разных браузерах.


      1. Fomich
        21.08.2018 15:24

        Angular Elements позволяют создавать Web Components, использующие всю мощь ангуляра.


        1. ShimON Автор
          21.08.2018 15:53

          Да, НО… Это не решает поставленные задачи. В браузерах, не реализующих полноценную поддержку Web Components, все равно не возможно добиться изоляции. А значит и их использование не оправданно. Надо сказать, что в некоторых случаях, если фрагмент пишется на Angular, Angular Elements используются для создания фрагмента.


  1. Goobs
    21.08.2018 18:04

    Упомянутая mui-platform доступна в Github? Где можно ознакомиться с техническими подробностями библиотеки? Если нет, планируете ли выложить в общий доступ?


    1. ShimON Автор
      21.08.2018 18:06

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


  1. ivanrave
    21.08.2018 19:45

    каждый фронтендный микросервис… может быть написан на любых технологиях

    хорошо сказано, это также снижает риски неправильно подобранного фреймворка


    1. ShimON Автор
      21.08.2018 19:48

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


  1. XT2
    21.08.2018 19:49
    -1

    На мой взгляд это называется из «мухи — слона». Столько излишних абстракций, сложностей, костылей, правил для команд, правил для правил… Модули на 100% реализуют микросервисный подход во фронте.

    Для каждого модуля лучше подходят свои технологии

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

    Значит выбран неподходящий для вашей задачи фреймфорк, но уж точно не позволять в одном проекте писать на разных. Либо выделить проблемный модуль (если он такой большой и пафосный) в отдельный проект. А ваш подход приемлем только для легаси.

    компиляцию в единый бандл

    Ни в коем случае, всё в модулях!

    Кто-то не готов перейти на обновленный API зависимости

    Вам всё равно придется при изменении глобальной зависимости (от которой зависят все фрагменты, а этих сервисов даже в статье перечислено много) править код во многих фрагментах, а так как они ещё и на разных технологиях, то это затронет все команды.

    Содержимое каждой закладки мы можем сделать отдельным фрагментом

    Всё независимое выкинуть в разные проекты (разные технологии/фреймворки), а что имеет связь в рамках одной страницы в рантайм должно быть одним проектом на одной технологии, и даже если он гипер-большой (тысячи модулей) — разделить на независимые глобальные модули, где каждая команда может отвечать за свой глобальный модуль.


    1. ShimON Автор
      21.08.2018 20:00
      +1

      Вы хотели поругать наш подход, но в результате пришли к нашему же выводу:

      Всё независимое выкинуть в разные проекты (разные технологии/фреймворки), а что имеет связь в рамках одной страницы в рантайм должно быть одним проектом на одной технологии

      Именно так. Только вы это называете проектом, а мы — микросервисом. И да, микросервис написан на едином фреймворке и поддерживается единой командой.

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

      Тут вы и правы и не правы. От глобальных зависимостей никуда не деться, хотя свести их к минимуму — вполне возможно. (Кстати, предлагаемый вами, модульный подход увеличивает dependency hell.) Другое дело, что мы об этом очень хорошо подумали заранее и добавили версионирование этой зависимости. Версионируется не только сам артифакт, но и рантайм зависимость — даже если в барузере подгружена новая версия общей зависимости, ваш код со старой будет продолжать работать.

      Ни в коем случае, всё в модулях!

      Модули потом все равно в единый бандл компилируются. Или вы имеете в виду, что каждый модуль компилируется в js отдельно и подгружается в браузер независимо? Тогда мы перейдем к обсуждению проблем — асинхронности подгрузки модулей, dependency management, производительность.
      Значит выбран неподходящий для вашей задачи фреймфорк, но уж точно не позволять в одном проекте писать на разных.

      Тут вы не уловили мою мысль. Наш проект — это сложная система, состоящая из выделенных частей (микросервисов). Разными фреймворками могут пользоваться разные части. Но понятно, что каждая часть обычно написана на едином фреймворке. В некоторых экзотических случаях могут использоваться специализированные фреймворки для решения конкретных задач в конкретном месте одной части. Но это уже выбор каждой части в отдельности.
      Модули на 100% реализуют микросервисный подход во фронте

      Если мы с вами одно и то же подразумеваем под микросервисном подходом (читай начало статьи), то раскройте, пожалуйста, мысль с модульным подходом. Будет интересно.


      1. noodles
        22.08.2018 00:14

        Именно так. Только вы это называете проектом, а мы — микросервисом. И да, микросервис написан на едином фреймворке и поддерживается единой командой.


        почему бы не разнести «микросервисы» одного проекта на разные урл-ы (серверный роутинг), каждая страничка которого собирает свой отдельный бандл.
        Таким образом, каждая страница — это отдельный spa и может писаться хоть на реакт-е, хоть на вью, хоть просто классический html с серверным рендерингом. Подумаешь, будет перезагрузка между «микросервисами» — но на то они и микросервисы, раз уж выделили их отдельно. Под этот вариант идеально подпадают закладки в каком-нибудь сайдбаре или те же табы.

        Делал так в одном легаси, на одной из страниц которого были «богатые» ui требования, которые снежным комом наращивали костыли. В итоге плюнул, и впилил туда div с реактом)


        1. ShimON Автор
          22.08.2018 12:08

          В целом, и такой подход используется — есть фрагмент «общая шапка», в котором ссылки на разные микросервсы, в каждый из которых этот фрагмент вставлен. Переходы между микросервисами осуществляются с перезагрузкой.

          Но как быть с примером из статьи? Есть корзина, владелец которой один микросервис, а отобразить надо в другом (при том с сохранением состояния при переходе)


  1. XT2
    21.08.2018 20:22

    мы об этом очень хорошо подумали заранее и добавили версионирование этой зависимости

    О, это круто, тогда ок.

    Да, крупные модули в отдельных js-файлах со своими зависимостями, иначе бы SPA весило сотни мегабайт. А на счет проблем асинхронности и производительности, не совсем понял, не знаю, может в каких-то фреймворках есть какие-то трудности в этом?

    Наш проект — это сложная система, состоящая из выделенных частей (микросервисов)

    Значит ваш проект уникален и требует именно такого подхода, поэтому согласен с вами.

    В целом у нас возникла путаница в терминах, если кратко, то меня лишь возмутило одновременное наличие различных технологий на странице, но как вы указали в последнем комментарии, что это специфика вашего проекта.


    1. ShimON Автор
      21.08.2018 23:42

      меня лишь возмутило одновременное наличие различных технологий на странице, но как вы указали в последнем комментарии, что это специфика вашего проекта.

      Я бы все-таки не совсем такой вывод сделал — это специфика микросервисного подхода для крупных проектов. Где UI — это не один портал или один рабочий стол, а целый набор разнородных инструментов, которые так или иначе друг с другом связаны по бизнес процессам. Наша компания тут не уникальна. Гугл, например — каждый сервис может выбрать свой фреймворк для написания (некоторые вообще свои рожают). Мейл — то же самое. Рамблер. Яндекс все-таки пытается стек держать консистентным, но в принципе тоже разные фреймворки попадаются.

      Справедливости ради — пока мы выбрали в качестве стандарта де-факто Angular 2+ (6 прямо сейчас) и выбор другого фреймворка для конкретного микросервиса должен быть очень мотивирован.

      И еще, когда мы переходили с Angular 1 на Angular 2+ — мы почувствовали все бенефиты подхода, когда микросервисный фронт может одновременно работать на нескольких технологиях.


  1. dfuse
    21.08.2018 22:40

    Мы пошли другим путем. Все под-приложения завернуты либо в веб-компонент, либо в IFRAME если это 3rd-party и там нужна особая безопасность и изолированность.


    1. ShimON Автор
      21.08.2018 23:34

      Спасибо за то, что делитесь опытом. Если я понял ваш подход, то каждый микросервис оборачивается в веб-компонент или IFRAME для обеспечения изолированности на странице. А кто занимается построением каждой страницы? А каким образом обеспечивается консистентность, если в конфигурации конкретного тенанта нет какого-то микросервиса? А каким образом обеспечивается взаимодействие кусочков? А как сделана авторизация? А как решаете проблемы перформанса, которые неизбежно приходят?

      Так вот вся соль в том, что в этой статье я описал именно решение задач, которые встают в момент, когда мы говорим — каждый микросервис отвечает за те куски экрана, которые к нему относятся. А уже как сам микросервис обеспечит изоляцию — это его выбор. Кто-то пытается идти с веб-компонентами — мы не советуем, т.к. спецификация поддерживается очень слабо в самых важных местах. Кто-то заморачивается на специфичность стилей и поставке бандла без внешних зависимостей. Кто-то действительно использует IFrame, если это интеграция с 3rd party. Важен общий подход — если есть кусок UI микросервиса, то только команда этого микросервиса должна за него отвечать.


      1. dfuse
        22.08.2018 00:03

        Вам спасибо, что делитесь опытом ) мы пока еще не на финальной стадии, чтобы обернуть опыт в статью, но это уже не за горами.


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


        Авторизация внутри IFRAME — обычный 3-legged OAuth с Implicit Grant и prompt=none, т.е. тихий редирект, поскольку и хост и приложение ходят на один и тот же login.example.com. Веб-компоненты используют SDK, у которого авторизация глобальна.


        Взаимодействие между кусочками — через общую шину (на основе postmessage в том числе) и через обычные коллбеки/пропсы которые можно передавать веб-компонентам, т.е. некая оркестрация в хосте конечно есть, но у нас довольно крупные куски.


        "конфигурации конкретного тенанта нет какого-то микросервиса" можно пояснить, что это значит? Если компонент вставлен на страницу, значит он получен из реестра, а соотв. равзернут, под него есть бакенд и все необходимое в энвайронменте.


        1. ShimON Автор
          22.08.2018 00:28

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


          Пока получается, что вы прошли тем же самым путём. Чем вдохновлялись?


          1. dfuse
            22.08.2018 01:13

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


            У нас два реестра ;) один — известные компоненты, грубо говоря хардкондый список, мы их сами пишем и к ним доверие (web components) и их можно монтировать руками + реестр установленных у юзера приложений, хранящийся на бакенде, там и наши и сторонние могут быть, это IFRAME/веб-компоненты, интерфейс одинаковый в данном случае, хосту все равно что вставлять на основе урла.


            Вдохновлялся… как то само родилось. Поглядывал на приложения VK и FB, на всякие CRM типа ZenDesk, Salesforce и тд., там кто во что горазд извращения, я решил, что надо творчески переосмыслить и пойти простым и понятным путем.


            Нам еще следующий шаг предстоит, надо как-то давать приложениям возможность влезать в другие приложения, парсить DOM, добавлять контролы (ну например в список юзеров кнопку с действием) и тд и все это безопасно надо делать, но это совсем другая история.


            1. x3mka
              22.08.2018 14:52

              Я разрабатываю похожую архитектуру. Взаимодействие микросервисов, встраивание UI.

              Есть идея реализовать что-то наподобие распределенного Redux. Каждое приложение может обновить store посредством dispatch какой нибудь мессаги (физически это может быть api, message queue и т.п.). Каждый микросервис выбирает себе канал общения со store, реактивный типа веб сокета, pull based через очереди и т.п. Когда в store приходит сообщение от одного микросервиса, он рассылает всем подписанным другим по выбранным каналам.

              Вторая идея, которая меня напрягает в реализациях у других, это понятие фичи. Я не понимаю, почему разработчики ограничиваются css/js при загрузке страницы. Хотелось бы нечто подобного на MEF/Prism в .NET с динамической подгрузкой модулей. On demand, когда запрошена какая-то фича из модуля. И фича может быть чем угодно, UI компонент, API wrapper и т.п.

              Если поразмыслить, приложение можно построить как граф (DAG) фич. Подграф фич, сам может являться фичей. Какие-то подграфы мы собираем вебпаком в бандлы (модули с фичами). Сами решаем какие в зависимости от размера, и т.п.

              Если реализовать управление зависимостями фич, их life cycle (конфиг, активация, деактивация) и варианты стыковки (две фичи не обязательно могут зависеть друг от друга, но отлично могут жить вместе), у меня в голове рисуется картинка, где собирание приложения сводится к набору нужных фич на всех уровнях.


              1. ShimON Автор
                22.08.2018 14:54

                Очень интересно! Но есть ощущение, что понятие фичи в вашем рассказе размыто. Создалось ощущение, что вы фичей как раз называете микросервис. У нас фича — это некая функция микросервиса. И да, она может состоять из чего угодно (чаще всего из FE API, js+css), фключаться и отключаться по запросу. Вот только в нашем понимании, каждый фрагмент может состоять в свою очередь из фич, которые очень даже взаимосвязаны. С другой стороны, взаимосвязанность фрагментов мы не приветствуем. Их цель — разделение ответственности и прочие плюшки микросервисной архитектуры.


                1. x3mka
                  22.08.2018 21:07

                  Как раз-таки размытость понятия фичи тут помогает. Делаю POC по этому поводу, React/Redux SPA. У каждой фичи есть тип. Например корневая фича имеет тип «react-redux-app». Тип фичи описывает, какие дочерние фичи требуются или могут использоваться. Например, рутовая фича требует фичу типа ui-template (min: 1 — фича UI контролов), может использовать фичи redux-module-batch (min: 0). Последняя может использовать фичи типа redux-module (min: 0).

                  В общем имеем граф фич. Для упрощения, можно положить что фича имеет фукцию активации, асинхронную, возвращающую Promise.

                  Теперь разный функционал описывается как F(dag of features). Под F скрывается выбор нужных типов фич для исполнения, обход и активация выбранных фич в топологическом порядке.

                  Например, app bootstrapper функция F активирует все фичи.
                  Генерация webpack конфига использует фичи типа redux-module-batch и обходит их отдельно из каждой как из корня.
                  Планирую добавить новый тип фичи для Gulp таска. Gult -T будет обходить граф и находить все таски.

                  Сейчас прихожу к мысли, что одного типа немного не хватает. Например, есть фича типа react-dev-tools, хотелось бы ее активировать только в девелопменте. То есть, активационная функция F должна скипать такие фичи для продакшна. Думаю добавить произвольный набор аттрибутов к каждой фиче и механизм зависимостей проапгрейдить с их использованием.

                  Простите, если все слишком высокопарно. Меня иногда поглощают философские вопросы. Удивляюсь, почему интерфейсами для пакетов являются классы, константы и т.п., а не функции активации с конфигом. А уже внутри можно использовать что угодно. Можно сказать, типа: «пиши, что тебе надобно, используя интерфейс классов и т.п». Но если представить, что помимо классов есть уже реализованные фичи, покрывающие наиболее используемые use кейсы, все было бы гораздо проще.

                  И вообще, DSL описания графа фич может быть кросплатфоменным. И если реализации фич есть на разных языках, мы автоматом получаем кросплатформенное приложение.


                  1. ShimON Автор
                    22.08.2018 22:57

                    Вы описываете все равно подход со статической линковкой компонентов. Моя же задача сделать так, чтобы если микросервис А использует часть из микросервиса Б, то после обновления микросервиса Б, микросервис А тут же получил обновления без пересборки и перезаливки докер образа. Это и есть принципиальный бенефит микросервисного фронта — только микросервис Б отвечает за то как будут выглядеть и работать принадлежащие ему куски.


                  1. bano-notit
                    23.08.2018 03:34

                    Удивляюсь, почему интерфейсами для пакетов являются классы, константы и т.п., а не функции активации с конфигом.

                    Давно уже придумали экспортировать default функцию-фабрику. debug в node.js использует как раз такой подход.


  1. imouseR
    22.08.2018 14:57

    Отличная статья, спасибо! А каков примерный общий объём «приращения» кода в % после внедрения такого подхода?


    1. ShimON Автор
      22.08.2018 14:58

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


  1. Quilin
    22.08.2018 15:30

    Внутри что-то шевелится, пытаясь как-то намекнуть, что в микросервисах одна из самых главных вещей это все-таки масштабируемость и fault-tolerance, а в однопоточном фронте это несуществующая проблема, но это холивор и к делу не относится.

    А вопрос вот такой — в микросервисах в мире backend есть достаточно обычная ситуация:
    1. Есть три микросервиса: А, В и С
    2. Микросервис А меняет контракт одной своей точки входа окончательно и бесповоротно.
    3. Сервис В сидит в соседней комнате и поддерживают новый контракт.
    4. Сервис С сидит в другой стране и не успел поддержать новый контракт.
    5. При деплое разворачивается две версии микросервиса А: новая и легаси, а с помощью service discovery сервисы В и С обращаются к разным инстансам этого микросервиса.
    6. Утром просыпается команда С и обновляет версию, после деплоя которой легаси-версия сервиса А прибивается.

    У вас не возникает такой проблемы, особенно с единой шиной взаимодействия всех микросервисов? Как вы решаете проблему версионности?


    1. ShimON Автор
      22.08.2018 23:08

      Да, вы правы, проблема версионности тут встает в полный рост. Тот подход, который вы описали для мира бекенда, называется canary deployment. Но это не единственный способ решить поставленную задачу. Также можно сделать, чтобы микросерис А версионировал свой API. Т.е. любой эндпоинт микросервиса А будет иметь версию. Например, /microserviceA/api/v1/endpointA. Микросервис должен гарантировать, что при любом изменении контракта этого эндпоинта, он получит новую версию, но старая продолжит работать по-старому.

      Вся эта проблема возникает по причине так называемой runtime зависимости. Т.е. зависимость, которую невозможно проверить во время компиляции.
      То же самое происходит и с фрагментами и с платформой, которая их менеджит. Приведу пример решения этой проблемы в платформе. У нас есть артефакт, который подключают к себе на страницу все микросервисы — он никак не версионируется (в URL нет ничего, говорящего о версии), но внутри него любой метод версионирован по принципу версионирования API микросервисов (methodA_v1, methodA_v2...). Кроме того, мы поставляем статическую зависимость (для каждого фреймворка свою), которая по сути является оберткой для вызова тех самых методов с версией. Она версионируется и вызывает соответствующую версию метода. Следовательно, не важно с какой версией обертки собран фрагмент, он сможет работать независимо от того, есть ли уже более новая версия зависимости runtime.

      Надеюсь, что не очень сумбурно :)


      1. Quilin
        23.08.2018 10:49

        Нет, это не сумбурно и понятно — проблему версионности уместнее всего решать, собственно, версионностью.
        Я думал о ситуации, когда новая версия категорически отметает обратную совместимость. Такое ведь тоже бывает? Никому не нравится видеть у себя в коде кучу устаревших контрактов, даже если эти контракты в отдельной сборке (субъективненько).

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

        Насчет fault-tolerance. Допустим, один микросервис упал, а другой в это время к нему обращается с целью например что-то обновить в себе. Единая шина умеет в MQ? Если нет, мне кажется, это было бы очень круто, научить ее отправлять в «свежеподнятый» компонент все неполученные им в момент простоя сообщения.


        1. ShimON Автор
          23.08.2018 18:04

          Я думал о ситуации, когда новая версия категорически отметает обратную совместимость.

          Такое безусловно может быть. В этом случае и тут можно использовать canary deployment. На данный момент наша разработка этого не поддерживает, но не вижу ничего сложного в реализации.
          это было бы очень круто, научить ее отправлять в «свежеподнятый» компонент все неполученные им в момент простоя сообщения

          Очень не однозначная фича, не находите? Подписаться на события, которые были до подключения. А что если эти события устарели? А что если события взаимоисключающие? А что если события были отменены? А главное — вы можете придумать бизнес кейс для этого?


    1. ShimON Автор
      22.08.2018 23:12

      Внутри что-то шевелится, пытаясь как-то намекнуть, что в микросервисах одна из самых главных вещей это все-таки масштабируемость и fault-tolerance, а в однопоточном фронте это несуществующая проблема, но это холивор и к делу не относится.

      Тоже люблю философствовать, поэтому позвольте прокомментировать. На мой взгляд, вы перечислили не одну вещь, а две разных :) И забыли третью очень важную — независимость (в том числе независимость обновлений). Вот именно это вполне достижимо и приносит очень хорошие плоды. А в целом, масштабируемость для фронта — это пока что-то непонятное. Но вот fault-tolerance вполне себе проявляется в нашем подходе. Например, если один из микросервисов упадет или не поднимется в облаке, любой другой, кто хочет показать в себе его кусок, просто ничего не покажет на этом месте. И пользователь сможет продолжать выполнять задачи присущие только не упавшему микросервису. Вуаля :)