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

Module federation

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

Подробнее..

Категории

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

  • Имя: Макс
    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