Русский
Русский
English
Статистика
Реклама

Blazor Server и WebAssembly одновременно в одном приложении


ASP.NET Core Blazor это разработанная Microsoft веб-платформа, предназначенная для запуска на стороне клиента в браузере на основе WebAssembly (Blazor WebAssembly) или на стороне сервера в ASP.NET Core (Blazor Server), но две эти модели нельзя использовать одновременно. Подробнее о моделях размещения написано в документации.


В статье я расскажу о том, как


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

TL;DR:


Gif с демонстрацией полученного результата


Пример доступен на github.


Введение: зачем это нужно


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


Преимущества Blazor Server:


  • Небольшой объём загружаемых данных (blazor.server.js без сжатия ~ 250 КБ).
  • Быстрая загрузка.
  • Отзывчивый UI.

Недостатки Blazor Server:


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

Преимущества Blazor WebAssembly


  • Нет всех недостатков Blazor Server, так как приложение работает в браузере автономно. Например, можно работать offline, или делать PWA.

Недостатки Blazor WebAssembly


  • Неприлично большой размер: 10 15 Мб.
  • Из-за такого размера от перехода по ссылке до появления интерфейса может пройти 15 20 секунд (для первого запуска), что в современном мире уже за гранью допустимого.

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


Чтобы объединить преимущества Server и WebAssembly, у меня появилась идея реализовать гибридный режим работы: приложение должно запускаться в режиме Server, а позже переходить в режим WebAssembly незаметно для пользователя, например, во время навигации между страницами.


Далее я расскажу как у меня получилось это реализовать.


Часть 1: запуск Server и WebAssembly одновременно


Начать нужно с размещения WebAssembly приложения в приложении ASP.NET Core и включения Prerendering.


В такой конфигурации запуск Blazor начинается в файле _Host.cshtml с добавления на страницу компонента, который создаст для нас DOM нашего приложения, и загрузки скрипта, делающего наше приложение интерактивным.


Для Server это выглядит так:


<component type="typeof(App)" render-mode="ServerPrerendered" /><script src="_framework/blazor.server.js"></script>

А для WebAssembly так:


<component type="typeof(App)" render-mode="WebAssemblyPrerendered" /><script src="_framework/blazor.webassembly.js"></script>

Поэтому нам ничего не мешает загрузить их одновременно:


<srvr-app>    <component type="typeof(App)" render-mode="ServerPrerendered" /></srvr-app><wasm-app style="display: none;">    <component type="typeof(App)" render-mode="WebAssembly"></wasm-app>

Работать это, естественно, не будет. Дело в том, что тег <component> превращается в такой html:


<!--Blazor:{ ... }> ... <-->

В процессе инициализации приложения, blazor ищет этот фрагмент, а затем заменяет его на DOM приложения. Если скрипты blazor.server.js и blazor.webassembly.js запустить одновременно, они оба будут конкурировать за первый компонент, игнорируя второй.


Этого легко избежать, если начинать запуск blazor.webassembly.js только после того, как blazor.server.js закончил работу, примерно так:


var loadWasmFunction = function () {    // Дождёмся момента, когда blazor.server.js закончит работу    if (srvrApp.innerHTML.indexOf('<!--Blazor:{') !== -1) {        setTimeout(loadWasmFunction, 100);        return;    }    // А потом, загрузим blazor.webassembly.js    loadScript('webassembly');};setTimeout(loadWasmFunction, 100);

Теперь оба приложения запускаются, но работают некорректно. Проблема в том, что оба приложения подписываются на события (click, submit, onpush и т.д.) у document и window. Из-за этого Server и WebAssembly пытаются обработать события друг друга.


Нам нужно, чтобы они прослушивали события только внутри своих узлов <srvr-app> и <wasm-app>. Для этого придётся нарушить js best practices и переопределить addEventListener для window и document:


var addServerEvent = function (type, listener, options) {    srvrApp.addEventListener(type, listener, options);}var addWasmEvent = function (type, listener, options) {    wasmApp.addEventListener(type, listener, options);}// Перед загрузкой blazor.server.jswindow.addEventListener = addServerEvent;document.addEventListener = addServerEvent;// ...// После загрузки blazor.server.js, // но перед загрузкой blazor.webassembly.jswindow.addEventListener = addWasmEvent;document.addEventListener = addWasmEvent;

Теперь оба приложения работают. Остаётся только дождаться загрузки WebAssembly и включить его, скрыв <srvr-app> и отобразив <wasm-app>:


// Можно разорвать соединение Blazor Server с серверомwindow.BlazorServer._internal.forceCloseConnection();// Переключим видимость приложенийwasmApp.style.display = "block";srvrApp.style.display = "none";// Или можно не скрывать Server, а просто удалить этот узел

Поместим эту логику в файл blazor.hybrid.js и подключим его к _Host.cshtml вместо первых двух скриптов. В этот же файл поместим функцию, которая будет переключать модель размещения по сигналу из приложения. Вызывать её мы будем из c# кода нашего приложения.


Со стороны c#-приложения создадим компонент RuntimeHeader.razor с примерно таким содержимым:


private string Runtime => RuntimeInformation.RuntimeIdentifier;protected override async Task OnAfterRenderAsync(bool firstRender){    if (!firstRender) return;    if (Runtime == "browser-wasm")    {        // Если код выполняется в wasm-runtime,        // значит WebAssembly - приложение загрузилось         await JSRuntime.InvokeVoidAsync("wasmReady");    }    // Переключимся на WebAssembly при ближайшей навигации    EventHandler<LocationChangedEventArgs> switchFunc = null;    switchFunc = async (_, e) =>    {        await JSRuntime.InvokeAsync<bool>("switchToWasm", e.Location);        NavManager.LocationChanged -= switchFunc;    };    NavManager.LocationChanged += switchFunc;}

Всё, гибридное приложение работает. Остаётся добавить немного удобств, например возможность задать тип запущенного приложения в appsettings.json


"HybridType": "HybridOnNavigation"

где HybridType это


public enum HybridType{    // Приложение работает только как Server    ServerSide,    // Приложение работает только как WebAssembly    WebAssembly,    // Переключение на WebAssembly вручную вызовом switchToWasm    HybridManual,    // Переключение на WebAssembly при навигации    HybridOnNavigation,    // Переключение на WebAssembly сразу, как только он загрузится    HybridOnReady}

Часть 2: Аутентификация


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


Раз серверная сторона и клиент расположены в одном приложении, нам хорошо подходит Cookie Authentication.


Настроим Startup.cs для использования Cookie Authentication как обычно.


Остаётся решить проблему: когда Blazor Server будет выполнять код клиентской части приложения, которая делает вызовы к API по HTTP, он станет использовать HttpClient внутри своего процесса (обращаться сам к себе). Значит, чтобы механизм авторизации работал, нужно добавлять cookies клиента в этот экземпляр HttpClient. Настроим Dependency Injection так, чтобы он создавал правильный HttpClient для Blazor Server:


// В ConfigureServices файла Startup.cs:services.AddTransient(sp =>{    var httpContextAccessor = sp.GetService<IHttpContextAccessor>();    var httpContext = httpContextAccessor.HttpContext;    // Получим Cookies пользователя    var cookies = httpContext.Request.Cookies;    var cookieContainer = new System.Net.CookieContainer();    // И поместим их в его экземпляр HttpClientHandler    foreach (var c in cookies)    {        cookieContainer.Add(            new System.Net.Cookie(c.Key, c.Value)             {                 Domain = httpContext.Request.Host.Host             });    }    return new HttpClientHandler { CookieContainer = cookieContainer };});services.AddTransient(sp =>{    var handler = sp.GetService<HttpClientHandler>();    return new HttpClient(handler);});

Теперь запросы к API, которые Blazor Server делает сам к себе внутри процесса, будут авторизованы.


Но в Blazor Server мы не сможем использовать HTTP-заголовок Set-Cookie, ведь он установит Cookie для нашего внутреннего HttpClient'а. Поэтому, для Blazor Server и для Blazor WebAssembly создадим разные реализации интерфейса IAuthService, чтобы заставить Blazor Server установить Cookie для браузера клиента.


public interface IAuthService{    Task<string> Login(LoginRequest loginRequest, string returnUrl);    Task<string> Logout();    Task<CurrentUser> CurrentUserInfo();}

Для WebAssembly WasmAuthService.cs и ServerAuthService.cs для Server.


Теперь мы имеем механизм аутентификации, работающий одновременно и с Blazor Server и с Blazor WebAssembly.


Часть 3: Синхронизация состояния Server и WebAssembly


Это сложная задача. Если ограничиться переключением Server на WebAssembly в момент навигации, можно её не решать.


Но мы не будем искать простых путей, и сделаем синхронизацию состояния компонента Counter.razor с помощью gRPC streaming.


Для этого создадим gRPC сервис


public interface ICounterService{    Task Increment();    Task Decrement();    IAsyncEnumerable<CounterState> SubscribeAsync();}

и реализуем его в CounterService.cs.


Смотрите как божественно статически типизовано наше приложение, на стороне клиента в Counter.razor мы создаём экземпляр ICounterService:


[Inject] ICounterService CounterService { get; set; }protected override void OnInitialized(){    var asyncState = CounterService.SubscribeAsync();}

А на стороне сервера мы видим место использования метода SubscribeAsync:



Возможно это благодаря библиотеке protobuf-net.Grpc, позволяющей использовать code-first подход в создании gRPC-сервисов, вместо ручного создания*.proto-файлов.


Сконфигурируем Dependency Injection для создания экземпляров gRPC сервисов:


services.AddTransient(sp =>{    // Interceptor удобен для централизованной обработки ошибок    var interceptor = sp.GetService<GrpcClientInterceptor>();    // Нужен для авторизации пользователей    var httpHandler = sp.GetService<HttpClientHandler>();    // Нужен, чтобы узнать URI нашего приложения     var httpClient = sp.GetService<HttpClient>();    var handler = new Grpc.Net.Client.Web.GrpcWebHandler(        Grpc.Net.Client.Web.GrpcWebMode.GrpcWeb,        httpHandler ?? new HttpClientHandler());    var channel = Grpc.Net.Client.GrpcChannel.ForAddress(        httpClient.BaseAddress,        new Grpc.Net.Client.GrpcChannelOptions()        {            HttpHandler = handler        });    // Просто для примера перехватим все вызовы    var invoker = channel.Intercept(interceptor);    // Создадим сервис с помощью protobuf-net.Grpc    return GrpcClientFactory.CreateGrpcService<T>(invoker);});

Теперь наш DI может создавать сервисы gRPC. Проверка авторизации в сервисах gRPC осуществляется с помощью аттрибута [Authorize], как и в обычном ASP.NET Core контроллере. В этом приложении, сервис WeatherForecastService как раз помечен этим атрибутом.


Результат


Как оказалось, сделать гибридное ASP.NET Core Blazor приложение не сложно. Приложение можно разместить в Kestrel, в IIS (IIS поддерживает только HTTPS) и в Docker (там используется Kestrel).


Получившийся проект доступен на github..


Также, я опубликовал образ для docker:


docker run --rm -p 80:80/tcp jdtcn/hybridblazorserver:latest

Можно запустить приложение в контейнере в любом из режимов работы:


docker run --rm -p 80:80/tcp jdtcn/hybridblazorserver:latest -e HybridType=HybridManual

В приложении для входа можно использовать логин и пароль demo.


Новый фреймворк Blazor даёт просто безграничные просторы для полёта фантазии c#-разработчикам.


Пробуйте, экспериментируйте!

Источник: habr.com
К списку статей
Опубликовано: 11.03.2021 04:12:23
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Net

C

Webassembly

Blazor server webassembly

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru