DevGang
Авторизоваться

Как разрабатывать и внедрять микро-фронтенды с Single-SPA

Микро-фронтенды - это будущее интерфейсной веб-разработки.

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

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

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

Настроим непрерывную интеграцию с помощью Travis CI. Каждый конвейер CI будет связывать JavaScript для приложения микро-фронтенда, а затем загружать полученные артефакты сборки в AWS S3.

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

Обзор демонстрационного приложения

Демо-приложение - конечный результат
Демо-приложение - конечный результат

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

  1. Приложение контейнера, которое служит контейнером главной страницы и координирует монтаж и демонтаж микро-фронтенд приложений
  2. Приложение микро-фронтенд "панель навигации", которое всегда присутствует на странице
  3. Микро-фронтенд приложение «страница 1», показывает только когда активна
  4. Микро-фронтенд приложение «страница 2», что также показывает только когда активна

Эти четыре приложения живут в отдельных репозиториях, доступных на GitHub, на которые я ссылался выше.

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

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

Создание приложения-контейнера

Для создания приложений для этой демонстрации мы будем использовать инструмент интерфейса командной строки (CLI), который называется create-single-spa. Версия create-single-spa на момент написания - 1.10.0, а версия single-spa, установленная через CLI, - 4.4.2.

Мы выполним следующие шаги для создания приложения контейнера (также иногда называемого корневым конфигом):

mkdir single-spa-demo

cd single-spa-demo

mkdir single-spa-demo-root-config

cd single-spa-demo-root-config

npx create-single-spa

Затем мы будем следовать указаниям CLI:

  1. Выберите «single spa root config»
  2. Выберите «yarn» или «npm» (я выбрал «yarn»)
  3. Введите название организации (я использовал «thawkin3», но это может быть все, что вы хотите)

Теперь, если вы проверите каталог single-spa-demo-root-config, вы должны увидеть скелетное приложение конфигурации корня. Мы немного настроим это, но сначала давайте также воспользуемся инструментом CLI для создания трех других приложений с микро-фронтендом.

Создание приложений Micro-Frontend

Чтобы создать наше первое приложение для микро-фронтенда, "панель навигации", мы выполним следующие действия:

cd ..

mkdir single-spa-demo-nav

cd single-spa-demo-nav

npx create-single-spa

Затем мы будем следовать указаниям CLI:

  1. Выберите «single-spa application / parcel»
  2. Выберите «react»
  3. Выберите «yarn» или «npm» (я выбрал «yarn»)
  4. Введите название организации, которое вы использовали при создании корневого конфигурационного приложения («thawkin3» в моем случае)
  5. Введите название проекта (я использовал «single-spa-demo-nav»)

Теперь, когда мы создали приложение "панель навигации", мы можем выполнить те же самые шаги, чтобы создать наши двухстраничные приложения. Но мы заменим каждое место, где мы видим «single-spa-demo-nav», на «single-spa-demo-page-1», а затем на «single-spa-demo-page-2» во второй раз.

На данный момент мы создали все четыре приложения, которые нам нужны: одно приложение-контейнер и три приложения с микро-фронтендом. Теперь пришло время соединить наши приложения.

Регистрация приложений Micro-Frontend с Container приложением

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

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

Внутри каталога single-spa-demo-root-config, в файле activity-functions.js, мы напишем следующие функции активности для наших трех приложений с микро-фронтендом.

export function prefix(location, ...prefixes) {
  return prefixes.some(
    prefix => location.href.indexOf(`${location.origin}/${prefix}`) !== -1
  );
}

export function nav() {
  // Навигация всегда активна
  return true;
}

export function page1(location) {
  return prefix(location, 'page1');
}

export function page2(location) {
  return prefix(location, 'page2');
}

Далее нам нужно зарегистрировать наши три приложения с микро-фронтендом с помощью single-spa. Для этого мы используем функцию registerApplication. Эта функция принимает минимум три аргумента: имя приложения, метод загрузки приложения и функцию активности, чтобы определить, когда приложение активно.

Внутри каталога single-spa-demo-root-config, в файл root-config.js, мы добавим следующий код для регистрации наших приложений:

import { registerApplication, start } from "single-spa";
import * as isActive from "./activity-functions";

registerApplication(
  "@thawkin3/single-spa-demo-nav",
  () => System.import("@thawkin3/single-spa-demo-nav"),
  isActive.nav
);

registerApplication(
  "@thawkin3/single-spa-demo-page-1",
  () => System.import("@thawkin3/single-spa-demo-page-1"),
  isActive.page1
);

registerApplication(
  "@thawkin3/single-spa-demo-page-2",
  () => System.import("@thawkin3/single-spa-demo-page-2"),
  isActive.page2
);

start();

Теперь, когда мы настроили функции активности и зарегистрировали наши приложения, последний шаг, прежде чем мы сможем запустить его локально, - обновить карту локального импорта внутри файла index.ejs в том же каталоге.

Мы добавим тег head в следующий код, чтобы указать, где каждое приложение может быть найдено при локальном запуске:

<% if (isLocal) { %>
  <script type="systemjs-importmap">
    {
      "imports": {
        "@thawkin3/root-config": "http://localhost:9000/root-config.js",
        "@thawkin3/single-spa-demo-nav": "http://localhost:9001/thawkin3-single-spa-demo-nav.js",
        "@thawkin3/single-spa-demo-page-1": "http://localhost:9002/thawkin3-single-spa-demo-page-1.js",
        "@thawkin3/single-spa-demo-page-2": "http://localhost:9003/thawkin3-single-spa-demo-page-2.js"
      }
    }
  </script>
<% } %> 

Каждое приложение содержит свой собственный сценарий запуска, что означает, что каждое приложение будет работать локально на своем собственном сервере разработки во время локальной разработки. Как видите, наше приложение навигационной панели находится на порту 9001, наше приложение страницы 1 - на порту 9002, а наше приложение страницы 2 - на порту 9003.

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

Тестовый запуск для локального запуска

Чтобы наше приложение работало локально, мы можем выполнить следующие шаги:

  1. Откройте четыре вкладки терминала, по одной для каждого приложения
  2. Для корневого конфига в каталоге single-spa-demo-root-configyarn start(по умолчанию работает на порту 9000)
  3. Для приложения nav в каталоге single-spa-demo-nav:yarn start --port 9001
  4. Для приложения страницы 1 в каталоге single-spa-demo-page-1:yarn start --port 9002
  5. Для приложения страницы 2 в каталоге single-spa-demo-page-2:yarn start --port 9003

Теперь перейдем в браузер по адресу http: // localhost: 9000 для просмотра нашего приложения.

Мы должны увидеть... какой-нибудь текст! Супер захватывающе.

Демо-приложение - главная страница
Демо-приложение - главная страница

На нашей главной странице отображается панель навигации, потому что приложение "панели навигации" всегда активно.

Теперь перейдем к http: // localhost: 9000 / page1. Как показано выше в наших функциях активности, мы указали, что приложение страницы 1 должно быть активным (показано), когда путь URL начинается с «page1». Таким образом, это активирует приложение страницы 1, и теперь мы должны увидеть текст для панели навигации и приложения страницы 1.

Демо-приложение - маршрут страницы 1
Демо-приложение - маршрут страницы 1

Еще раз, теперь давайте перейдем к http: // localhost: 9000 / page2. Как и ожидалось, это активирует приложение страницы 2, поэтому теперь мы должны увидеть текст для панели навигации и приложения страницы 2.

Демо-приложение - маршрут страницы 2
Демо-приложение - маршрут страницы 2

Незначительные изменения в приложениях

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

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

Указание контейнеров монтирования

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

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

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

В нашем файле index.ejs, с которым мы работали ранее, давайте добавим HTML-код, который будет служить основными контейнерами содержимого для страницы:

<div id="nav-container"></div>
<main>
  <div id="page-1-container"></div>
  <div id="page-2-container"></div>
</main>

Затем в нашем файле root-config.js, где мы зарегистрировали наши приложения, давайте предоставим четвертый аргумент для каждого вызова функции, который включает элемент DOM, в который мы хотели бы подключить каждое приложение:  

import { registerApplication, start } from "single-spa";
import * as isActive from "./activity-functions";

registerApplication(
  "@thawkin3/single-spa-demo-nav",
  () => System.import("@thawkin3/single-spa-demo-nav"),
  isActive.nav,
  { domElement: document.getElementById('nav-container') }
);

registerApplication(
  "@thawkin3/single-spa-demo-page-1",
  () => System.import("@thawkin3/single-spa-demo-page-1"),
  isActive.page1,
  { domElement: document.getElementById('page-1-container') }
);

registerApplication(
  "@thawkin3/single-spa-demo-page-2",
  () => System.import("@thawkin3/single-spa-demo-page-2"),
  isActive.page2,
  { domElement: document.getElementById('page-2-container') }
);

start();

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

Стилизация приложения

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

В каталоге single-spa-demo-root-config, в файле index.ejs снова, мы можем добавить несколько основных стилей для всего приложения, вставив следующий CSS в нижней части тега head:

<style>
  body, html { margin: 0; padding: 0; font-size: 16px; font-family: Arial, Helvetica, sans-serif; height: 100%; }
  body { display: flex; flex-direction: column; }
  * { box-sizing: border-box; }
</style>

Затем мы можем стилизовать наше приложение "панели навигации", найдя каталог single-spa-demo-nav, создав файл root.component.css и добавив следующий CSS:  

.nav {
  display: flex;
  flex-direction: row;
  padding: 20px;
  background: #000;
  color: #fff;
}

.link {
  margin-right: 20px;
  color: #fff;
  text-decoration: none;
}

.link:hover,
.link:focus {
  color: #1098f7;
}

Затем мы можем обновить файл root.component.js в том же каталоге, чтобы импортировать файл CSS и применить эти классы и стили к нашему HTML. Мы также изменим содержимое навигационной панели, чтобы на самом деле содержать две ссылки, чтобы мы могли перемещаться по приложению, щелкая ссылки вместо ввода нового URL-адреса в адресной строке браузера.

import React from "react";
import "./root.component.css";

export default function Root() {
  return (
    <nav className="nav">
      <a href="/page1" className="link">
        Page 1
      </a>
      <a href="/page2" className="link">
        Page 2
      </a>
    </nav>
  );
}

Мы будем следовать аналогичному процессу для приложений страницы 1 и страницы 2. Мы создадим файл root.component.css для каждого приложения в соответствующих каталогах проектов и обновим файлы root.component.js для обоих приложений.

Для приложения на странице 1 изменения выглядят так:

.container1 {
  background: #1098f7;
  color: white;
  padding: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 1;
  font-size: 3rem;
}
import React from "react";
import "./root.component.css";

export default function Root() {
  return (
    <div className="container1">
      <p>Page 1 App</p>
    </div>
  );
}

И для приложения страницы 2 изменения выглядят так:

.container2 {
  background: #9e4770;
  color: white;
  padding: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 1;
  font-size: 3rem;
}
import React from "react";
import "./root.component.css";

export default function Root() {
  return (
    <div className="container2">
      <p>Page 2 App</p>
    </div>
  );
}

Добавление React Router

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

Чтобы использовать React Router, нам сначала нужно его установить. Из терминала в каталог single-spa-demo-nav мы установим React Router, используя yarn, введя yarn add react-router-dom. (Или, если вы используете npm, вы можете введя npm install react-router-dom.)

Затем в каталоге single-spa-demo-nav  в файле root.component.js мы заменим наши теги привязки компонентами Link React Router следующим образом:

import React from "react";
import { BrowserRouter, Link } from "react-router-dom";
import "./root.component.css";

export default function Root() {
  return (
    <BrowserRouter>
      <nav className="nav">
        <Link to="/page1" className="link">
          Page 1
        </Link>
        <Link to="/page2" className="link">
          Page 2
        </Link>
      </nav>
    </BrowserRouter>
  );
}

Это выглядит и работает намного лучше!

Демо-приложение - стилизовано и использует React Router
Демо-приложение - стилизовано и использует React Router

Готовимся к производству

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

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

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

В этой статье мы собираемся использовать AWS S3 для хранения наших активов, и мы собираемся использовать Travis CI для выполнения задания сборки и задания загрузки как части конвейера непрерывной интеграции.

Давайте сначала настроим AWS S3.

Настройка AWS S3 Bucket

Это должно быть само собой разумеющимся, но вам понадобится учетная запись AWS.

Если мы являемся пользователем root в нашей учетной записи AWS, мы можем создать нового пользователя IAM, у которого будет только программный доступ. Это означает, что нам будет предоставлен идентификатор ключа доступа и секретный ключ доступа от AWS при создании нового пользователя. Мы захотим сохранить их в надежном месте, так как они нам понадобятся позже.

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

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

Затем нам нужно создать корзину S3. S3 расшифровывается как Simple Storage Service и по сути является местом для загрузки и хранения файлов, размещенных на серверах Amazon. Корзина - это просто каталог.

Я назвал свою корзину «single-spa-demo», но вы можете назвать свою как хотите. Вы можете следовать руководствам AWS, чтобы узнать, как создать новую корзину, для получения дополнительной информации.

Корзина AWS S3
Корзина AWS S3

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

В разрешениях для нашей корзины мы можем добавить следующие правила конфигурации CORS:

<CORSConfiguration>
 <CORSRule>
   <AllowedOrigin>*</AllowedOrigin>
   <AllowedMethod>GET</AllowedMethod>
 </CORSRule>
</CORSConfiguration>

В консоли AWS после нажатия кнопки «Сохранить» это выглядит так:

Конфигурация CORS
Конфигурация CORS

Создание задания Travis CI для загрузки артефактов в AWS S3

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

Для этого мы воспользуемся Travis CI. Как упоминалось ранее, каждое приложение живет в своем собственном репо на GitHub, поэтому у нас есть четыре репозитория GitHub для работы. Мы можем интегрировать Travis CI с каждым из наших репозиториев и настроить конвейеры непрерывной интеграции для каждого из них.

Чтобы настроить Travis CI для любого данного проекта, мы создаем файл .travis.yml в корневом каталоге. Давайте создадим этот файл в каталоге single-spa-demo-root-config и вставим следующий код:

language: node_js
node_js:
  - node
script:
  - yarn build
  - echo "Commit sha - $TRAVIS_COMMIT"
  - mkdir -p dist/@thawkin3/root-config/$TRAVIS_COMMIT
  - mv dist/*.* dist/@thawkin3/root-config/$TRAVIS_COMMIT/
deploy:
  provider: s3
  access_key_id: "$AWS_ACCESS_KEY_ID"
  secret_access_key: "$AWS_SECRET_ACCESS_KEY"
  bucket: "single-spa-demo"
  region: "us-west-2"
  cache-control: "max-age=31536000"
  acl: "public_read"
  local_dir: dist
  skip_cleanup: true
  on:
    branch: master

Это то, что я придумал после просмотра документации Travis CI для загрузки AWS S3 и примера конфигурации Travis CI с single-spa.

Поскольку мы не хотим, чтобы наши секреты AWS раскрывались в нашем репо GitHub, мы можем хранить их как переменные среды. Вы можете поместить переменные среды и их секретные значения в веб-консоль Travis CI для всего, что вы хотите сохранить конфиденциальным, чтобы файл .travis.yml брал эти значения именно отсюда.

Теперь, когда мы фиксируем и отправляем новый код в основную ветку, запускается задание Travis CI, которое создает пакет JavaScript для приложения, а затем загружает эти ресурсы в S3. Чтобы проверить это, мы можем проверить консоль AWS и увидеть наши недавно загруженные файлы:

Загруженные файлы в результате задания Travis CI
Загруженные файлы в результате задания Travis CI

Теперь нам нужно реализовать ту же конфигурацию Travis CI для наших других трех микро-фронтенд-приложений, но поменяв имена каталогов в них на .travis.yml по мере необходимости. После выполнения тех же шагов и слияния нашего кода у нас теперь есть четыре каталога, созданные в нашей корзине S3, по одному для каждого репо.

Четыре каталога в нашей корзине S3
Четыре каталога в нашей корзине S3

Создание карты импорта для производства

Давайте вспомним, что мы сделали до сих пор. У нас есть четыре приложения, и все они находятся в разных репозиториях GitHub. Каждое репо настраивается с помощью Travis CI для запуска задания, когда код объединяется в основную ветку, и это задание обрабатывает загрузку артефактов сборки в корзину S3.

Когда все это собрано в одном месте, по-прежнему не хватает одного: как на эти новые артефакты сборки ссылаются в нашем контейнерном приложении? Другими словами, несмотря на то, что мы добавляем новые пакеты JavaScript для наших микро-фронтендов с каждым новым обновлением, новый код на самом деле еще не используется в нашем приложении контейнера!

Если мы вспомним, как наше приложение запускалось локально, мы использовали карту импорта. Эта карта импорта представляет собой просто JSON, который сообщает приложению-контейнеру, где можно найти каждый пакет JavaScript.

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

Если мы посмотрим в каталог single-spa-demo-root-config, в файл index.ejs, мы увидим эту строку:

<script type="systemjs-importmap" src="https://storage.googleapis.com/react.microfrontends.app/importmap.json"></script>

При открытии этого URL-адреса в браузере открывается карта импорта, которая выглядит следующим образом:

{
  "imports": {
    "react": "https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.production.min.js",
    "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.production.min.js",
    "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.5.3/lib/system/single-spa.min.js",
    "@react-mf/root-config": "https://react.microfrontends.app/root-config/e129469347bb89b7ff74bcbebb53cc0bb4f5e27f/react-mf-root-config.js",
    "@react-mf/navbar": "https://react.microfrontends.app/navbar/631442f229de2401a1e7c7835dc7a56f7db606ea/react-mf-navbar.js",
    "@react-mf/styleguide": "https://react.microfrontends.app/styleguide/f965d7d74e99f032c27ba464e55051ae519b05dd/react-mf-styleguide.js",
    "@react-mf/people": "https://react.microfrontends.app/people/dd205282fbd60b09bb3a937180291f56e300d9db/react-mf-people.js",
    "@react-mf/api": "https://react.microfrontends.app/api/2966a1ca7799753466b7f4834ed6b4f2283123c5/react-mf-api.js",
    "@react-mf/planets": "https://react.microfrontends.app/planets/5f7fc62b71baeb7a0724d4d214565faedffd8f61/react-mf-planets.js",
    "@react-mf/things": "https://react.microfrontends.app/things/7f209a1ed9ac9690835c57a3a8eb59c17114bb1d/react-mf-things.js",
    "rxjs": "https://cdn.jsdelivr.net/npm/@esm-bundle/rxjs@6.5.5/system/rxjs.min.js",
    "rxjs/operators": "https://cdn.jsdelivr.net/npm/@esm-bundle/rxjs@6.5.5/system/rxjs-operators.min.js"
  }
}

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

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

{
  "imports": {
    "react": "https://cdn.jsdelivr.net/npm/react@16.13.0/umd/react.production.min.js",
    "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.13.0/umd/react-dom.production.min.js",
    "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.5.1/lib/system/single-spa.min.js",
    "@thawkin3/root-config": "https://single-spa-demo.s3-us-west-2.amazonaws.com/%40thawkin3/root-config/179ba4f2ce4d517bf461bee986d1026c34967141/root-config.js",
    "@thawkin3/single-spa-demo-nav": "https://single-spa-demo.s3-us-west-2.amazonaws.com/%40thawkin3/single-spa-demo-nav/f0e9d35392ea0da8385f6cd490d6c06577809f16/thawkin3-single-spa-demo-nav.js",
    "@thawkin3/single-spa-demo-page-1": "https://single-spa-demo.s3-us-west-2.amazonaws.com/%40thawkin3/single-spa-demo-page-1/4fd417ee3faf575fcc29d17d874e52c15e6f0780/thawkin3-single-spa-demo-page-1.js",
    "@thawkin3/single-spa-demo-page-2": "https://single-spa-demo.s3-us-west-2.amazonaws.com/%40thawkin3/single-spa-demo-page-2/8c58a825c1552aab823bcbd5bdd13faf2bd4f9dc/thawkin3-single-spa-demo-page-2.js"
  }
}

Вы заметите, что первые три импорта предназначены для общих зависимостей: react, react-dom и single-spa. Таким образом, в нашем приложении не будет четырех копий React, что приведет к раздуванию и увеличению времени загрузки. Далее у нас есть импорт для каждого из наших четырех приложений. URL - это просто URL для каждого загруженного файла в S3 (в терминологии AWS это называется «object»).

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

Примечание. Это довольно важное и интересное предостережение при использовании single-spa: карта импорта на самом деле нигде не находится в системе управления версиями или в любом из репозиториев git. Таким образом, карту импорта можно обновлять «на лету», не требуя внесения изменений в репо. Мы вернемся к этой концепции чуть позже.

Импорт карты, загруженной вручную в корзину S3
Импорт карты, загруженной вручную в корзину S3

Наконец, теперь мы можем ссылаться на этот новый файл в нашем файле index.ejs вместо ссылки на исходную карту импорта.

<script type="systemjs-importmap" src="//single-spa-demo.s3-us-west-2.amazonaws.com/%40thawkin3/importmap.json"></script>

Создание рабочего сервера

Мы приближаемся к тому, чтобы что-то было запущено в производство! Мы собираемся разместить эту демонстрацию на Heroku, поэтому для этого нам потребуется создать простой сервер Node.js и Express для обслуживания нашего файла.

Сначала в каталоге single-spa-demo-root-config мы установим express, запустив yarn add express(или npm install express). Далее мы добавим файл server.js с небольшим количеством кода для запуска экспресс-сервера и обслуживания нашего основного файла index.html.

const express = require("express");
const path = require("path");
const PORT = process.env.PORT || 5000;

express()
  .use(express.static(path.join(__dirname, "dist")))
  .get("*", (req, res) => {
    res.sendFile("index.html", { root: "dist" });
  })
  .listen(PORT, () => console.log(`Listening on ${PORT}`));

Наконец, мы обновим сценарии NPM в нашем файле package.json, чтобы различать запуск сервера в режиме разработки и запуск сервера в рабочем режиме.  

"scripts": {
  "build": "webpack --mode=production",
  "lint": "eslint src",
  "prettier": "prettier --write './**'",
  "start:dev": "webpack-dev-server --mode=development --port 9000 --env.isLocal=true",
  "start": "node server.js",
  "test": "jest"
}

Развертывание в Heroku

Теперь, когда у нас есть готовый производственный сервер, давайте развернем эту штуку в Heroku! Для этого вам потребуется создать учетную запись Heroku, установить Heroku CLI и войти в систему. Развертывание в Heroku выполняется так же просто:

  1. В каталоге single-spa-demo-root-configheroku create thawkin3-single-spa-demo(изменение последнего аргумента на уникальное имя, которое будет использоваться для вашего приложения Heroku)
  2. git push heroku master
  3. heroku open

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

Демо-приложение - запущено в производство
Демо-приложение - запущено в производство

Создание обновлений

Стоимость установки любого микросервиса или микро-фронтенда часто намного превышает стоимость установки монолита; только позже вы начнете пожинать плоды.

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

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

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

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

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

Но как сделать такие обновления? 

Прямо сейчас у нас есть карта производственного импорта в нашем файле index.ejs, но она просто указывает на файл, который мы вручную загрузили в нашу корзину S3. Если бы мы хотели выпустить некоторые новые изменения прямо сейчас, нам нужно было бы добавить новый код для одного из микро-фронтенда, получить новый артефакт сборки, а затем вручную обновить карту импорта со ссылкой на новый пакет JavaScript.

Есть ли способ автоматизировать это? Да!

Обновление одного из приложений

Допустим, мы хотим обновить наше приложение на странице 1, чтобы оно отображало другой текст. Чтобы автоматизировать развертывание этого изменения, мы можем обновить наш конвейер CI, чтобы не только создать артефакт и загрузить его в нашу корзину S3, но также обновить карту импорта для ссылки на новый URL-адрес для последнего пакета JavaScript.

Начнем с обновления нашего файла .travis.yml так:

language: node_js
node_js:
  - node
env:
  global:
    # включать $HOME/.local/bin for `aws`
    - PATH=$HOME/.local/bin:$PATH
before_install:
  - pyenv global 3.7.1
  - pip install -U pip
  - pip install awscli
script:
  - yarn build
  - echo "Commit sha - $TRAVIS_COMMIT"
  - mkdir -p dist/@thawkin3/root-config/$TRAVIS_COMMIT
  - mv dist/*.* dist/@thawkin3/root-config/$TRAVIS_COMMIT/
deploy:
  provider: s3
  access_key_id: "$AWS_ACCESS_KEY_ID"
  secret_access_key: "$AWS_SECRET_ACCESS_KEY"
  bucket: "single-spa-demo"
  region: "us-west-2"
  cache-control: "max-age=31536000"
  acl: "public_read"
  local_dir: dist
  skip_cleanup: true
  on:
    branch: master
after_deploy:
  - chmod +x after_deploy.sh
  - "./after_deploy.sh"

Основными изменениями здесь являются добавление глобальной переменной среды, установка интерфейса командной строки AWS и добавление сценария after_deploy как части конвейера. Это ссылка на файл after_deploy.sh, который нам нужно создать. Содержимое будет:  

echo "Downloading import map from S3"
aws s3 cp s3://single-spa-demo/@thawkin3/importmap.json importmap.json
echo "Updating import map to point to new version of @thawkin3/root-config"
node update-importmap.mjs
echo "Uploading new import map to S3"
aws s3 cp importmap.json s3://single-spa-demo/@thawkin3/importmap.json --cache-control 'public, must-revalidate, max-age=0' --acl 'public-read'
echo "Deployment successful"

Этот файл загружает существующую карту импорта из S3, изменяет ее для ссылки на новый артефакт сборки, а затем повторно загружает обновленную карту импорта в S3. Чтобы обработать фактическое обновление содержимого файла карты импорта, мы используем специальный сценарий, который мы добавим в файл с именемupdate-importmap.mjs.

// Обратите внимание, что этот файл требует node@13.2.0 или выше (или флаг -- experimental-modules)
import fs from "fs";
import path from "path";
import https from "https";

const importMapFilePath = path.resolve(process.cwd(), "importmap.json");
const importMap = JSON.parse(fs.readFileSync(importMapFilePath));
const url = `https://single-spa-demo.s3-us-west-2.amazonaws.com/%40thawkin3/root-config/${process.env.TRAVIS_COMMIT}/root-config.js`;

https
  .get(url, res => {
    // Http-редиректы (301, 302 и т.д.) в настоящее время не поддерживаются, но могут быть добавлены
если (res.statusCode >= 200 && res.statusCode < 300) {
      if (
        res.headers["content-type"] &&
        res.headers["content-type"].toLowerCase().trim() ===
          "application/javascript"
      ) {
        const moduleName = `@thawkin3/root-config`;
        importMap.imports[moduleName] = url;
        fs.writeFileSync(importMapFilePath, JSON.stringify(importMap, null, 2));
        console.log(
          `Updated import map for module ${moduleName}. New url is ${url}.`
        );
      } else {
        urlNotDownloadable(
          url,
          Error(`Content-Type response header must be application/javascript`)
        );
      }
    } else {
      urlNotDownloadable(
        url,
        Error(`HTTP response status was ${res.statusCode}`)
      );
    }
  })
  .on("error", err => {
    urlNotDownloadable(url, err);
  });

function urlNotDownloadable(url, err) {
  throw Error(
    `Refusing to update import map - could not download javascript file at url ${url}. Error was '${err.message}'`
  );
}

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

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

Боковое примечание к карте импорта

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

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

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

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

Момент истины

С этими изменениями в наших конвейерах CI пришло время для последнего момента истины: можем ли мы обновить одно из наших микро-интерфейсных приложений, развернуть его независимо, а затем увидеть, как эти изменения вступают в силу в производстве, не касаясь ни одного из наших других приложений?

В каталоге single-spa-demo-page-1, в файле root.component.js, давайте изменим текст с «Приложение для страницы 1» на «Приложение для страницы 1 - ОБНОВЛЕНО!» Затем давайте зафиксируем это изменение, отправим и объединим его в master.

Это запустит конвейер Travis CI для создания нового артефакта приложения страницы 1, а затем обновит карту импорта для ссылки на этот новый URL-адрес файла.

Если затем мы перейдем в нашем браузере по адресу https://thawkin3-single-spa-demo.herokuapp.com/page1, мы увидим… барабанную дробь, пожалуйста… наше обновленное приложение!

Демонстрационное приложение - успешно обновляет одно из приложений микро-фронтенда
Демонстрационное приложение - успешно обновляет одно из приложений микро-фронтенда

Вывод

Я сказал это раньше и скажу еще раз: микро-фронтенд - это будущее веб-разработки.

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

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

Single-spa упрощает архитектуру микро-фронтенда. Теперь ты тоже можешь разрушить монолит!

Источник:

Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу