Практика микроинтерфейса в корпоративных приложениях (часть 1)

внешний интерфейс
Практика микроинтерфейса в корпоративных приложениях (часть 1)
DevUI – это команда, которая занимается как проектированием, так и проектированием, обслуживая HUAWEI CLOUD.DevCloudПлатформа и несколько промежуточных и серверных систем в Huawei предназначены для дизайнеров и проектировщиков.
Официальный сайт:devui.design
Библиотека компонентов Ng:ng-devui(Добро пожаловать в Звезду)

введение

До сих пор помню Дэна Абрамова, автора редукса в 19 лет, про микро фронтендTwitterВ то время это вызвало широкие дебаты в области фронтенда, многие люди говорили, что микро-фронтенд был ложным предложением, но с наступлением 2020 года одна за другой появлялись различные статьи и фреймворки, связанные с микро-фронтендом, и продвигали эту идею. тема на передний план.Факты доказывают, что микро-интерфейс после модульности и компонентизации постепенно был принят в отрасли в качестве еще одной модели архитектуры внешнего интерфейса.В крупномасштабных сценариях разработки средних и фоновых корпоративных приложений ToB он будет играют все более важную роль.Поэтому сейчас самое время поговорить Давайте поговорим о микро интерфейсе. Эта статья разделена на верхнюю и нижнюю части.В верхней части в основном обсуждается происхождение и сценарии применения микро-фронтендов, DevUI исследует эволюцию микро-фронтендов и подробное изучение одиночного спа.Обсудим подробно, как разработайте решение микроинтерфейса корпоративного уровня Я надеюсь, что эта статья может быть использована в качестве важного справочника для исследователей микроинтерфейса, чтобы войти в яму.

источник

Концепция микро-интерфейса заключается в том, что с появлением внутренних микросервисов бизнес-команда делится на разные небольшие команды разработчиков, каждая из которых имеет фронт-энд, бэк-энд, тестирование и другие роли, службы могут звонить друг другу через http или rpc, интерфейс также может быть интегрирован и агрегирован через API-шлюз, далее следует надежда, что фронтенд-команда также сможет самостоятельно разрабатывать микро-приложения, а затем агрегировать эти микро-приложения. приложений на определенном этапе интерфейса (сборка, выполнение) для формирования законченного Большого веб-приложения. Так что эта концепция была предложена в обзоре Thoughtworks Technology Radar 2016 года.

                 

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

  • Независимая разработка, независимое развертывание, добавочное обновление: Согласно картинке выше, команды A, B и C предпочтительно не должны знать друг о друге, и каждое подприложение должно разрабатываться, развертываться и обновляться в соответствии с собственным ритмом версий.
  • Независимость от стека технологий: Команды A, B и C могут выбрать любую структуру для разработки в соответствии со своими потребностями, и их не нужно заставлять быть последовательными.
  • Изоляция и совместное использование во время выполнения: Во время выполнения приложения A, B и C образуют законченное приложение, доступ к которому можно получить через основную запись приложения.Необходимо убедиться, что js и css, соответствующие A, B и C, изолированы друг от друга и В то же время существует механизм связи, гарантирующий, что A, B, C могут общаться друг с другом или обмениваться данными.
  • Хороший опыт для одностраничных приложений: при переключении с одного подприложения на другое изменение маршрутизации не приведет к перезагрузке всей страницы, а эффект переключения аналогичен переключению внутрисайтовой маршрутизации одностраничного приложения.

веб-интеграция

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

  • Интеграция времени сборки: Интеграция в основной репозиторий приложения на этапе сборки с помощью подмодуля git или пакета npm. Команды A, B и C разрабатывают независимо. Преимущество заключается в простоте реализации и совместном использовании зависимостей. Недостатком является то, что A, B и C не могут быть обновлены независимо друг от друга.Один из Когда происходит обновление, необходимо собрать и развернуть основное приложение, чтобы обновить все приложение. следующим образом:

                     

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

  • Интеграция шаблонов на стороне сервера:Определите шаблон на домашней странице основного приложения и позвольте серверу динамически выбирать, какое подприложение для интеграции групп A, B и C с помощью технологии, аналогичной SSI nginx, следующим образом:

index.html

<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>Feed me</title>
  </head>
  <body>
    <h1>content here</h1>
    <!--# include file="$CONTENT.html" -->
  </body>
</html>

Соответствующая конфигурация nginx nginx.conf

server {
    root html;    #ssi配置开始
    ssi on;  
    ssi_silent_errors on;  
    ssi_types text/shtml;  
    #ssi配置结束         
    index index.html index.htm;    rewrite ^/$ http://localhost/appa redirect;

    location /appa {
      set $CONTENT 'appa';
    }
    location /appb {
      set $CONTENT 'appb';    }
    location /appc {
      set $CONTENT 'appc'    }
}

Конечным результатом работы команд A, B и C является файл шаблона, расположенный на сервере.Как и PHP, интеграция с JSP-сервером основана на том же принципе.На стороне сервера различные шаблоны выбираются путем маршрутизации, а содержимое домашней страницы собран и укомплектован. Во-первых, этот режим идет вразрез с общей тенденцией разделения фронтенда и бэкенда, что приведет к сопряжению, в то же время требует определенного объема работы по обслуживанию серверной части, не подходит для больших масштабные сценарии интеграции одностраничных приложений.

  • Интеграция с Iframe во время выполнения:Можно сказать, что этот метод должен быть самым простым и эффективным на ранней стадии.Различные команды могут разрабатывать и развертывать независимо друг от друга.Только одно основное приложение должно указывать на адреса, соответствующие приложениям A, B и C через iframe. Если требуется связь между основным приложением и вспомогательными приложениями, это также можно легко сделать с помощью почтового сообщения. ,следующим образом:
<html>
  <head>    
  <title>index.html</title>
  </head>
<body>   
 <iframe id="content"></iframe>    
 <script type="text/javascript">        
    const microFrontendsByRoute = {           
    '/appa': 'https://main.com/appa/index.html',            
    '/appb': 'https://main.com/appb/index.html',            
    '/appc': 'https://main.com/appc/index.html',        
  };        
    const iframe = document.getElementById('content');       
    iframe.src = microFrontendsByRoute[window.location.pathname];        
    window.addEventListener("message", receiveMessage, false);       
   function receiveMessage(event) {            
   var origin = event.origin           
   if (origin === "https://main.com") {                
   // do something       
    }             
  }        
 }    
  </script>
 </body>
</html>

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

  • Интеграция JS во время выполнения:У этого метода интеграции обычно есть два режима: первый — упаковать приложения A, B и C в разные пакеты, а затем загрузить разные пакеты через загрузчик, динамически запустить логику пакета и отобразить страницу следующим образом:

                

В настоящее время приложения A, B и C совершенно не знают друг о друге и могут быть разработаны с использованием любой среды. Переключение приложений, вызванное переключением маршрутизации, не приведет к перезагрузке страницы. Если приложения A, B и C хотят обмениваться данными во время выполнения , используйте CustomEvent или self. Можно определить EventBus. A, B и C также могут обеспечить изоляцию приложений с помощью механизма изоляции различных платформ или некоторых механизмов песочницы.Так хорошо выглядит.

Второй способ интеграции во время выполнения заключается в использовании веб-компонентов.Приложения A, B и C пишут свою собственную бизнес-логику в виде веб-компонента и упаковывают его в пакет, который затем загружается, выполняется и отображается основным приложением следующим образом. :

<html> 
 <body>    
<script src="https://main.com/appa/bundle.js"></script>    
<script src="https://main.com/appb/bundle.js"></script>    
<script src="https://main.com/appc/bundle.js"></script>    
<div id="content"></div>    
<script type="text/javascript">          
const routeTypeTags = {        
'/appa': 'app-a',        
'/appb': 'appb',        
'/appc': 'app-c',     
 };     
 const componentTag = routeTypeTags[window.location.pathname];    
 const content = document.getElementById('content');     
 const component = document.createElement(componentTag);      
 content.appendChild(component);   
 </script>  
 </body>
</html>

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

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

Эволюция режима интеграции интерфейса DevUI


Как показано на рисунке выше, отличительными чертами интерфейса devui являются:

1) сервисов много, у каждого сервиса свой репозиторий кода фронтенда, который нужно самостоятельно разработать, протестировать и развернуть;

2) Передняя часть каждой службы состоит из заголовка и области содержимого, которая представляет собой одностраничное приложение на основе Angular.У разных служб различается только область содержимого, а заголовки одинаковы;

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

Фаза 1: Общая компонентизация + гиперссылки между сервисами

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


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

Основной причиной этой проблемы в основном является:
1) Основываясь на чистом режиме рендеринга на стороне клиента Angular, перед рендерингом необходимо дождаться времени выполнения самого Angular и загрузки статических ресурсов самого компонента заголовка.Обычно процесс длится около 1 секунды.В течение этой секунды , на элементе page ничего нет, решение обычно SSR или пререндеринг;
2) Каждое имя субдомена службы отличается, предыдущий статус входа в систему (sessionId) сохраняется на основе каждого имени субдомена службы и не может быть передан совместно, что приводит к повторному входу в систему и проверке;

Этап 2: App Shell (предварительный рендеринг) + общий доступ к сеансу

В отрасли существуют стандартные решения проблемы рендеринга белого экрана для одностраничных приложений.Обычно используются SSR (рендеринг на стороне сервера) и предварительный рендеринг (Prerender).Разница между ними заключается в том, что будет выполняться SSR. на стороне сервера (обычно Node).Для некоторой логики HTML, соответствующий текущему маршруту, сначала генерируется, а затем возвращается в браузер, в то время как Prerender обычно генерирует соответствующий HTML-контент в соответствии с некоторыми правилами на этапе сборки и возвращает непосредственно в браузер, когда пользователь получает к нему доступ, следующим образом:


С точки зрения пользовательского опыта и эффектов, SSR, несомненно, лучший, но если весь сайт является SSR, стоимость очень велика (каждому сервису нужно добавить слой слоя рендеринга Node, а у SSR высокие требования к качеству кода, Angular's собственный SSR недостаточно развит), поэтому, взвесив друг друга, мы выбрали Prerender для решения проблемы белого экрана путем создания оболочки приложения на этапе сборки следующим образом:


На этом этапе мы разделили часть логики шапки и других сервисов на две части, одна часть — это левая часть шапки, которую можно увидеть интуитивно при обновлении страницы, эта часть вместе с некоторыми глобальными состояниями и встроенным -in event bus (с Это все сделано в пакете npm, который единообразно инжектится в index.html дельца при построении, а правая часть шапки по прежнему является Angular компонентом (выпадающие меню и прочее области, которые требуют, чтобы действия пользователя были видны, даже если отложенная визуализация не повлияет на опыт), бизнес должен быть представлен в собственном дереве компонентов. На этапе выполнения, когда пользователь обращается к index.html, часть оболочки Сначала визуализируется все приложение, затем загружаются статические ресурсы, соответствующие angular, а затем визуализируется правая сторона.Раскрывающееся меню заголовка и бизнес-контент.Заголовок связывается с бизнесом через шину событий.Когда контент, такой как раскрывающееся меню в правой части заголовка успешно отображается, мы добавляем это содержимое ко всей области заголовка.

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

Этап 3: виджет (микроприложение)

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


Мы надеемся, что бизнес выработает свою собственную логику, заголовок выработает публичную логику, не будет мешать друг другу и будет выпускать обновления самостоятельно, а затем во время выполнения бизнес будет ссылаться на заголовок через что-то похожее на header-loader (обратите внимание, что это ссылка во время выполнения) Таким образом, можно избежать пассивной работы по обновлению, вызванной обновлением общедоступной логики заголовка для бизнеса, и бизнес не знает о заголовке. Таким образом, основная проблема здесь заключается в том, как интегрируется заголовок.Согласно предыдущей главе, есть два способа: использование интеграции iframe и использование javascript для динамического рендеринга. iframe явно не подходит для такого сценария как по эффекту реализации, так и по сложности делового общения, поэтому здесь мы реализуем его по аналогии с веб-компонентами (в реальном процессе можно выбрать любой фреймворк, лишь бы он может удовлетворить Загрузить пакет, выполнить логику и отобразить такой шаблон)


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

Этап 4. Оркестрация и интеграция между приложениями

Представьте себе такой сценарий, для крупного предприятия внутри много мидл и бэкенд приложений, его можно представить как пул приложений (рынок приложений), для некоторых бизнесов надеюсь вывести C, D, E интегрирует их все, чтобы сформировать для пользователей крупномасштабный бизнес A. В то же время я надеюсь изъять D, E и F с рынка приложений и интегрировать их в крупномасштабный бизнес B, чтобы обеспечить единый вход для пользователи могут использовать.Среди них приложения A, B, C, D, E, F разрабатываются и поддерживаются разными командами. В этом случае должен быть механизм для определения правил, которым должно следовать стандартное подприложение, и того, как основное приложение интегрируется (загрузка, рендеринг, логика выполнения, изоляция, связь, маршрутизация ответов, совместное использование зависимостей, фреймворки). неважно и др.)


Такой механизм - это то, что нужно обсудить самому микро-фронтенду.На данном этапе как его реализовать зависит от сложности вашего бизнеса.Он может быть простым или сложным, или даже сервисным продуктом и предоставлять полный набор решения, которые помогут вам достичь таких целей (см.Система микро-фронтальной архитектуры), весь DevUI также находится в такой исследовательской стадии, и некоторые основные моменты будут описаны во второй половине этой статьи. В настоящее время, согласно таким требованиям, нам сначала нужно изучить, как реализовать микро-интерфейс в этом режиме приложения master-slave.

Использование одного СПА

Во всей отрасли существует множество решений для реализации микроинтерфейса, которые широко приняты всеми.single-spa, это самая ранняя реализация решения микро-фронтенда на основе режима master-slave, а так же она заимствована различными более поздними решениями (такими как qiankun, mooa и т.д.) Не будет преувеличением сказать, что если вы хотите чтобы изучить микро-фронтенды, вам нужно сначала глубоко погрузиться в single-spa и то, как он работает.

Классификация микрофронтендов:single-spa делит микрофронтенды на следующие три категории:

  • стандартное вспомогательное приложение single-spa: различные компоненты могут отображаться с помощью единого спа-центра, соответствующего разным маршрутам, обычно это полное подприложение;
  • single-spa parcels: Посылка обычно не связана с маршрутом, а просто областью на странице (аналогично упомянутому выше виджету)
  • utility module: Некоторые подмодули, разработанные независимо, не отображают страницу, а только выполняют некоторую общую логику.

Первые две категории находятся в центре нашего исследования.Здесь мы берем angular8 в качестве примера, чтобы показать использование single-spa и вышеупомянутых концепций.

Шаг 1 создайте подприложение:Сначала создайте корневой каталог

mkdir microFE && cd microFE

Затем используйте angular cli для создания двух проектов в этом каталоге следующим образом:

ng new my-app --routing --prefix my-app

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

ng add single-spa-angular

Операция здесь в основном делает следующие вещи:

1) Преобразуйте запись углового приложения из main.ts в main.single-spa.ts следующим образом:

import { enableProdMode, NgZone } from '@angular/core';

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { Router } from '@angular/router';
import { ɵAnimationEngine as AnimationEngine } from '@angular/animations/browser'; 
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import singleSpaAngular from 'single-spa-angular';
import { singleSpaPropsSubject } from './single-spa/single-spa-props';


if (environment.production) {
  enableProdMode();
}
const lifecycles = singleSpaAngular({
  bootstrapFunction: singleSpaProps => {
    singleSpaPropsSubject.next(singleSpaProps);
    return platformBrowserDynamic().bootstrapModule(AppModule);
  },
  template: '<my-app-root />',
  Router,
  NgZone: NgZone,
  AnimationEngine: AnimationEngine,
});

export const bootstrap = lifecycles.bootstrap;
export const mount = lifecycles.mount;
export const unmount = lifecycles.unmount;

Из этого видно, что стандартное подприложение single-spa должно предоставлять три операции жизненного цикла, а именно начальную загрузку, монтирование и размонтирование.

2) В каталоге src/single-spa создаются два файла, один — single-spa-props для передачи пользовательских свойств, а другой — assets-url.ts для динамического получения пути к статическому ресурсу текущего приложения.

3) В каталоге src создается пустой маршрут, чтобы одно приложение могло отображать пустой маршрут, когда маршрут не может быть найден при переходе между приложениями

app-routing.module.ts

const routes: Routes = [  { path: '**', component: EmptyRouteComponent }];

4) Добавлены две команды build:single-spa и serve:single-spa в package.json для создания субприложения single-spa и запуска субприложения single-spa соответственно.

5) Создал пользовательский файл конфигурации webpack в корневом каталоге, и ввел конфигурацию webpack single-spa-angular (содержимое которого мы разберем позже)

Затем вам нужно добавить базовый href/в app-routing.module.ts следующим образом, чтобы избежать конфликта между всем подприложением и всем маршрутом angular при переключении маршрута angular:

@NgModule({  
 imports: [RouterModule.forRoot(routes)],  
 exports: [RouterModule],  
 providers: [{ provide: APP_BASE_HREF, useValue: '/' }]})
 export class AppRoutingModule { }

В это время, если вы используете команду npm run serve:single-spa, субприложение single-spa будет запущено на соответствующем порту (здесь 4201) следующим образом:


На странице ничего не отображается, но соответствующий single-spa создается как пакет main.js и сопоставляется с портом 4201.

В то же время выполните описанные выше шаги, чтобы создать другое приложение my-app2 и сопоставить его пакет с портом 4202. На данный момент наша структура каталогов выглядит следующим образом:

  • my-app: одно спа-приложение 1
  • my-app2: одно вспомогательное спа-приложение 2
Шаг 2 Создайте основное приложение:

Мы создаем root-html в корневом каталоге проекта и генерируем файл package.json.

npm init -y && npm i serve -g

{  "name": "root-html",  
   "version": "1.0.0",  
   "description": "", 
    "main": "index.js",  
    "scripts": {    
      "start": "serve -s -l 4200"  
     }, 
   "keywords": [],  
   "author": "",  
    "license": "ISC"
}

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

Создайте index.html в этом каталоге со следующим содержимым:

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Security-Policy" content="default-src *  data: blob: 'unsafe-inline' 'unsafe-eval'; script-src * 'unsafe-inline' 'unsafe-eval'; connect-src * 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; frame-src *; style-src * data: blob: 'unsafe-inline'; font-src * data: blob: 'unsafe-inline';">
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Your application</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="importmap-type" content="systemjs-importmap">
    <script type="systemjs-importmap">
      {
        "imports": {
          "app1": "http://localhost:4201/main.js",
          "app2": "http://localhost:4202/main.js",
          "single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.5/system/single-spa.min.js"
        }
      }
    </script>
    <link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.5/system/single-spa.min.js" as="script" crossorigin="anonymous" />
    <script src='https://unpkg.com/core-js-bundle@3.1.4/minified.js'></script>
    <script src="https://unpkg.com/zone.js"></script>
    <script src="https://unpkg.com/import-map-overrides@1.6.0/dist/import-map-overrides.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/4.0.0/system.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/4.0.0/extras/amd.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/4.0.0/extras/named-exports.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/4.0.0/extras/named-register.min.js"></script>
  </head>
  <body>
    <script>
      System.import('single-spa').then(function (singleSpa) {
        singleSpa.registerApplication(
          'app1',
          function () {
            return System.import('app1');
          },
          function (location) {
            return location.pathname.startsWith('/app1');
          }
        );

        singleSpa.registerApplication(
          'app2',
          function () {
            return System.import('app2');
          },
          function (location) {
            return location.pathname.startsWith('/app2');
          }
        )
        
        singleSpa.start();
      })
    </script>
    <import-map-overrides-full></import-map-overrides-full>
  </body>
</html>
Когда страница обновляется, мы используем systemjs для загрузки single-spa в первую очередь.Когда файл загружается успешно, мы определяем входные файлы двух подприложений 1 и 2. Каждое подприложение должно предоставлять функцию активности для одиночного -spa для определения текущего маршрута.Какое под-приложение находится в активном состоянии, функция загрузки, при переключении на соответствующее под-приложение, какие статические ресурсы необходимо загрузить, соответствующие знания о systemjs и картах импорта можно посмотреть по себя, здесь это просто понимается как загрузчик пакетов. На самом деле, в простых случаях вы можете использовать динамическую вставку тегов скрипта для достижения того же эффекта. Затем используйте npm run start для запуска основного приложения на 4200, и результат через localhost:4200/app1 будет следующим:

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

Шаг 3. Создайте заявку на посылку:

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

Сначала мы создаем новый проект с помощью vue-cli в корневом каталоге:

vue create my-parcel

Затем добавить single-spa под проект (конкретная операция здесь подробно не описана, можно посмотреть, что сделано в документации)

vue add single-spa

Затем создайте и запустите приложение для посылок,npm run serve

В это время пакет подприложения, упакованный проектом vue, также будет запущен на порту localhost: 8080. Мы настраиваем его в index.html корневого приложения, чтобы systemjs мог его найти.


Затем мы загружаем и отображаем его в my-app2.

app.component.ts моего приложения2

import { Component,ViewChild, ElementRef, OnInit, AfterViewInit } from '@angular/core';
import { Parcel, mountRootParcel } from 'single-spa';
import { from } from 'rxjs';
@Component({  
selector: 'my-app2-root', 
templateUrl: './app.component.html',  
styleUrls: ['./app.component.css']})
export class AppComponent implements OnInit, AfterViewInit {
  title = 'my-app2';  
  @ViewChild('parcel', { static: true }) private parcel: ElementRef;  
  ngOnInit() {    
     from(window.System.import('parcel')).subscribe(app => { 
      mountRootParcel(app, { domElement :this.parcel.nativeElement});   
   })  
  } 
}

При инициализации мы получаем точку монтирования посылки на компоненте, загружаем пакет субприложения vue, а затем вызываем метод mountRootParcel, предоставленный single-spa, для монтирования субкомпонента (приложения).Второй параметр, переданный этим method — это элемент dom точки монтирования.Первый параметр — это подприложение посылки.Важным отличием между подприложением посылки и подприложением с одним спа является то, что приложение посылки может предоставлять необязательный метод обновления.

main.js проекта vue

import './set-public-path';
import Vue from 'vue';
import singleSpaVue from 'single-spa-vue';
import App from './App.vue';
Vue.config.productionTip = false;
const vueLifecycles = singleSpaVue({
 Vue,
 appOptions: {render: (h) => h(App),
},
});
export const bootstrap = vueLifecycles.bootstrap;
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;

Эффект следующий: когда мы переключаемся на подприложение App2, мы обнаруживаем, что наш компонент представления также отображается:


Принципиальный анализ Single-SPA

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


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

модуль инструментов разработки: После single-spa5 предоставляется devtools, который может напрямую просматривать состояние текущего суб-приложения через chrome и т. д., поэтому модуль devtools в основном оборачивает некоторые API, необходимые инструментам разработчика, и назначает их окну.__SINGLE_SPA_DEVTOOLS__. переменнаяposedMethods, для вызова devtools;

модуль утилит:Модуль utils реализует некоторые методы и функции в основном для совместимости с браузерами;

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

  • Для подприложений с одним спа: load->bootstrap->Mount->Unmount->Unload
  • Для подкомпонентов посылок (приложений): bootstrap->Mount->Unmount->Update

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

навигационный модуль:При переключении маршрута одностраничного приложения обычно запускаются два разных события, а именно: hashchange и popstate, которые одновременно соответствуют хэш-маршрутизации и маршрутизации истории.Single-spa отслеживает эти события глобально в модуле навигации. переключатели маршрута приложения (соответствующие маршруту), сначала введите index.html, выполнит захват текущего маршрута single-spa, вызовет функцию активности, настроенную, когда подприложение зарегистрировано в соответствии с текущим маршрутом, определите, какое под- приложение принадлежит, а затем вызовите функцию загрузки для загрузки подприложений, поток подприложений в соответствии с предыдущим жизненным циклом, размонтируйте и размонтируйте старое приложение, соответствующее текущему маршруту, и вызовите начальную загрузку, чтобы запустить новое приложение и смонтировать новое приложение. В то же время singles-spa также предоставляет API для ручного запуска переключения приложений. Механизм такой же, как у пассивного обновления маршрута. Кроме того, этот модуль также предоставляет в качестве записи метод перенаправления, который при переключении маршрута выполняет вышеуказанные операции по очереди.

jquery-support.js: поскольку jquery использует прокси-серверы событий, многие прокси-серверы событий привязаны к окну.Если в jquery зарегистрированы события hashchange и popstate, требуется специальная обработка.

start.js: ввести всю логику перенаправления в навигацию и явно запустить single-spa.

single-spa.js:В качестве точки входа single-spa API-интерфейсы, предоставляемые вышеуказанными модулями, объединяются и экспортируются для внешних вызовов.

Таким образом, с вышеуказанной точки зрения, независимо от того, какая структура используется для написания приложения, пока оно подключено к single-spa, оно должно реализовать три метода жизненного цикла bootstrap, mount и Unmout для вызова single-sap, как показано ниже:


В модуле App2 загружаются методы Bootstrap, Mount и Messount

Согласно вышеуказанному анализу, единый спа примерно, а процесс выглядит следующим образом:


Среди них три шага 5, 7 и 8 должны быть реализованы с помощью библиотеки, похожей на single-spa-angular из-за различий во фреймворке. реализовано.

Анализ Single-SPA-Angular

single-spa-angular разделен на четыре части, структура каталогов src выглядит следующим образом:


Каждая из этих частей соответствует тому, что мы делали в предыдущем разделе с ng add single-spa-angular:

каталог веб-пакетовСодержимое :index.ts выглядит следующим образом:

import * as webpackMerge from 'webpack-merge';
import * as path from 'path'

export default (config, options) => {
  const singleSpaConfig = {
    output: {
      library: 'app3',
      libraryTarget: 'umd',
    },
    externals: {
      'zone.js': 'Zone',
    },
    devServer: {
      historyApiFallback: false,
      contentBase: path.resolve(process.cwd(), 'src'),
      headers: {
          'Access-Control-Allow-Headers': '*',
      },
    },
    module: {
      rules: [
        {
          parser: {
            system: false
          }
        }
      ]
    }
  }
  // @ts-ignore
  const mergedConfig: any = webpackMerge.smart(config, singleSpaConfig)
  removePluginByName(mergedConfig.plugins, 'IndexHtmlWebpackPlugin');
  removeMiniCssExtract(mergedConfig);

  if (Array.isArray(mergedConfig.entry.styles)) {
    // We want the global styles to be part of the "main" entry. The order of strings in this array
    // matters -- only the last item in the array will have its exports become the exports for the entire
    // webpack bundle
    mergedConfig.entry.main = [...mergedConfig.entry.styles, ...mergedConfig.entry.main];
  }

  // Remove bundles
  delete mergedConfig.entry.polyfills;
  delete mergedConfig.entry.styles;
  delete mergedConfig.optimization.runtimeChunk;
  delete mergedConfig.optimization.splitChunks;

  return mergedConfig;
}
function removePluginByName(plugins, name) {
  const pluginIndex = plugins.findIndex(plugin => plugin.constructor.name === name);
  if (pluginIndex > -1) {
    plugins.splice(pluginIndex, 1);
  }
}
function removeMiniCssExtract(config) {
  removePluginByName(config.plugins, 'MiniCssExtractPlugin');
  config.module.rules.forEach(rule => {
    if (rule.use) {
      const cssMiniExtractIndex = rule.use.findIndex(use => typeof use === 'string' && use.includes('mini-css-extract-plugin'));
      if (cssMiniExtractIndex >= 0) {
        rule.use[cssMiniExtractIndex] = {loader: 'style-loader'}
      }
    }
  });
}

Мы представили эту конфигурацию через настраиваемый файл конфигурации webpack в предыдущем разделе и позволили angular-cli использовать эту конфигурацию для упаковки.Эта конфигурация упаковывает наш окончательный выходной пакет в формате umd и дает ему экспорт с именем app3 , extract зону и js, и расшарить их прямо в index.html, при этом, чтобы вебпак не перезаписывал системную глобальную переменную, выставляем system под парсером в false, а оставшаяся операция - удалить все записи включая глобальный css, сохраните только основную запись, чтобы гарантировать, что только один main.js будет упакован в угловое подприложение в конце.

каталог схемы:Если вы не знаете о схемах, вы можете временно подумать, что они могут расширить или переопределить команду добавления angular cli и выполнить некоторые пользовательские операции над командой добавления. Код ядра, выполненный в каталоге схем, не будет опубликован.На самом деле, в результате, когда вы вводите ng add single-spa-angular, он выполняет четыре действия:

1) Обновите package.json в корневом каталоге проекта и пропишите зависимости, относящиеся к single-spa-angular, такие как @angular-builders/custom-webpack, single-spa-angular и т. д.

2) Будут созданы четыре файла со встроенными шаблонами: main.single-spa.ts, single-spa-props.ts, assets-url.ts, extra-webpack.config.js;

3) Обновит angular.json для использования @angular-builders/custom-webpack:browser и @angular-builders/custom-webpack:dev-server builder

4) Обновите packages.json и добавьте две новые команды: build:single-spa и serve:single-spa, которые используются для сборки и запуска субприложения single-spa.

каталог застройщика:Что такое угловой построитель, здесь не представлено, вам нужно только понимать, что использование построителя может охватывать команды сборки и обслуживания расширенного углового cli.Операции двух команд build:single-spa и serve:single-spa в До angular8 это было реализовано с помощью билдера.После angular8 это было реализовано напрямую с помощью custom-webpack.Если вы используете angular8 и выше, эти коды не будут выполняться здесь.

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

/* eslint-disable @typescript-eslint/no-use-before-define */
import { AppProps, LifeCycles } from 'single-spa'

const defaultOpts = {
  // required opts
  NgZone: null,
  bootstrapFunction: null,
  template: null,
  // optional opts
  Router: undefined,
  domElementGetter: undefined, // only optional if you provide a domElementGetter as a custom prop
  AnimationEngine: undefined,
  updateFunction: () => Promise.resolve()
};

export default function singleSpaAngular(userOpts: SingleSpaAngularOpts): LifeCycles {
  if (typeof userOpts !== "object") {
    throw Error("single-spa-angular requires a configuration object");
  }

  const opts: SingleSpaAngularOpts = {
    ...defaultOpts,
    ...userOpts,
  };

  if (typeof opts.bootstrapFunction !== 'function') {
    throw Error("single-spa-angular must be passed an opts.bootstrapFunction")
  }

  if (typeof opts.template !== "string") {
    throw Error("single-spa-angular must be passed opts.template string");
  }

  if (!opts.NgZone) {
    throw Error(`single-spa-angular must be passed the NgZone opt`);
  }

  return {
    bootstrap: bootstrap.bind(null, opts),
    mount: mount.bind(null, opts),
    unmount: unmount.bind(null, opts),
    update: opts.updateFunction
  };
}

function bootstrap(opts, props) {
  return Promise.resolve().then(() => {
    // In order for multiple Angular apps to work concurrently on a page, they each need a unique identifier.
    opts.zoneIdentifier = `single-spa-angular:${props.name || props.appName}`;

    // This is a hack, since NgZone doesn't allow you to configure the property that identifies your zone.
    // See https://github.com/PlaceMe-SAS/single-spa-angular-cli/issues/33,
    // https://github.com/single-spa/single-spa-angular/issues/47,
    // https://github.com/angular/angular/blob/a14dc2d7a4821a19f20a9547053a5734798f541e/packages/core/src/zone/ng_zone.ts#L144,
    // and https://github.com/angular/angular/blob/a14dc2d7a4821a19f20a9547053a5734798f541e/packages/core/src/zone/ng_zone.ts#L257
    opts.NgZone.isInAngularZone = function() {
      // @ts-ignore
      return window.Zone.current._properties[opts.zoneIdentifier] === true;
    }

    opts.routingEventListener = function() {
      opts.bootstrappedNgZone.run(() => {
        // See https://github.com/single-spa/single-spa-angular/issues/86
        // Zone is unaware of the single-spa navigation change and so Angular change detection doesn't work
        // unless we tell Zone that something happened
      })
    }
  });
}

function mount(opts, props) {
  return Promise
    .resolve()
    .then(() => {
      const domElementGetter = chooseDomElementGetter(opts, props);
      if (!domElementGetter) {
        throw Error(`cannot mount angular application '${props.name || props.appName}' without a domElementGetter provided either as an opt or a prop`);
      }

      const containerEl = getContainerEl(domElementGetter);
      containerEl.innerHTML = opts.template;
    })
    .then(() => {
      const bootstrapPromise = opts.bootstrapFunction(props)
      if (!(bootstrapPromise instanceof Promise)) {
        throw Error(`single-spa-angular: the opts.bootstrapFunction must return a promise, but instead returned a '${typeof bootstrapPromise}' that is not a Promise`);
      }

      return bootstrapPromise.then(module => {
        if (!module || typeof module.destroy !== 'function') {
          throw Error(`single-spa-angular: the opts.bootstrapFunction returned a promise that did not resolve with a valid Angular module. Did you call platformBrowser().bootstrapModuleFactory() correctly?`)
        }
        opts.bootstrappedNgZone = module.injector.get(opts.NgZone)
        opts.bootstrappedNgZone._inner._properties[opts.zoneIdentifier] = true;
        window.addEventListener('single-spa:routing-event', opts.routingEventListener)

        opts.bootstrappedModule = module;
        return module;
      });
    });
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function unmount(opts, props) {
  return Promise.resolve().then(() => {
    if (opts.Router) {
      // Workaround for https://github.com/angular/angular/issues/19079
      const routerRef = opts.bootstrappedModule.injector.get(opts.Router);
      routerRef.dispose();
    }
    window.removeEventListener('single-spa:routing-event', opts.routingEventListener)
    opts.bootstrappedModule.destroy();
    if (opts.AnimationEngine) {
      const animationEngine = opts.bootstrappedModule.injector.get(opts.AnimationEngine);
      animationEngine._transitionEngine.flush();
    }
    delete opts.bootstrappedModule;
  });
}

Суть здесь заключается в реализации трех методов начальной загрузки, монтирования и отключения.Этап boostrap выполняется только после завершения загрузки подприложения, чтобы пометить угловое приложение с несколькими экземплярами и сообщить, что zonejs single-spa запускает переключение подприложения, и должен начать обнаружение изменений. На этапе монтирования метод angular platformBrowserDynamic().bootstrapModule(AppModule) вызывается для ручного запуска приложения angular, и экземпляр запущенного модуля сохраняется. На этапе unmout вызывается метод уничтожения запущенного экземпляра модуля, субприложение уничтожается, а для особых случаев выполняется некоторая обработка. Ключевым моментом здесь является крепление.

Суммировать:

В верхней части этой статьи мы описали происхождение микро-фронтендов и различные методы интеграции веб-приложений.Описав случай режима веб-интеграции DevUI, мы углубили наше понимание этой части контента, и использовали single-spa для реализации микро-интеграции.Проводятся интерфейсная модель и принципиальный анализ единого спа-центра.Во второй половине мы подробно обсудим процесс преобразования микро-интерфейса DevUI и опишем, как разработать микроинтерфейсное решение корпоративного уровня. Код https://github.com/myzhibie/microFE-single-spa.

Присоединяйтесь к нам

МыКоманда DevUIДобро пожаловать и присоединяйтесь к нам, чтобы создать элегантную и эффективную систему человеко-машинного проектирования/исследований и разработок. Электронная почта для набора: muyang2@huawei.com.

Текст/DevUI мыжибие

Рекомендуемые статьи в прошлом

«Гибкое проектирование, эффективное сотрудничество, подчеркивание ценности совместной работы дизайнерских устройств и облака»

Модульный механизм Quill, современного редактора форматированного текста