Более двух лет назад в моей дебютной статье на Medium я написал следующее:

«Мое желание с помощью [dotNetify] — достичь состояния, при котором будет поддерживаться микросервис с полным стеком на платформе .NET: возможность иметь сложное веб-приложение, состоящее из независимых внешних и внутренних модулей, которые можно разрабатывались и развертывались полностью автономными командами с минимальным временем простоя»

Я рад сообщить, что с выпуском dotNetify версии 3.5 это желание, наконец, было реализовано!

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

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

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

Что нового в версии 3.5

DotNetify — это бесплатное программное обеспечение с открытым исходным кодом, которое помогает создавать реактивные и работающие в реальном времени веб-приложения на сервере .NET Core. Этот уникальный подход MVVM помещает модели представлений на серверную часть и пишет их на C#, а также взаимодействует со слоем пользовательского интерфейса, написанным на React, Vue или Knockout, через веб-сокет с помощью технологии SignalR (но, как вы прочтете далее, это стало необязательным в этой последней версии.)

Мульти-концентраторы

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

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

Режим веб-API

Если вам действительно не нужны push-уведомления в реальном времени, теперь вы можете выбрать HTTP Web API, включив сервисы MVC в Startup.cs и установив флаг в параметрах подключения:

dotnetify.react.connect("MyViewModel", this, { webApi: true } });

Что делает этот флаг, так это заставляет ваш компонент на стороне клиента подключаться через реализацию прокси-концентратора, которая взаимодействует с сервером как обычные HTTP-запросы к встроенной конечной точке веб-API dotNetify. Эта конечная точка создаст экземпляр запрошенной модели представления, установит значения свойств, если их отправит клиент, вернет сериализованный JSON в тексте ответа и удалит экземпляр.

Переключение в режим веб-API не потребует слишком больших изменений в способе написания моделей представлений C#. Вы просто не можете использовать функции реального времени, такие как PushUpdates() или многоадресная рассылка, и вам нужно сохранять их без состояния; либо позволить клиентскому сценарию управлять состоянием, либо всегда сохранять состояние в резервном хранилище. Любое ПО промежуточного слоя, которое вы пишете, будет продолжать работать, за исключением того, что аргумент контекста концентратора, который используется для заполнения контекстом концентратора SignalR, вместо этого будет получать свои значения из контекста HTTP.

Локальный режим

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

window.HelloWorld = {
   onConnect() {
     return { Greetings: 'Hello World' };
   }
 };
class MyApp extends React.Component {
   constructor(props) {
     super(props);
     this.vm = dotnetify.react.connect('HelloWorld', this);
     this.state = { Greetings: '' };
   }
   /*...*/
}

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

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

Обзор микроинтерфейса

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

Если вы думаете, что это звучит сложнее, чем стоит, то в большинстве случаев вы будете правы! Просто с микросервисами это не для всех. Этот метод увеличивает сложность как разработки, так и развертывания, а также создает целый ряд новых проблем. Так почему же это вообще «вещь»?

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

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

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

На диаграмме ниже показана предлагаемая архитектура:

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

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

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

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

Пример реализации микроинтерфейса

Простой образец реализации, охватывающий прикладной уровень указанной архитектуры, представлен в DotNetify.React.Template nuget:

dotnet new -i dotnetify.react.template
dotnet new mfe -o MyApp
// To run the app, follow the instructions in README.md

Выполнение приведенных выше командных строк создаст решение, состоящее из нескольких проектов .NET Core, представляющих службы приложений и службу портала. Пользовательский интерфейс в основном разработан с помощью React, с добавлением одного проекта Vue, чтобы проиллюстрировать многоязычный характер этого решения. Nginx используется в качестве шлюза API, а IdentityServer4 — в качестве поставщика удостоверений.

Службы приложений

Каждая служба приложений представляет собой веб-приложение ASP.NET Core, настроенное для создания сценариев пользовательского интерфейса с помощью Webpack при компиляции приложения (сборка разработки при запуске dotnet, сборка рабочей сборки при публикации dotnet).

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

Ниже приведен пример реализации основного скрипта. Функция createWebComponent предоставляется dotNetify-Elements для включения компонента React в пользовательский элемент HTML:

import { createWebComponent } from 'dotnetify-elements/web-components/core';
import MyApp from './components/MyApp';
const elementName = 'app1-element';
createWebComponent(TodoList, elementName);
export default () => document.createElement(elementName);

Сервис портала

Служба портала также является веб-приложением ASP.NET Core в сочетании с Webpack. Сценарий пользовательского интерфейса содержит функцию с именем loader, которая будет выполнена, как только портал запустится. Этой функции предоставляется список, содержащий информацию обо всех службах приложения, включая их адреса. Для демонстрационных целей этот список жестко запрограммирован.

loader(
  [
    {
      id: 'app1',
      label: 'App 1',
      routePath: 'app1',
      baseUrl: '//localhost:8080/app1',
      moduleUrl: '/dist/app.js'
    },
    ...
  ],
  // External dependencies required by the apps.
  [ 'dotnetify', 'dotNetifyElements' ]
);

Функция loader пропингует все адреса службы приложений и, получив ответ, использует загрузчик модулей SystemJS для загрузки приложения и передаст функцию модуля, создающую корневой компонент приложения, объекту, который управляет этими компонентами и отображает их.

function importApp(app) {
  const appUrl = app.baseUrl + app.moduleUrl;
  return SystemJS.import(appUrl)
    .then(module => updatePortal({
        ...app, 
        rootComponent: module.default
    }));
}

Когда корневой компонент приложения смонтирован, он будет обмениваться данными со своим собственным узловым сервером. Поскольку адрес является относительным, портал должен перехватывать каждый запрос на подключение и перенаправлять его на правильный адрес. Портал может сделать это, внедрив dotnetify.connectHandler и используя специальный идентификатор, который каждое приложение должно предоставить, когда его компонент делает вызов connect для соответствующей пересылки запроса:

const apps = /* registered apps */;
dotnetify.connectHandler = vmConnectArgs => {
  const appId = vmConnectArgs.options.appId;
  const app = apps.find(x => x.id === appId);
  if (app) {
    app.hub = app.hub || dotnetify.createHub(app.baseUrl);
    return {
      ...vmConnectArgs,
      hub: app.hub,
      options: { 
         ...vmConnectArgs.options, 
         headers: { Authorization: 'Bearer ' + getAccessToken()}
      }
    };
  }
};

Наряду с изменением концентратора он также внедряет токен доступа в запрос connect. Серверная часть службы настроена на получение ключей подписи JWT от IdentityServer4 для аутентификации токена.

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

Общая библиотека пользовательского интерфейса

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

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

Резюме

DotNetify v3.5 вносит заметные улучшения в поддержку шаблона микроинтерфейса для веб-приложений, разработанных с помощью серверной части .NET Core, либо с помощью SignalR, либо со стандартным веб-API HTTP.

Подробную документацию можно найти на сайте dotNetify. Пожалуйста, направляйте дальнейшие вопросы или отзывы на форум проблем github. Поддержите этот проект своими звездами!