Pull to refresh
1200.87
МТС
Про жизнь и развитие в IT

Готовим микрофронтенды на чистом JS без фреймворков

Level of difficultyMedium
Reading time15 min
Views7.9K

Привет, Хабр! Меня зовут Евгений Лабутин, я разработчик в МТС Digital. Сегодня я расскажу вам о своем рецепте приготовления микрофронтендов без использования каких либо фреймворков. Ведь такие фреймворки как Webpack Module Federation, Single-SPA, SystemJS и подобные вам просто не нужны для написания микрофронтендов, ровно так же как вам не нужен jQuery для написания современных фронтендов. Ведь все необходимое для разработки и работы Микрофронтендов уже встроено во все современные браузеры. Интересно? Добро пожаловать в статью.

Терминология

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

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

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

Именно такая характеристика как встраивание посредством динамического импорта отличает микрофронтенд от подключения внешней библиотеки и сборки монолита.

Где использовался микрофронтенд

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

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

Второй проект — SMS-рассылка, в нем микрофронтенды используются как инструмент миграции с legacy-кода из razor на современный стек на NextJS.

Микрофронтенд как инструмент масштабирования

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

И микрофронтенды помогали решать следующие задачи:

Задача 1: Переиспользование общих компонентов между разными приложенииями.

У нас было несколько микросервисов и встал вопрос — как переиспользовать общие элементы, а не копировать их между микросервисами. Ответ очевиден: такие общие элементы как Шапка, Футер, Формочки, Виджеты вынести в отдельный проект и подключать к Микросервисам как Микрофронтенды.

Причем не все приложения написаны на Реакте. У нас также были приложения, написанные на Tilda. И там привычные инструменты встраивания через сборку просто не работают.

Задача 2: Встраивание элементов нашего кабинета в партнерские кабинеты

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

Мы решили вынести функционал отображения состояния сервиса и функцию оплаты приложения в микрофронтенды и встроить их в партнерские кабинеты. Таким образом клиенту не надо было никуда переходиться для оплаты сервиса партнера.

Задача 3: Переиспользование инфраструктурной логики между микросервисами

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

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

Требования к микрофронтендам

Исходя из тех задач, которые должны решать микрофронтенды, были сформированы следующие требования:

  • независимость от внешнего проекта. Наши микрофронтенды встраивались не только в проекты написанные на Реакте, но и в тильду, в партнерские продукты, написанные на совершенно разных технологиях. Поэтому на окружение хостового приложения надеяться не стоит. Так мы решили все необходимые зависимости — встраивать в Микрофронтенд, включая Реакт и библиотеки;

  • использование микрофреймворков и легковесных библиотек. В связи с тем, что в наш микрофронтенд входили все его зависимости, решение могло быть тяжеловесным и ухудшать опыт пользователя в приложениях, куда встраивается микрофронтенд. А для того, чтобы не вредить хостовым приложениям нашими микрофронтендами, мы решили отказаться от тяжеловесных фреймворков и библиотек. Поэтому вместо тяжеловесных React, Angular, Vue лучше взять их легковесные аналоги Preact, Svelte, SolidJS и тому подобные. В частности, мы использовали Preact, так как он позволяет переиспользовать опыт, полученный от разработки на React;

  • использовать модули. Микрофронтенд должен встраиваться в хостовое приложение посредством динамического импорта. Динамический импорт поддерживает уже 95% браузеров. И подключение посредствам динамического импорта гораздо удобнее, чем подключение посредством встраиваемого скрипта;

  • не использовать глобальные переменные. Глобальные переменные могут привести к конфликтам на хостовом приложении. Поэтому нельзя использовать ни глобальные JS переменные, ни глобальные CSS переменные. И если использование модулей решает проблемы с JS-переменными, то для CSS надо в принципе отказаться от CSS-переменных. И с этой задачей отлично помогает справиться подход CSS-in-JS и такие библиотеки как Styled Components;

  • независимость от внешних настроек стилей. Все мы используем CSS Reset чтобы стили во всех браузерах выглядели одинаково. Но мы используем разные CSS Reset. И не стоит надеяться что CSS Reset, настроенный в вашем проекте, работает так же, как CSS Reset, написанный в хостовом приложении. Поэтому микрофронтенды должны иметь свой собственный CSS Reset, который будет иметь область действия только в вашем микрофронтенде;

  • использовать авторизацию по OpenID. Наши микрофронтенды встраивались не только в домены mts.ru, но и в домены партнеров. А авторизация посредством кук на разных доменах не работает, так как куки работают только в рамках своих доменов. Для решения этой проблемы необходимо передавать авторизацию не посредством кук, а посредством заголовков или в теле запросов.

Создание микрофронтендов

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

Для этого мы создаем JS-файл, который экспортирует наружу две функции render и clear.

Пример функции render:

import ReactDOM from "react-dom";
import {resolve} from "first-di";
import {App} from "./../../components/App";
import {Logger} from "./../../helpers/Logger";

const logger = resolve(Logger);
let memRootElement: HTMLElement | null = null;

export const render = (elem: HTMLElement, config?: Config) => {
	try {
		memRootElement = elem;

		ReactDOM.render(<App config={config} />, elem);

	} catch (error: unknown) {
		logger.error("Error on draw widget", error);
	}
};

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

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

Пример функции clear:

export const clear = (): void => {
	ReactDOM.render([], memRootElement);
};

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

Сборка Микрофронтенда

Теперь мы можем собрать наш микрофронтенд в JS-бандл. Для этого подходит практически любой инструмент сборки: rollup, webpack, vite, swcpack. Я приведу примеры нескольких из них.

Пример сборки на Rollup:

// Используемые мною плагины
import nodeResolve from "@rollup/plugin-node-resolve";
import commonJs from "@rollup/plugin-commonjs";
import postcss from 'rollup-plugin-postcss'
import replace from "@rollup/plugin-replace";
import swc from "@rollup/plugin-swc";
import terser from '@rollup/plugin-terser';

export default {
	input: [
		"src/microfront-first-main.tsx",
		"src/microfront-second-main.tsx"
	],
	plugins: [...], // тут подключаются плагины
	output: [
		{
			dir: "dist",
			format: "esm",
			entryFileNames: "[name].esm.min.js",
		}
	]
};

Это мой любимый инструмент сборки. Rollup в связке с Swc позволяет очень быстро собирать микрофронтенды и тестировать результат. Еще одна отличительная черта Rollup — возможность делать сборки одновременно для новых и старых браузеров. Для чего может понадобиться поддержка старых браузеров, расскажу ниже.

Пример сборки на Webpack:

module.exports = {
	entry: {
		microfrontFirst: "src/microfront-first-main.tsx",
		microfrontSecond: "src/microfront-second-main.tsx"
	},
	plugins: [...],
	output: {
		filename: "[name].esm.min.js",
		path: path.join(__dirname, "dist")
	},
};

Как видите такой же простой конфиг что и у Rollup. Но настройка плагинов не такая простая как у Rollup.

Пример сборки на vite:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  build: {
    emptyOutDir: true,
    outDir: "dist",
    sourcemap: true,
    lib: {
      entry: {
        "first": "src/microfront-first-main.tsx",
        "second": "src/microfront-second-main.tsx"
      },
      formats: ["es"],
    },
  },
});

Vite — очень простой сборщик, в котором из коробки уже все настроено, фактически это пресет для сборщика Rollup. Но у него есть существенный недостаток. Для транспиляции typescript в javascript используется быстрый транспиллер esbuild. И проблема в том, что esbuild поддерживает не все фичи typescript. И поэтому если вам нужны продвинутые возможности typescript, такие, как декораторы, рефлексия и подобные, — esbuild вам не подходит и его необходимо заменить на swc. А так как из коробки этого сделать нельзя, то проще вернуться к варианту Rollup + Swc.

Пример конфига swc-pack:

const { config } = require("@swc/core/spack");
 
module.exports = config({
  entry: {
    first: __dirname + "/src/microfront-first-main.tsx",
    second: __dirname + "/src/microfront-second-main.tsx",
  },
  output: {
    path: __dirname + "/dist",
  },
  module: {},
});

Хороший быстрый сборщик от авторов swc, настроенный из коробки. Но опять же, имеет существенный недостаток. В момент, когда я его тестировал, он не имел поддержки некоторых CommonJS-модулей. Возможно, этот функционал уже добавили и его стоит попробовать.

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

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

Публикация микрофронтендов

Микрофронтенды публикуем как статичные файлы в отдельном контейнере. Публикуем в интернете на отдельном каталоге внутри домена.

Сборка контейнера очень проста:

FROM node:20-alpine AS build

WORKDIR /app
COPY package*.json .npmrc ./
RUN npm ci

COPY . .
RUN npm run lint
RUN npm run build


FROM nginx:1.25-alpine as production

RUN rm -rvf /etc/nginx/conf.d/default.conf
RUN rm -rvf /usr/share/nginx/html/index.html

COPY --from=build /app/dist /usr/share/nginx/html
RUN chown nginx:nginx -R /usr/share/nginx/html

Таким образом когда разработка микрофронтенда завершена, разработчик сливает код в мастер-ветку, отрабатывают CI/CD процессы и новый микрофронтенд публикуется в интернете. Клиенты при запросе микрофронтенда автоматически получат новую версию микрофронтенда без необходимости пересборки хостового приложения.

Подключение микрофронтенда к хостовому приложению

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

Подключение выглядит следующим образом:

const microElement = document.querySelector("#micro");

const path = "https://tb.mts.ru/micro/header.esm.min.js";
const widget = await import(path);

widget.render(microElement, {openId: ""});

Для импорта микрофронтенда мы используем нативный динамический импорт, который поддерживают уже 95% браузеров и фактически 100% из поддерживаемых браузеров. Импортируем нашу функции render и clear. И вызываем функцию render для отрисовки микрофронтенда в нужном элементе с нужными параметрами.

Такое подключение имеет ряд преимуществ:

  • это гораздо проще, чем подключать внешний скрипт и ожидать его загрузки;

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

  • вам не надо «ловить» момент прогрузки и инициализации скрипта;

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

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

  • таким образом можно подключать микрофронтенды к Tilda и другим платформам;

  • для обновления микрофронтенда не надо пересобирать хостовое приложение. Достаточно опубликовать новую версию микрофронтенда и он появится на всех хостовых приложениях;

  • этот вид импорта работает не только в браузере, но в nodejs. Таким образом можно переиспользовать серверную логику;

  • такой микрофронтенд совместим с SSR. Вы можете отрендерить компонент в текст на стороне сервера и гидрировать его на стороне браузера.

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

const microElement = document.querySelector("#micro");

const webpackDynamicImport = new Function(
	"return import('https://tb.mts.ru/micro/header.esm.min.js')"
);
const widget = await webpackDynamicImport();

widget.render(microElement, {openId: ""});

Дело в том, что Webpack имеет неправильное поведение и пытается динамический импорт встроить в сборку, что неправильно. Чтобы скрыть динамический импорт от сборщика необходимо спрятать динамический импорт в строке.

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

const microElement = document.querySelector("#micro");
let widget = nul;
try {
	new Function("(async(a=0)=>({...{a}},await import('')))()");

	const path = "https://tb.mts.ru/micro/header.esm.min.js";
	widget = await import(path);

} catch (err) {
	widget = require("https://tb.mts.ru/micro/header.cjs.min.js");
}

widget.render(microElement, {openId: ""});

Здесь мы сначала проверяем, поддерживает ли браузер новые возможности, в том числе динамический импорт. Если поддерживает, то подгружаем версию для новых браузеров. Если все же нет, то подгружаем CommonJS версию микрофронтенда. И как я писал выше, Rollup позволяет делать одновременно сборку для новых и старых браузеров. Но, кроме самой сборки для старых браузеров, понадобится еще и полифил require для браузеров.

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

Что не так с Webpack Module Federation?

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

В итоге Webpack Module Federation приносит следующие проблемы:

  • Vendor Lock. Вы замыкаете свою разработку на этот проприетарный инструмент. С большой долей вероятности вам понадобится встроить ваш микрофронтенд в продукт, где нет MF и тогда вы столкнетесь с серьезными проблемами;

  • вы не можете встроить такие микрофронтенды в клиентские приложения, где нет системы сборки MF. Попробуйте, например, встроить MF в Tilda или проект на jQuery.

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

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

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

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

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

Частые ошибки в разработке микрофронтендов

Также я встречал много микрофронтендов от коллег и собрал популярные ошибки которые встретил у них:

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

  • скрипт сам начинает работать. Часто бывает что скрипт сам загрузился и начал что-то мониторить на странице. Такая тактика, как правило, не эффективна и потребляет клиентские ресурсы. Гораздо эффективнее будет подгрузить микрофронтенд в момент, когда он понадобился и запустить его исполнение. Например, форму оплаты не надо подгружать, когда клиент зашел на вашу страницу. Форму оплаты можно подгрузить динамическим импортом, когда пользователь нажал кнопку Купить;

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

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

Возвращаемся к первоначальным задачам и целям:

  1. Микрофронтенды встроены в наши микросервисы и обновляются без необходимости пересборки хостовых приложений.

  2. Микрофронтенды встраиваются в кабинеты партнеров, написанные на самых разных технологиях, и обновляются без необходимости пересборки партнерских кабинетов.

  3. Микрофронтенды встраиваются в Тильду и обновляются без правки в Тильде.

  4. Общая логика микросервисов вынесена в микрофронтенды и обновляется без необходимости пересборки хостовых приложений.

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

Как ведется разработка микрофронтендов?

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

Для этого мы сделали проект, содержащий Workspace, в котором располагается еще два проекта Components и Docs. В первом ведется непосредственно разработка микрофронтендов, из которых потом собирается статика для публикации. Во втором находится проект на NextJS, который одновременно выполняет функцию документации и тестовой площадки.

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

Микрофронтенд как инструмент миграции с legacy

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

В качестве легаси выступает фронт, написанный на C# и Razor шаблонизаторе. В качестве целевого решения был выбран NextJS. А в задаче — требования совершить беспростойную миграцию с легаси на новый стек.

Решение задачи следующее. Рядом с легаси-проектом был создан новый проект на NextJS. Код шаблонов Razor постранично копируется в шаблоны React с попутной правкой синтаксиса Razor на React. Проект немаленький, за один спринт совершить переход нереально. Поэтому за один спринт переводим по одной странице.

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

Микрофронтенд позволяет собрать уже мигрированную страницу в подключаемый JS-файл и подключить к легаси-проекту на Razor. В качества инструмента сборки используется описанный выше Rollup + SWC, в качестве механизма подключения — динамический импорт, о котором я также говорил ранее.

В итоге Razor страница примет следующий вид:

@using WebApp.Helpers;

@{
    Layout = "~/Views/Shared/_Grid_12.cshtml";
}

@section PageHead{
}

<div id="naming-template-page"></div>
<script type="text/javascript">
    (async () => {
        const { injectInRazor } = await import("/app-build/first-page.min.js");
        injectInRazor("naming-template-page");
    })()
</script>

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

Спасибо за уделенное статье время!

Понравилась статья? Нажмите нравится и порекомендуй коллегам!

Остались вопросы или пожелания? С удовольствием пообщаюсь в комментариях к статье!

Нашли очепятку? Сообщите о ней в личку!

Only registered users can participate in poll. Log in, please.
Вы используете Микрофронтенды?
3.17% Использую вместе с динамическим импортом2
26.98% Использую вместе с Webpack Module Federation17
3.17% Использую вместе с Single SPA2
0% Использую вместе с SystemJS0
1.59% Использую как script встраиваемый в head.1
1.59% Использую с другим фреймоврком/подходом1
63.49% Не использовал микрофронтенды40
63 users voted. 11 users abstained.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 20: ↑14 and ↓6+12
Comments21

Articles

Information

Website
www.mts.ru
Registered
Founded
Employees
over 10,000 employees
Location
Россия