Микрофронтенды на Cloudflare: как собрать вертикальное SPA из нескольких Workers

Обновлено в 6:55 утра по тихоокеанскому времени

Сегодня мы представляем новый шаблон Worker для Вертикальных Микрофронтендов (VMFE). Этот шаблон позволяет сопоставить несколько независимых Cloudflare Workers с одним доменом, давая командам возможность работать в полной изоляции — независимо выпуская маркетинговые сайты, документацию и панели управления — при этом представляя пользователю единое, бесшовное приложение.

Большинство архитектур микрофронтендов являются "горизонтальными", что означает, что разные части одной страницы загружаются из разных сервисов. Вертикальные микрофронтенды используют другой подход, разделяя приложение по URL-путям. В этой модели команда, владеющая путём `/blog`, владеет не только компонентом; она владеет всем стеком для этого маршрута — фреймворком, выбором библиотек, CI/CD и многим другим. Владение всем стеком пути или набора путей позволяет командам иметь полный контроль над своей работой и выпускать обновления с уверенностью.

По мере роста команды сталкиваются с проблемами, когда разные фреймворки обслуживают различные варианты использования. Например, маркетинговый сайт может быть лучше реализован на Astro, в то время как для панели управления лучше подойдёт React. Или представьте, что у вас есть монолитная кодовая база, где многие команды выпускают обновления коллективно. Обновление для добавления новых функций от нескольких команд может быть досадно откатано из-за того, что одна команда внесла регрессию. Как решить проблему сокрытия технических деталей реализации от пользователя и дать командам возможность выпускать целостный пользовательский опыт с полной автономией и контролем над своими доменами?

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

Что такое вертикальные микрофронтенды?

Вертикальный микрофронтенд — это архитектурный паттерн, при котором одна независимая команда владеет целым срезом функциональности приложения, от пользовательского интерфейса и до CI/CD-конвейера. Эти срезы определяются путями на домене, где вы можете связать отдельных Workers с определёнными путями:

/      = Маркетинг
/docs  = Документация
/blog  = Блог
/dash  = Панель управления

Мы можем пойти дальше и сосредоточиться на более детальных ассоциациях Worker с подпутями, например, внутри панели управления. Внутри панели управления вы, вероятно, сегментируете различные функции или продукты, добавляя глубину вашему URL-пути (например, /dash/product-a), и переход между двумя продуктами может означать две совершенно разные кодовые базы.

Теперь с вертикальными микрофронтендами у нас также может быть следующее:

/dash/product-a  = WorkerA
/dash/product-b  = WorkerB

Каждый из вышеперечисленных путей представляет собой собственный фронтенд-проект без общего кода между ними. Маршруты product-a и product-b сопоставляются с отдельно развёрнутыми фронтенд-приложениями, которые имеют свои собственные фреймворки, библиотеки, CI/CD-конвейеры, определённые и принадлежащие их собственным командам. НАКОНЕЦ-ТО.

Теперь вы можете владеть своим кодом от начала до конца. Но теперь нам нужно найти способ объединить эти отдельные проекты и, более того, сделать так, чтобы они воспринимались как единый опыт.

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

Внутренне мы теперь используем аналогичную стратегию для нашей собственной панели управления. Когда пользователи переходят из основной панели управления в наш продукт Zero Trust, на самом деле это два совершенно отдельных проекта, и пользователь просто перенаправляется на этот проект по его пути /:accountId/one.

Визуально единый опыт

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

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

Переходы между представлениями (View Transitions)

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

Однако могут быть случаи, когда допустимо, чтобы различные вертикальные микрофронтенды ощущались по-разному. Например, наш маркетинговый сайт, документация и панель управления могут быть уникально оформлены. Пользователь не ожидает, что все три части будут восприниматься как единое целое при переходе между ними. Но… если вы решите внедрить вертикальные срезы в отдельный опыт, например, в панель управления (например, /dash/product-a и /dash/product-b), то пользователи никогда не должны знать, что под капотом это два разных репозитория/воркера/проекта.

Ладно, хватит разговоров — давайте работать. Я говорил, что требуется минимум усилий, чтобы два отдельных проекта ощущались пользователем как один, и если вы ещё не слышали о CSS View Transitions, то сейчас я вас удивлю.

Что, если я скажу вам, что вы можете создавать анимированные переходы между различными представлениями — как в одностраничном (SPA), так и в многостраничном (MPA) приложении — чтобы они ощущались как единое целое? До добавления любых переходов между представлениями, если мы переходим между страницами, принадлежащими двум разным Workers, промежуточным состоянием загрузки был бы белый пустой экран в нашем браузере в течение нескольких сотен миллисекунд, пока не начнётся полная отрисовка следующей страницы. Страницы не ощущались бы целостно, и это определённо не было бы похоже на одностраничное приложение.

Building vertical microfrontends on Cloudflare’s platform

Воспринимается как несколько элементов навигации между каждым сайтом.

Если мы хотим, чтобы элементы оставались на экране, вместо белого пустого листа, мы можем достичь этого, определив CSS View Transitions. С помощью кода ниже мы говорим нашей текущей странице документа, что когда событие перехода между представлениями вот-вот произойдёт, нужно оставить DOM-элемент nav на экране, и если существует какая-либо разница во внешнем виде между нашей текущей страницей и целевой страницей, то мы анимируем её с помощью перехода ease-in-out.

И внезапно два разных Worker'а ощущаются как один.

@supports (view-transition-name: none) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 0.3s;
    animation-timing-function: ease-in-out;
  }
  nav { view-transition-name: navigation; }
}
Building vertical microfrontends on Cloudflare’s platform

Воспринимается как единый элемент навигации между тремя разными сайтами.

Предварительная загрузка (Preloading)

Переход между двумя страницами делает его визуально бесшовным — и мы также хотим, чтобы он ощущался таким же мгновенным, как клиентский SPA. Хотя в настоящее время Firefox и Safari не поддерживают API Правил Спекуляции (Speculation Rules), Chrome/Edge/Opera поддерживают этого нового игрока. API правил спекуляции предназначен для повышения производительности будущих переходов, особенно для URL-адресов документов, делая многостраничные приложения более похожими на одностраничные.

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

<script type="speculationrules">
  {
    "prefetch": [
      {
        "urls": ["https://product-a.com", "https://product-b.com"],
        "requires": ["anonymous-client-ip-when-cross-origin"],
        "referrer_policy": "no-referrer"
      }
    ]
  }
</script>

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

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

Благодаря View Transitions и Speculation Rules мы можем связать вместе совершенно разные репозитории кода так, что они будут ощущаться как единое одностраничное приложение. Дикость, по-моему.

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

Теперь нам нужен механизм для размещения нескольких приложений и метод для их объединения по мере поступления запросов. Определение одного Cloudflare Worker как «Маршрутизатора» (Router) позволяет иметь единую логическую точку (на границе сети) для обработки сетевых запросов и их перенаправления на тот вертикальный микрофронтенд, который отвечает за данный URL-путь. Плюс, не помешает то, что мы можем сопоставить один домен с этим Worker-маршрутизатором, а остальное «просто заработает».

Привязки сервисов (Service Bindings)

Если вы ещё не знакомы с привязками сервисов в Cloudflare Workers, стоит потратить на это время.

Привязки сервисов позволяют одному Worker'у вызывать другой без обращения к общедоступному URL. Привязка сервиса позволяет Worker'у A вызывать метод Worker'а B или передавать запрос от Worker'а A Worker'у B. Разбирая подробнее: Worker-маршрутизатор может обращаться к каждому определенному вертикальному микрофронтенд Worker'у (например, маркетинг, документация, панель управления), при условии, что каждый из них является Cloudflare Worker'ом.

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

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "router",
  "main": "./src/router.js",
  "services": [
    {
      "binding": "HOME",
      "service": "worker_marketing"
    },
    {
      "binding": "DOCS",
      "service": "worker_docs"
    },
    {
      "binding": "DASH",
      "service": "worker_dash"
    },
  ]
}

Наш пример выше определен в нашем Worker-маршрутизаторе и сообщает нам, что нам разрешено делать запросы в три отдельных дополнительных Worker'а (marketing, docs и dash). Предоставление разрешений так же просто, но давайте углубимся в более сложную логику маршрутизации запросов и перезаписи HTML в сетевых ответах.

Маршрутизация запросов

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

Первый шаг — сопоставить URL-пути с соответствующими Worker'ами. Когда получен определенный URL запроса, мы должны знать, куда его нужно перенаправить. Мы делаем это путем определения правил. Хотя мы поддерживаем wildcard-маршруты, динамические пути и параметрические ограничения, мы сосредоточимся на основах — буквальных префиксах путей — так как это иллюстрирует суть более понятно.

В этом примере у нас три микрофронтенда:

/      = Маркетинг
/docs  = Документация
/dash  = Панель управления

Каждый из вышеуказанных путей нужно сопоставить с конкретным Worker'ом (см. наше определение сервисов в wrangler в разделе выше). Для нашего Worker-маршрутизатора мы определяем дополнительную переменную со следующими данными, чтобы мы могли знать, какие пути сопоставляются с какими привязками сервисов. Теперь мы знаем, куда маршрутизировать пользователей по мере поступления запросов! Определите переменную wrangler с именем ROUTES и следующим содержимым:

{
  "routes":[
    {"binding": "HOME", "path": "/"},
    {"binding": "DOCS", "path": "/docs"},
    {"binding": "DASH", "path": "/dash"}
  ]
}

Представим, что пользователь посещает путь нашего сайта /docs/installation. Под капотом происходит следующее: запрос сначала достигает нашего Worker-маршрутизатора, который отвечает за понимание того, какие URL-пути сопоставляются с какими отдельными Worker'ами. Он понимает, что префикс пути /docs сопоставлен с нашей привязкой сервиса DOCS, которая, ссылаясь на наш файл wrangler, указывает на наш проект worker_docs. Наш Worker-маршрутизатор, зная, что /docs определен как маршрут вертикального микрофронтенда, удаляет префикс /docs из пути и перенаправляет запрос нашему Worker'у worker_docs для обработки запроса, а затем, наконец, возвращает полученный ответ.

Почему он отбрасывает путь /docs? Это было решение детали реализации, принятое для того, чтобы при доступе к Worker'у через Worker-маршрутизатор можно было очистить URL и обработать запрос так, как если бы он был вызван извне нашего Worker-маршрутизатора. Как и любой Cloudflare Worker, наш сервис worker_docs может иметь свой собственный индивидуальный URL, по которому к нему можно получить доступ. Мы решили, что хотим, чтобы этот сервисный URL продолжал работать независимо. При подключении к нашему новому Worker-маршрутизатору он будет автоматически обрабатывать удаление префикса, чтобы сервис был доступен как по своему собственному URL, так и через наш Worker-маршрутизатор... в любом случае, неважно.

HTMLRewriter

Разделение наших различных фронтенд-сервисов по URL-путям (например, /docs или /dash) позволяет нам легко перенаправлять запрос, но когда наш ответ содержит HTML, который не знает, что его проксируют через компонент пути... это вызывает проблемы.

Допустим, на сайте документации в ответе есть тег изображения <img src="./logo.png" />. Если наш пользователь посещал эту страницу по адресу https://website.com/docs/, то загрузка файла logo.png, скорее всего, завершится неудачей, потому что путь /docs в некотором роде искусственно определен только нашим Worker-маршрутизатором.

Только когда к нашим сервисам обращаются через наш Worker-маршрутизатор, нам нужно выполнить некоторую перезапись HTML абсолютных путей, чтобы наш возвращаемый браузеру ответ ссылался на валидные ресурсы. На практике происходит следующее: когда запрос проходит через наш Worker-маршрутизатор, мы передаем запрос в правильную привязку сервиса и получаем от него ответ. Прежде чем вернуть его клиенту, у нас есть возможность переписать DOM — поэтому там, где мы видим абсолютные пути, мы добавляем к ним проксированный путь. Там, где ранее наш HTML возвращал тег изображения с <img src="./logo.png" />, мы теперь модифицируем его перед возвратом в клиентский браузер на <img src="./docs/logo.png" />.

Building vertical microfrontends on Cloudflare’s platform

Давайте на мгновение вернемся к магии CSS-переходов между состояниями представления (View Transitions) и предварительной загрузки документов. Конечно, мы могли бы вручную поместить этот код в наши проекты, и он бы работал, но этот Worker-маршрутизатор будет автоматически обрабатывать эту логику для нас, также используя HTMLRewriter.

В переменной ROUTES вашего Worker-маршрутизатора, если вы установите smoothTransitions в значение true на корневом уровне, то код CSS-переходов между состояниями представления будет добавлен автоматически. Кроме того, если вы установите ключ preload внутри маршрута в значение true, то код скрипта с правилами предположения (Speculation Rules) для этого маршрута также будет автоматически добавлен.

Ниже приведен пример работы обоих:

{
  "smoothTransitions":true, 
  "routes":[
    {"binding": "APP1", "path": "/app1", "preload": true},
    {"binding": "APP2", "path": "/app2", "preload": true}
  ]
}

Начать работу

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

Посетите Cloudflare Dashboard по этой прямой ссылке или перейдите в раздел «Workers & Pages» и нажмите кнопку «Create application», чтобы начать работу. Затем нажмите «Select a template», а затем «Create microfrontend», и вы можете приступить к настройке.

Building vertical microfrontends on Cloudflare’s platform