Русский
Русский
English
Статистика
Реклама

Microfrontends

Микрофронтенды разделяй и властвуй

14.04.2021 16:05:26 | Автор: admin


Всем привет! Меня зовут Аня, я фронтенд-разработчик в Delivery Club. Хочу рассказать про использование микрофронтендов. Поговорим о том, что же это за зверь такой микрофронтенд, почему мы решили использовать этот подход в своих проектах и с какими проблемами столкнулись при внедрении.

Для чего они нам понадобились


Delivery Club это не только пользовательское приложение для заказы еды. Наши команды работают над сайтом, приложениями для курьеров и партнёров (ресторанов, магазинов, аптек и т.д.), занимаются прогнозированием времени, оптимизацией доставки и другими сложными задачами. Моя команда создаёт админку для работы с партнёрами. В ней можно управлять меню, создавать акции, отвечать на отзывы, общаться со службой поддержки и делать много других полезных для партнёров вещей. Но мы не единственная команда, которая занимается админкой. Отсюда возникало множество проблем при разработке:

  • Сложно жить с монолитом. Быстро писать код мешали устаревание подходов, фреймворков и зависимостей, а также связанность логики.
  • Совместная разработка. Долго решать конфликты, противоречивость изменений.
  • Проблема с тестированием. Когда прилетали баги от других задач и команд, было непонятно, в чём причины.
  • Развёртывание было зависимое и блокирующее.

Поэтому нам нужна была возможность:

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

Устройство проекта


Для начала расскажу, как сейчас устроен наш проект.

  • Основное старое приложение на AngularJS, к которому мы планируем подключать новые микроприложения.
  • Dashboard-приложение на Angular 6, подключенное через iframe (но оно со временем разрослось и от описанных выше проблем не избавило). К нему подключаются приложения, здесь хранятся старые страницы.
  • Приложения на VueJS, которые используют самописную библиотеку компонентов на VueJS.





Мы поняли, что ограничения тормозят развитие проекта. Поэтому сформулировали возможные пути:

  • Разделение приложения на страницы по маршрутам.
  • iframe.
  • Микрофронтенды.

Мы сразу отказались от варианта с iframe (почему мы так решили, читайте ниже). У разделения страниц по маршрутам есть один большой недостаток: дублирование логики в каждом приложении (типа навигации) и вообще всего, кроме специфики страницы. Поддерживать такую систему в актуальном состоянии очень сложно. Поэтому мы выбрали микрофронтенды.

Что такое микрофронтенды


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


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

Проблемы внедрения микрофронтендов


1. Ещё один iframe? Может, уже хватит?


К великому разочарованию, наше dashboard-приложение открывается внутри iframe (исторически так сложилось), и нам очень не хотелось делать один iframe внутри другого. Однако это был запасной план. Если вдруг что-то пойдёт не так, то просто заворачиваем микрофронтенд в iframe и готово рабочее приложение.

Мы видели несколько недостатков:

  • Неудобная навигация. Каждый раз для редиректа на внешнюю ссылку нужно использовать window.postMessage.
  • Сложно верстать в iframe.

К счастью, нам удалось этого всего этого избежать, и микрофрентенд мы подключили как веб-компонент с shadow dom: <review-ui-app></review-ui-app>. Такое решение выгодно с точки зрения изоляции кода и стилей. Веб-компонент мы сделали с помощью модифицированного vue-web-component-wrapper. Почитать подробнее о нём можно здесь.

Что мы сделали:

  1. Написали скрипт, который добавляет ссылку на сборку микрофронтенда в разделе head страницы при переходе на соответствующий маршрут.
  2. Добавили конфигурацию для микрофронтенда.
  3. Добавили в window.customElements тег review-ui-app.
  4. Подключили review-ui-app в dashboard-приложение.

Так мы сразу убили несколько зайцев. Во-первых, приложение представляет собой кастомный тег, как мы привыкли компонент. Во-вторых, кастомный тег принимает данные как свойства, следит за ними и выбрасывает события, которые слушает обёртка. Ну и в-третьих, избавились от iframe внутри iframe :)

2. А где стили?


Ещё одна неприятная проблема нас ждала дальше. Компоненты в микрофронтенде работали, только вот стили не отображались. Мы придумали несколько решений:

  • Первое: импортировать все стили в один файл и передать его во vue-wrapper (но это слишком топорно и пришлось бы добавлять вручную каждый новый файл со стилями).
  • Второе: подключить стили с помощью CSS-модулей. Для этого пришлось подкрутить webcomponents-loader.js, чтобы он вшивал собранный CSS в shadow dom. Но это лучше, чем вручную добавлять новые CSS-файлы :)

3. Теперь про иконки забыли!


Затем мы подключили внутреннюю библиотеку, но в ней не отображались SVG.
С иконками нам пришлось повозиться подольше. Для их подключения нам пришлось сделать несколько итераций:

  1. Сначала мы попытались подрубить спрайт так же, как и стили, через appendChild. Они подключились, но всё равно не отображались.
  2. Затем мы решили подключить через sprite.mount(this.shadowRoot). Добавили в вебпаке в svg-sprite-loader опцию spriteModule: path.resolve(__dirname, './src/renderers/sprite.js). Внутри sprite.js экспортировали BrowserSprite, и иконки начали отображаться! Мы, счастливые, подумали, что победили, но не тут-то было. Да, иконки отображались, и мы даже выкатились с этой версией в прод. Но потом нашли один неприятный баг: иконки пропадали, если походить по вкладкам dashboard-приложения.
  3. Наконец, во vue-wrapper мы подключили DcIconComponent (библиотечный компонент, позволяющий работать с библиотечными иконками) и в нём подключили иконки из нашего проекта. Получили отображение без багов :)

4. Без авторизации никуда!


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

  • Токен с авторизацией передаём с помощью свойств веб-компонента.
  • С помощью AuthRequestInterceptor подключаем токен-запросы для API.
  • Используем токен, пока он не протухнет. После протухания ловим ошибку 401 и кидаем в dashboard-приложение событие обнови токен, пожалуйста (ошибка обрабатывается в AuthResponseInterceptor).
  • Dashboard-приложение обновляет токен. Следим за его изменением внутри main-сервиса, и когда токен обновился, заворачиваем его в промис и подписываемся на обновления токена в AuthResponseInterceptor.
  • Дождавшись обновления ещё раз повторяем упавший запрос, но уже с новым токеном.

5. Нас волнуют зависимости


Проблема выглядела так: с каждым подключённым микрофронтом тянется бандл. И сколько микрофронтов подключено, столько и бандлов подтянуто. Чтобы избежать этого, мы применили общие зависимости. Вот как мы это реализовали:

  • В микрофронтенд-приложении указываем в webpack.config.prod.js в разделе externals те зависимости, которые хотим вынести:

    module.exports = {externals: {vue: Vue},
    

    Здесь мы указываем, что под именем Vue в window можно будет найти зависимость vue.
  • В рамках оболочки (в нашем случаем в dashboard-приложении) выполняем npm install vue (и другие npm install-зависимости).
  • В dashboard-приложении импортируем все зависимости:

    import Vue from vue(window as any).Vue = Vue;
    
  • Получаем удовольствие.

6. Разные окружения


Мы имеем три окружения: локальное, тестовое и прод. Сначала у нас была проблема при переходе с тестового окружения на прод: для микрофронтенда приходилось явно прописывать в проекте ссылку.

Решили мы это следующим образом:

  1. Добавили в микрофронтенд файл, в котором определяем конфигурацию для приложения в runtime браузера. Также добавили в Docker системный пакет, который предоставляет команду envsubst. Она подставляет значения в env.js, который тянет микрофронтенд-приложение, и эти переменные пишутся в window['${APP_NAME}_envConfig'].
  2. Добавили переменные окружения отдельно для прода и отдельно для тестового окружения.

Так мы решили несколько проблем:

  • настроили локальные моки (раньше приходилось вручную включать их локально);
  • настроили тестовое окружение без дополнительных ручных переключений на прод и обратно.

Выводы


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

Думаю, со временем всё больше проектов будут отказываться писать монолит и переходить на более универсальные подходы. Одним из таких подходов могут стать и микрофронтенды. Надеюсь, эта статья поможет кому-то в реализации :).
Подробнее..

Frontend Meetup 2004

19.04.2021 16:09:25 | Автор: admin

Вместе со спикерами из Devexperts, Почты России, Леруа Мерлен и Райффайзенбанка узнаем об опыте разработки продуктов: как найти подход к Blazor, использовать плагин Figma для работы с white label, разрабатывать картографический раздел отделений и внедрять микрофронтенды.

О чем поговорим

Blazor поневоле

Александр Кильганов, Райффайзенбанк

О докладе: Я не Frontend-разработчик, но с Blazor пришлось познакомиться: подход к нему получилось найти не сразу, об этом и расскажу с какими болями и трудностями при работе столкнулся и как их решал. Также расскажу, как смешивал несмешиваемые компоненты Blazor, C# и HTML и что это из этого получилось. Ну, и отвечу на главный вопрос: стоит ли Blazor вообще использовать?

Плагин в Figma для работы с white label

Наталья Ильина, Devexperts

О докладе:Мы занимаемся разработкой финтех продуктов и поставляем клиенту white label. В свое время мы пришли к пониманию, что нам нужно автоматизировать рутинные процессы. Так был разработан плагин Хамелеон, который интегрирует содержимое макетов в Figma со сборками. В докладе расскажу, как плагин работает, с какими сложностями столкнулись в Figma, как решили их и какой у плагина есть потенциал.

Опыт отрисовки отделений на карте в Почте России

Михаил Вовренчук, Почта России

О докладе:Поделюсь историей трех итераций разработки картографического раздела отделений. Расскажу, как мы начинали разрабатывать данный сервис. С какими проблемами столкнулись в процессе разработки и к каким выводам пришли к началу третьей итерации.

Эволюция микрофронтендной платформы в Леруа Мерлен

Роман Соколов, Леруа Мерлен

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


Начнем митап в 19:00 (мск).
Зарегистрируйтесь, чтобы получить ссылку на трансляцию: письмо придет вам на почту.

Подробнее..

Как готовить микрофронтенды в Webpack 5

27.04.2021 16:10:26 | Автор: admin

Всем привет, меня зовут Иван и я фронтенд-разработчик.

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

Начнём с того, что ребята с Хабра (@artemu78, @dfuse, @Katsuba) уже писали про Module Federation, так что, моя статья - это не что-то уникальное и прорывное. Скорее, это шишки, костыли и велосипеды, которые полезно знать тем, кто собирается использовать данную технологию.

Причина

Причина, по которой решено было внедрять микросервисный подход на фронте, довольно простая - много команд, а проект один, нужно было как-то разделить зоны ответственности и распараллелить разработку. Как раз в тот момент, мне на глаза попался доклад Павла Черторогова про Webpack 5 Module Federation. Честно, это перевернуло моё видение современных веб-приложений. Я очень вдохновился и начал изучать и крутить эту технологию, чтобы понять, можно ли применить это в нашем проекте. Оказалось, всё что нужно, это дописать несколько строк в конфиг Webpack, создать пару компонентов-хелперов, и... всё завелось.

Настройка

Итак, что же нужно сделать, чтобы запустить микрофронтенды на базе сборки Webpack 5?

Для начала, убедитесь, что используете Webpack пятой версии, потому что Module Federation там поддерживается из коробки.

Настройка shell-приложения

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

Чтобы создать контейнер на базе сборки Webpack и при помощи этого контейнера иметь возможность импортировать ресурсы с удаленных хостов добавляем в Webpack-конфиг следующий код:

const webpack = require('webpack');// ...const { ModuleFederationPlugin } = webpack.container;const deps = require('./package.json').dependencies;module.exports = {  // ...  output: {    // ...    publicPath: 'auto', // ВАЖНО! Указывайте либо реальный publicPath, либо auto  },  module: {    // ...  },  plugins: [    // ...    new ModuleFederationPlugin({      name: 'shell',      filename: 'shell.js',      shared: {        react: { requiredVersion: deps.react },        'react-dom': { requiredVersion: deps['react-dom'] },        'react-query': {          requiredVersion: deps['react-query'],        },      },      remotes: {        widgets: `widgets@http://localhost:3002/widgets.js`,      },    }),  ],  devServer: {    // ...  },};

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

// bootstrap.tsximport React from 'react';import { render } from 'react-dom';import { App } from './App';import { config } from './config';import './index.scss';config.init().then(() => {  render(<App />, document.getElementById('root'));});

А в index.tsx вызываем этот самый bootstrap

import('./bootstrap');

В общем то всё, в таком виде уже можно импортировать ваши микрофронтенды - они указываются в объекте remotes в формате <name>@<адрес хоста>/<filename>. Но нам такая конфигурация не подходит, ведь на момент сборки приложения мы ещё не знаем откуда будем брать микрофронтенд, к счастью, есть готовое решение, поэтому возьмем код из примера для динамических хостов, так как наше приложение написано на React, то оформим хэлпер в виде React-компонента LazyService:

// LazyService.tsximport React, { lazy, ReactNode, Suspense } from 'react';import { useDynamicScript } from './useDynamicScript';import { loadComponent } from './loadComponent';import { Microservice } from './types';import { ErrorBoundary } from '../ErrorBoundary/ErrorBoundary';interface ILazyServiceProps<T = Record<string, unknown>> {  microservice: Microservice<T>;  loadingMessage?: ReactNode;  errorMessage?: ReactNode;}export function LazyService<T = Record<string, unknown>>({  microservice,  loadingMessage,  errorMessage,}: ILazyServiceProps<T>): JSX.Element {  const { ready, failed } = useDynamicScript(microservice.url);  const errorNode = errorMessage || <span>Failed to load dynamic script: {microservice.url}</span>;  if (failed) {    return <>{errorNode}</>;  }  const loadingNode = loadingMessage || <span>Loading dynamic script: {microservice.url}</span>;  if (!ready) {    return <>{loadingNode}</>;  }  const Component = lazy(loadComponent(microservice.scope, microservice.module));  return (    <ErrorBoundary>      <Suspense fallback={loadingNode}>        <Component {...(microservice.props || {})} />      </Suspense>    </ErrorBoundary>  );}

Хук useDynamicScript нужен нам, чтобы в рантайме прикреплять загруженный скрипт к нашему html-документу.

// useDynamicScript.ts  import { useEffect, useState } from 'react';export const useDynamicScript = (url?: string): { ready: boolean; failed: boolean } => {  const [ready, setReady] = useState(false);  const [failed, setFailed] = useState(false);  useEffect(() => {    if (!url) {      return;    }    const script = document.createElement('script');    script.src = url;    script.type = 'text/javascript';    script.async = true;    setReady(false);    setFailed(false);    script.onload = (): void => {      console.log(`Dynamic Script Loaded: ${url}`);      setReady(true);    };    script.onerror = (): void => {      console.error(`Dynamic Script Error: ${url}`);      setReady(false);      setFailed(true);    };    document.head.appendChild(script);    return (): void => {      console.log(`Dynamic Script Removed: ${url}`);      document.head.removeChild(script);    };  }, [url]);  return {    ready,    failed,  };};

loadComponent это обращение к Webpack-контейнеру, по сути - обычный динамический импорт.

// loadComponent.tsexport function loadComponent(scope, module) {  return async () => {    // Initializes the share scope. This fills it with known provided modules from this build and all remotes    await __webpack_init_sharing__('default');    const container = window[scope]; // or get the container somewhere else    // Initialize the container, it may provide shared modules    await container.init(__webpack_share_scopes__.default);    const factory = await window[scope].get(module);    const Module = factory();    return Module;  };}

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

// types.tsexport type Microservice<T = Record<string, unknown>> = {  url: string;  scope: string;  module: string;  props?: T;};
  • url - имя хоста + имя контейнера (например, http://localhost:3002/widgets.js), с которого мы хотим подтянуть модуль

  • scope - параметр name, который мы укажем в удаленном конфиге ModuleFederationPlugin

  • module - имя модуля, который мы хотим подтянуть

  • props - опциональный параметр, если вдруг наш микросервис требует пропсы, нужно их типизировать

Вызов компонента LazyService происходит следующим образом:

import React, { FC, useState } from 'react';import { LazyService } from '../../components/LazyService';import { Microservice } from '../../components/LazyService/types';import { Loader } from '../../components/Loader';import { Toggle } from '../../components/Toggle';import { config } from '../../config';import styles from './styles.module.scss';export const Video: FC = () => {  const [microservice, setMicroservice] = useState<Microservice>({    url: config.microservices.widgets.url,    scope: 'widgets',    module: './Zack',  });  const toggleMicroservice = () => {    if (microservice.module === './Zack') {      setMicroservice({ ...microservice, module: './Jack' });    }    if (microservice.module === './Jack') {      setMicroservice({ ...microservice, module: './Zack' });    }  };  return (    <>      <div className={styles.ToggleContainer}>        <Toggle onClick={toggleMicroservice} />      </div>      <LazyService microservice={microservice} loadingMessage={<Loader />} />    </>  );};

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

Так, с shell-приложением вроде разобрались, теперь нужно откуда-то брать наши модули.

Настройка микрофронтенда

Для начала проделываем все те же манипуляции что и в shell-приложении и убеждаемся, что версия Webpack => 5

Настраиваем ModuleFederationPlugin, но уже со своими параметрами, эти параметры указываем при подключении модуля в основное приложение.

// ...new ModuleFederationPlugin({      name: 'widgets',      filename: 'widgets.js',      shared: {        react: { requiredVersion: deps.react },        'react-dom': { requiredVersion: deps['react-dom'] },        'react-query': {          requiredVersion: deps['react-query'],        },      },      exposes: {        './Todo': './src/App',        './Gallery': './src/pages/Gallery/Gallery',        './Zack': './src/pages/Zack/Zack',        './Jack': './src/pages/Jack/Jack',      },    }),// ...

В объекте exposes указываем те модули, которые мы ходим отдать наружу, точку входа в приложение так же нужно забутстрапить. Если в микрофронтенде нам не нужны модули с других хостов, то компонент LazyService тут не нужен.

Вот и всё, получен работающий прототип микрофронтенда.

Выглядит круто, работает тоже круто. Общие зависимости не грузятся повторно, версии библиотек рулятся плагином, можно динамически переключать модули, в общем, сказка. Если копать глубже, то это очень гибкая технология, можно использовать её не только с React и JavaScript, но и со всем, что переваривает Webpack, то есть теоретически можно подружить части приложения написанные на разных фреймворках, это конечно не очень хорошо, но сделать так можно. Можно собрать модули и положить на CDN, можно использовать контейнер как общую библиотеку компонентов для нескольких приложений. Возможностей реально много.

Проблемы

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

Потеря контекстов в React-компонентах

Как только понадобилось работать с контекстом библиотеки react-router, то возникли проблемы, при попытке использовать в микрофронтенде хук useLocation, например, приложение вылетало с ошибкой.

Ошибка при попытке обращения к контексту shell-приложения из микрофронтендаОшибка при попытке обращения к контексту shell-приложения из микрофронтенда

Для взаимодействия с бэкендом мы используем Apollo, и хотелось, чтобы ApolloClient объявлялся только единожды в shell-приложении. Но при попытке из микрофронтенда просто использовать хук useQuery, в рантайме приложение вылетало с такой же ошибкой как и для useLocation.

Экспериментальным путём было выяснено, для того чтобы контексты правильно работали, нужно в микрофронтендах использовать версию npm-пакета не выше, чем в shell-приложение, так что за этим нужно внимательно следить.

Дублирование UI-компонентов в shell-приложении и микрофронтенде

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

  1. Выносить UI-компоненты в отдельный npm-пакет и использовать его как shared-модуль

  2. "Делиться" компонентами через ModuleFederationPlugin

В принципе, у обоих подходов есть свои плюсы, но мы выбрали первый, потому что так удобнее и прозрачнее управлять библиотекой компонентов. Да и саму технологию Module Federation хотелось использовать как механизм для построения микрофронтендов, а не аналог npm.

Заключение

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

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

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

Полезные ссылки

Репозиторий из примера

Документация Module Federation в доках Webpack 5

Примеры использования Module Federation

Плейлист по Module Federation на YouTube

Подробнее..

От одного приложения к сотне. Путь микрофронтенда в Тинькофф Бизнес

16.06.2021 12:16:05 | Автор: admin

Привет, меня зовут Ваня, недавно я выступил на CodeFest 11, где рассказал про путь Тинькофф Бизнеса на фронтенде от одного приложения к сотне. Но так как в ИT очень быстро все меняется, а ждать запись еще долго, сейчас я тезисно расскажу о нашем шестилетнем путешествии в дивный мир микрофронтенда!

Мою статью можно разбить на две части. В первой части вас ждет история развития через призму бизнеса. А во второй рассказ о том, как мы адаптировались к новым вызовам.

Этапы развития

  1. Одно приложение на AngularJS в 20142015 годах.

  2. Миграция на Angular2.

  3. Утяжеление десяти приложений новой функциональностью.

  4. Переход к микросервисам и разбиение на 100 приложений.

На дворе начало 2015 года. К нам приходит бизнес и говорит: Мы хотим сделать зарплатный проект! Посмотрите, что есть сейчас на рынке по технологиям, и сделайте. Выбираем AngularJS, быстро создаем приложение. Спустя некоторое время аппетиты вырастают, мы создаем еще два сервиса. На этот момент фронтенд-приложения никак не взаимодействуют друг с другом.

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

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

В одном из проектов у нас было пять фронтенд-команд, в каждой по 35 человек, то есть в самый лучший момент в одном проекте работали 25 фронтендеров! Иногда было действительно больно: ты вот-вот замержишь свою задачку, но нет! Перед тобой кто-то успевает и все твои пайплайны начинают проходить заново! До сих пор мне не по себе от этих воспоминаний.

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

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

Сайдбар

Первые три приложения мы подружили между собой с помощью сайдбара.

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

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

Подсвеченная область отдельное приложение СайдбарПодсвеченная область отдельное приложение Сайдбар

Frame Manager

Именно рваные переходы мы убрали с появлением Frame Manager'а (далее буду называть его ФМ).

Подсвеченная область отдельное приложение Frame ManagerПодсвеченная область отдельное приложение Frame Manager

В отличие от сайдбара, который встраивался в приложение с помощью iframe, ФМ находился на странице всегда и сам встраивал в себя приложения.

Слева концепция сайдбара (было), справа Frame Manager'а (стало)Слева концепция сайдбара (было), справа Frame Manager'а (стало)

Для клиента ФМ так же не заметен, как и сайдбар, но для нас он выполняет уже больше функций. Кроме рейтинга, списка продуктов и авторизации он может передавать состояние между приложениями, например через глобальный window. Пользователь больше не замечает, что ходит между несколькими отдельными приложениями, так как верхняя часть у него никуда не пропадает и визуально весь процесс выглядит как подгрузка контента на одну страницу.

В плане интеграции приложения тоже все поменялось:

  • Раньше приложению-клиенту достаточно было подключить необходимый скрипт к себе в index.html.

  • Теперь все приложения ФМа хранятся в отдельной конфигурации и используются как единый источник правды.

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

Однажды через поддержку к нам обратились пользователи с ситуацией: Раньше у меня работал плагин для Google Chrome, а с недавнего времени именно на вашем сайте перестал. Почините, пожалуйста! Обычно на такие просьбы не реагируют: пользователь что-то себе установил пусть сам и разбирается. Но только не в нашей компании. Команда долго изучала вопрос, смотрела, какое окружение у клиента, версия браузера и все-все, но ответа так и не было. В итоге мы полностью повторили окружение, загрузили себе плагины и путем дебагинга установили, что данный плагин не работает, если у iframe динамически менять атрибут src или пересоздавать фрейм. К сожалению, мы так и не смогли исправить такое поведение, поскольку на этой концепции построено все взаимодействие ФМ и дочерних приложений.

Бесфрейм-менеджер

Однажды мы собрались и подумали: Несколько лет страдаем от iframe. Как перестать страдать? Давайте просто уберем его! Сказано сделано. Так и появился бесфрейм-менеджер с фантазией у нас, конечно, не фонтан ;-)

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

В решении три составляющие:

  1. Webpack-плагин основа нашего решения, подробнее о которой можно прочитать в статье Игоря.

  2. Angular builder обвязка для настройки и запуска плагина.

  3. Angular schematics скрипт для упрощения работы с файловой структурой с помощью AST.

В 2021 году плагин становится менее актуальным, потому что вышел Webpack 5 с Module Federation, но напомню, что мы вели разработку в 2018 году, а Angular стал поддерживать последнюю версию вебпака лишь с двенадцатой версии, которая вышла 12 мая 2021 года. Мы пока не уверены, сможет ли MF заменить наше решение, и изучаем комбинацию подходов.

Что же касается других решений, на которые можно было перейти для отказа от iframe, то это Single SPA. Он всем хорош и очень популярен, но в плане Angular есть небольшой дисклеймер.

http://personeltest.ru/aways/single-spa.js.org/docs/ecosystem-angular/https://single-spa.js.org/docs/ecosystem-angular/

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

Что же касается Angular builder и schematics, то они нужны, чтобы разработчики, которые будут интегрировать наше решение к себе, не выполняли километровую инструкцию, а просто написали в консоли:

ng update @scripts/deframing

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

Тестирование

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

  1. Можно прогонять тесты совместно с локальным ФМом. Так разработчики всегда могут быть уверены, что на текущей продовой сборке все работает и выглядит так, как и задумывалось.

  2. Сам ФМ определяет несколько жизненно важных процессов, работоспособность которых гарантирует при любых условиях: это авторизация, роутинг, работа с данными приложений. Для этого создаются приложения-стабы (stub), суть которых подключиться к ФМу и выполнить одну из вышеперечисленных функций. То есть на каждое изменение кодовой базы ФМа будет гарантированно работать эта функция.

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

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

.my-pretty-header {    display: none;}

Если у кого-то из следующих приложений есть такое же название класса, этот стиль применится так же!

Пример: диалог решил спрятаться под меню, чтобы пользователь не догадался, что от него требуется:

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

Сторонние библиотеки если на одной странице два и более приложения используют библиотеку, которая на старте создает новый инстанс, то получается такая картина:

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

Microzord

Вот мы и прошли шесть лет технического развития нашего решения. И что может быть лучше, чем поделиться этим опытом с сообществом? Все наработки будут публиковаться под npm scope @microzord с открытым кодом на Гитхабе.

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

Подробнее..

Микрофронтенды. Учимся на ошибках

18.08.2020 10:04:18 | Автор: admin
В этой статье я расскажу с какими проблемами удалось столкнуться при построении микрофронтендов, каким образом их можно было бы избежать, а также немного привнести полученного опыта в довольно редкоподнимаемую тему микрофронтендов.



Микрофронтенды на iframe


В одной компании взвешенным и обдуманным решением CTO было принято, что микрофронтендам наравне с микросервисами быть, причем сервировать их надо на iframe'ах.

В качестве аргумента, кстати, был приведен продукт Office 360 от Microsoft, раньше там использовался `<iframe />` для верхнего и бокового меню. Теперь iframe'ов там нет.

Причины и предпосылки для микрофронтендов


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

Все имеющиеся приложения это React SPA. Ничего общего кроме Material UI, React Router v4 и зародыша UI-kit в качестве npm-модулей, не имеют.

Часть приложений будет использоваться и поставляться как в standalone-варианте, так и как часть другого приложения.

Микрофронтенды были поделены по крупным функциональным блокам:

1. Шапка приложения. Роутинг между приложениями
2. Приложение-дэшбоард. С метрикой и виджетами. Каждый виджет дэшборда должен быть частью соответсвующего приложения (подключаться по iframe).
3. Сами приложения. Некоторые из них включают части друг друга (но без фанатизма и рекурсии)



Таким образом, получаются разные релизные циклы, не зависящие друг от друга. Лишь бы приложения следовали внутреннему API.

Проблема #0. Неправильное разделение на микрофронтенды


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



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

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

Как результат, экономия времени на переиспользовании приложений не удалась.
Дублирование функционала все также осталось на высоком уровне. Использование микрофронтендов без iframe или компонентов с более сложной логикой из расширенного UI-kit могло бы решить проблему дублирования функионала.

Проблема #1. Отсутствие оркестрации процесса


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

Чтобы нивелировать все CORS проблемы раз и навсегда, было принято решение посадить nginx, который бы разруливал маршрутизацию. Таким образом, каждый микрофронтенд и каждый микросервис имел свой адрес, например:

https://domain.zone/dashboard
https://domain.zone/header
https://domain.zone/app1
https://domain.zone/app2
https://domain.zone/api1
https://domain.zone/api2


Остается вопрос, как тестировать приложеняи во время режима разработки? Каждое приложение будет сервироваться на своем порту?

Здесь приходит на помощь пакет `http-proxy-middleware` которое можно настроить в паре с CRA. Оказалось, только половина front-end разработчиков оказалось в силе настроить такой сетап. Никого обвинять здесь, разумеется, нельзя, но такая проблема появилась, а решать ее надо организационно.

Также необходима четкая версионность всех приложений с описанием функционала, доступных методов и внутреннего API. Отсюда вытекает следующая проблема: документация.

Проблема #2. Отсутствие внутреннего API


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

Это очень критическая часть системы в случае распределенных команд, да еще с учетом текучки кадров.

Также необходимо разработать механизм взаимодействия между приложениями. Тут на помощь приходит `postMessage API` или же прямой доступ к другому, встроенному почти в каждое React-приложение Redux. Чем не message bus? Но об этом позже.

Проблема #3. Iframe не обладает достаточной гибкостью


В использовании тега `<iframe />` нет ничего плохого. Это мощный инструмент с встроенным message bus (postMessage API) и широкой настройкой безопасности.

Однако, в случае микрофронтендов, `<iframe />` накладывает большое количество ограничений. Одно из них невозможность переиспользовать одно приложение в нескольких частях страницы.

Переиспользование приложений

В случае с аналогией интернет-магазина, 10 кнопок Купить создадут 10 `<iframe />`, то есть 10 запущенных React-приложений. Никакой памяти на это не хватит. Это одна из причин, по которой разделение приложения на команды по фичам не выдается возможным.

URL в качестве управлением стейтом не подходит

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

Вот пример, как то фиолетовое приложение из КДПВ с другим URL может работать как standalone приложение:



Однако, использовать URL интерфейс iframe для переключения состояния микрофронтенда в нашем случае оказалось невозможным: микрофронтенд начинает грузиться с нуля из-за неполной интеграции history API с его `pushState`и React Router получаем полное обновление страницы.

Обработчики кликов вне области `iframe`

Представьте, что вы заходите сделать выпадающее меню. Как на схеме выше: из розового меню. А еще и закрывать его по клику по пустому месту. В случае с iframe, вам необходимо использовать условный postMessage API, так как клик вне по просту не распознается из-за разных объектов window. Либо же придумывать хак с прозрачным фоном увеличенного элемента iframe во весь экран.

Кстати, изменение размеров iframe и подстраивание под него родительского приложения тоже становится более громоздкой и сложной.

Бонусная проблема: Несоразмерное использоваение Cookie


Эта проблема не относится напрямую к микрофронтендам, но с ними она переходит на следующий уровень безумства.

Было решено записывать в авторизационные куки не только токен, но и полный список прав на те или иные части приложения. Все это шифровалось SHA-??? и конвертировалось в base64.
Как результат, размер кук превышал 8KB, что является значением по умолчанию для nodejs/nginx, (или 2KB для размера одной записи Cookie в Google Chrome), что привело более сложной настройке серверов (без eject CRA уже не запустить с такой настройкой), а также к разделению этого большого зашифрованного массива данных на более мелкие куки-строки.

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

Заключение. Как тогда жить с микрофронтендами?


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

  • Не использовать iframe. Он не упрощает работу, а лишь добавляет проблем с производительностью, сильно ограничивая в возможности разбиения приложения на микрофронтенды.
    Разработать оркестрацию процесса: как в продакшене, так и для разработки. Не каждый разработчик захочет лезть в смежную отрасль маршрутизации и прокси, когда он нанимался клепать интерфейсы из готовых блоков.
    Разработать message bus между приложениями. Это намного проще в случае единого глобального пространства объекта window.
    Создать документацию по интерфейсу взаимодействия приложений, а также их запуска и настройки.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru