Правильные ли у вас микрофронты?

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

Рассмотрю на примере nx.dev и webpack module federation.

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

По самом nx.dev, писал когда-то статью, можно почитать тут. Некоторые моменты могли устареть, но сама концепция осталсь та же. Так например package-based проектов уже нет.

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

  1. Независимые команды разработки. Каждая команда должна иметь свои микрофронты.

  2. Крупный проект с независимыми подсистемами. Каждую из таких подсистем можно оформить как отдельный микрофронт.

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

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

Проблемный проект

Публикуемые библиотеки (или еще одна дополнительная явная зависимоть)

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

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

Примерная структура проекта:

my-workspace/
├── apps/                # Приложения (frontend, backend, mobile и др.)
├── libs/                # Библиотеки (переиспользуемые модули, UI-компоненты и др.)
├── dist/                # Результаты сборки
├── nx.json              # Глобальная конфигурация Nx
├── package.json         # Общие зависимости монорепозитория

В свое время мне помогла эта статья, чтобы gitlab-ci создавался динамически и туда попадали микрофронты, которые требуется обновить.

Как оперативно подгружать зависимости без публикации

Пакеты также могут быть связаны через soft link пакетного менеджера, но если у них одна область — это префикс перед именем пакета, тогда будет пересоздавать папку, этот вариант не подходит, если несколько проектов в одной области. "@my-corp/a", "@my-corp/b"

Временное решение - создание symlink операционной системы через ln -s <source_file> <link_name>, но они будут удалены после того когда зависимость из package.json будет удалена или добавлена.

Еще как вариант указание в package.json зависимости, которая локально расположена также:

"dependencies": {
  "my-local-package": "file:../path/to/local/package"
}

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

Как вариант использовать yarn workspaces в package.json указываем:

{
  "workspaces": ["apps/*", "libs/*"]
}

В этом случае он за нас создает символические ссылки в node_modules.

Либо в файле tsconfig.js указываем пути (nx.dev по сути тоже самое делает при создании новой библиотеки и микрофронта):

{
  "paths": {
    "@app/app1": ["apps/app1/index.ts"],
    "@lib/utils": ["libs/utils/index.ts"]
  }
}

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

Статичный импорт

Нет смысла между микрофронтами статистический импорт применять, так весь смысл микрофронтов убивается, т.к. у вас микрофронты собираются и в каждый финальный бандл поместится тот участок кода, которые импортируете. Это хорошо работает с libs, когда микрофронты в apps импортируют их. Динамическая загрузка микрофронтов для webpack описана здесь. React-lazy в данном случае нам не подойдет, т.к. он создает chunk внутри проекта, а каждый микрофронтенд на своем адресе должен размещаться и загружать от туда необходимые ресурсы.

Примерный файл webpack.config.js remote приложение:

const { ModuleFederationPlugin } = require("webpack").container;
const packageJson = require("./package.json");
const path = require("path");

module.exports = {
  entry: "./src/index",
  mode: "development",
  output: {
    publicPath: "http://localhost:3001/",
  },
  plugins: [
    new ModuleFederationPlugin({
      name: "remoteApp",
      filename: "remoteEntry.js",
      exposes: {
        "./Widget": "./src/components/Widget",
      },
      shared: {
        react: { singleton: true, eager: true, requiredVersion: packageJson.dependencies.react},
        "react-dom": { singleton: true, eager: true, requiredVersion: packageJson.dependencies["react-dom"] },
      },
    }),
  ],
  devServer: {
    port: 3001,
    static: path.join(__dirname, "dist"),
  },
};

Примерный файл host приложения:

const { ModuleFederationPlugin } = require("webpack").container;
const packageJson = require("./package.json");
const path = require("path");

module.exports = {
  entry: "./src/index",
  mode: "development",
  output: {
    publicPath: "http://localhost:3000/",
  },
  plugins: [
    new ModuleFederationPlugin({
      name: "hostApp",
      shared: {
        react: { singleton: true, eager: true, requiredVersion: packageJson.dependencies.react},
        "react-dom": { singleton: true, eager: true, requiredVersion: packageJson.dependencies["react-dom"] },
      },
    }),
  ],
  devServer: {
    port: 3000,
    static: path.join(__dirname, "dist"),
  },
};

Допустим у вас есть метод для загрузки микрофронтендов loadRemote.js

import React, { useState, useEffect } from "react";
import { loadRemoteModule } from "./utils/loadRemote";

function App() {
  const [RemoteComponent, setRemoteComponent] = useState(null);

  useEffect(() => {
    loadRemoteModule("http://localhost:3001", "remoteApp", "./Widget")
      .then((module) => setRemoteComponent(() => module.default))
      .catch((error) => console.error("Error loading remote module:", error));
  }, []);

  return (
    <div>
      <h1>Host Application</h1>
      {RemoteComponent ? <RemoteComponent /> : <p>Loading remote component...</p>}
    </div>
  );
}

export default App;

Строчка loadRemoteModule("http://localhost:3001", "remoteApp", "./Widget") берется не из потолка, мы взяли из remote приложения, адрес приложения, у нас указан порт 3001, название - remoteApp, в ModuleFederationPlugin это name и сам компонент в exposes.

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

import React, { useState, useEffect } from "react";
import { loadRemoteModule } from "./utils/loadRemote";

function App() {
  const [RemoteComponent, setRemoteComponent] = useState(null);

  useEffect(() => {
    loadRemoteModule("http://localhost:3001", "remoteApp", "./Widget")
      .then((module) => setRemoteComponent(() => module.default))
      .catch((error) => console.error("Error loading remote module:", error));
  }, []);

  return (
    <div>
      <h1>Host Application</h1>
      {RemoteComponent ? <RemoteComponent /> : <p>Loading remote component...</p>}
    </div>
  );
}

export default App;

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

Отсутствует адекватной архитектуры приложения

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

Вспоминаем нашу структуру проекта:

my-workspace/
├── apps/                # Приложения (frontend, backend, mobile и др.)
├── libs/                # Библиотеки (переиспользуемые модули, UI-компоненты и др.)

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

Т.к. у нас все микрофронты и их зависимости (libs) находятся в одной монорепозитории, у нас есть возможность вносить оперативно правки и не ждать пока кто-то релизнит стороннюю библиотеку, если вдруг мы бы выбрали подход не монорепозиторий, а микрорепозиторий.

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

branches
├── (pre-)ui-kit              # ui-kit
├── (pre-)utils               # utils
├── (pre-)site                # site

Да и при таком выборе у нас как минимум будет x2 веток, dev ветка pre-, и ветка релиза.

В данном случае это 6 веток. Если мы выбрали корректный подход, то веток было всего 2, pre-site и site, т.к. utils и ui-kit нет смысла выносить в отдельный микрофронтенды.

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

Источник: https://habr.com/ru/articles/859230/


Интересные статьи

Интересные статьи

История видеокарт началась задолго до появления персональных компьютеров. Так, в 1970-х чипы для работы с графикой устанавливали в аркадные автоматы. Со временем были сформированы стандарты работы с G...
В современном мире у одного бекенд-приложения обычно запущено больше одного экземпляра, хотя бы из соображений надёжности. А это значит, что для синхронизации их действий надо что-то придумывать, ка...
Цель статьи, – показать примеры управления реализацией стратегии с помощью корпоративной единой информационной площадки на доступном инструменте, - Битрикс24. В статье на простом языке обсуждаются воз...
Нередко при работе с Bitrix24 REST API возникает необходимость быстро получить содержимое определенных полей всех элементов какого-то списка (например, лидов). Традиционн...
Тема статьи навеяна результатами наблюдений за методикой создания шаблонов различными разработчиками, чьи проекты попадали мне на поддержку. Порой разобраться в, казалось бы, такой простой сущности ка...