Микро-фронтенды

У вас есть крупномасштабный пользовательский интерфейс, восстановление которого занимает слишком много времени? У вас несколько команд и вы часто сталкиваетесь с конфликтами кода и другими проблемами интеграции? Приложение отвечает за слишком много функций? Микро-интерфейсы, вероятно, могут вам здесь помочь. Micro-Frontends берет концепцию архитектуры микросервисов из бэкэнд-инжиниринга и применяет ее к фронтенд-разработке. Но как разделение пользовательского интерфейса на несколько интерфейсов поможет масштабировать ваш проект?

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

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

Micro-Frontend не имеет определенной структуры или API, это архитектурная концепция. Основная предпосылка микро-интерфейсов - разделение вашего приложения на несколько более мелких приложений, каждое со своими собственными репозиториями, которые сосредоточены на одной функции. Эта архитектура может применяться по-разному. Архитектура может быть как можно более либеральной, где каждое приложение может быть реализовано с разными фреймворками, или она может быть более предписывающей, предоставляя инструменты и обеспечивая выполнение проектных решений. У обоих этих подходов есть свои преимущества и недостатки, и они во многом зависят от потребностей вашей организации.

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

Когда использовать эту архитектуру?

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

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

Зачем использовать этот подход?

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

Более быстрые сборки

Чем больше становится проект, тем больше времени может потребоваться для его создания. Хотя такие компоновщики, как Webpack и Parcel, приложили немало усилий для повышения производительности компоновки за счет использования нескольких потоков и кеширования, на мой взгляд, это лишь временная мера для решения более фундаментальной проблемы. Даже с этими улучшениями производительности, по мере того, как ваше приложение продолжает расти, ваше приложение будет постепенно замедляться. Помните, что без хорошего опыта разработчика трудно обеспечить хорошее взаимодействие с пользователем.

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

Динамическое развертывание

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

Распараллеливание разработки

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

По большей части этим командам не нужно ничего знать друг о друге. Однако между этими двумя приложениями все еще должна быть точка интеграции. Каждой команде необходимо определить и гарантировать обратную совместимость в общедоступном API. Этот API обычно реализуется с помощью URL-адреса.

Как реализовать?

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

Маршрутизация и загрузка приложений

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

onRouteChange (route) {
    // Assuming routes are "/<app_name>/<internal-app-url>".
    let parts = route.split('/');
    let app = parts[1];
    let app_url = parts.slice(2).join('/');
    
    if (this.isRunningApp()) {
        this.suspendCurrentApp();
    }
    import(`/${app}/main.js`).then(app => {
        this.startApp(app, app_url);
    });
}

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

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

Жизненный цикл приложения

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

class MyApp extends Application {
    constructor (args) {}
    onAppSuspended () {}
    onAppResumed () {}
    onAppQuit () {}
}

Связь между приложениями

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

Для простых данных здесь может работать URL-адрес. Команда, ответственная за приложение / функцию, будет реализовывать общедоступный URL-API, для которого они могут гарантировать обратную совместимость. Это похоже на то, как приложения взаимодействуют в операционных системах, таких как Android, где вы можете зарегистрировать пользовательские обработчики URL с разными целями. Операционная система перехватывает эти URL-адреса и загружает соответствующее приложение, передавая данные. Тот же принцип можно использовать в микро-интерфейсах. Среда выполнения перехватит URL-адрес, загрузит приложение и передаст остальные данные URL-адреса в приложение.

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

Совместное использование библиотек

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

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

libraries/
    preact/
        8/
        10/
    components/
        1/
        2/

Какие числа? Это основные версии этих библиотек. Если библиотека следует за semver, по определению, она обратно совместима, если у нее такой же основной номер версии. Когда приложение хочет использовать библиотеку, оно должно указывать основную версию, которую оно хочет использовать, а не конкретную вспомогательную версию и версию исправления.

Преимущества этого подхода:

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

А как насчет iframe?

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

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

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

Однако в архитектуре микро-интерфейса глобальные объекты необходимо тщательно контролировать. Globals не только относится к переменным или состоянию, но также может включать в себя такие вещи, как обработчики событий окна / документа, циклы requestAnimationFrame, постоянные сетевые соединения, все, что может активно работать, несмотря на то, что приложение больше не находится в DOM. Невероятно легко забыть, что эти вещи могут протекать и что они требуют надлежащего демонтажа.

Заключение

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

Спасибо за прочтение!