[.NET, C#, WebAssembly] Blazor: Server и WebAssembly одновременно в одном приложении
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
ASP.NET Core Blazor — это разработанная Microsoft веб-платформа, предназначенная для запуска на стороне клиента в браузере на основе WebAssembly (Blazor WebAssembly) или на стороне сервера в ASP.NET Core (Blazor Server), но две эти модели нельзя использовать одновременно. Подробнее о моделях размещения написано в документации.
В статье я расскажу о том, как
- запустить Server и WebAssembly одновременно в одном приложении,
- переключаться с Server на WebAssembly без перезагрузки приложения,
- реализовать универсальный механизм аутентификации,
- синхронизировать состояние Server и WebAssembly с помощью gRPC.
TL;DR:
Gif с демонстрацией полученного результата
SPL
Пример доступен на 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.js
window.addEventListener = addServerEvent;
document.addEventListener = addServerEvent;
// ...
// После загрузки blazor.server.js,
// но перед загрузкой blazor.webassembly.js
window.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
===========
Похожие новости:
- [.NET, C#] Реализация локализации при помощи Source code generators
- [.NET] A little life hack when you work with Azure Service Bus and ASP.NET Core
- [Высокая производительность, JavaScript, Программирование, WebAssembly] Разгоняем JS-парсер с помощью WebAssembly (часть 1: базовые возможности)
- [.NET, C#] SmartTraits или добавляем «множественное» наследование в C#
- [.NET, C#, ООП, Промышленное программирование] Lazy Properties Are Good. That Is How You Are to Use Them
- [.NET, C#] Реализуем кооперативную многозадачность на C#
- [C#, Учебный процесс в IT, Карьера в IT-индустрии] Как мы перестали проверять всё подряд одной задачей и ускорили проверку тестовых на стажировку
- [Управление проектами, Управление персоналом, Карьера в IT-индустрии, Социальные сети и сообщества] .NET разработчик, найдись! или история о строителях социальных сетей
- [Open source, Виртуализация, Разработка под Windows, Openshift] Windows-контейнеры на Red Hat OpenShift
- [.NET, Проектирование и рефакторинг, C#, Профессиональная литература] Книга «Внедрение зависимостей на платформе .NET. 2-е издание»
Теги для поиска: #_.net, #_c#, #_webassembly, #_blazor_server_webassembly, #_.net, #_c#, #_webassembly
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 08:56
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
ASP.NET Core Blazor — это разработанная Microsoft веб-платформа, предназначенная для запуска на стороне клиента в браузере на основе WebAssembly (Blazor WebAssembly) или на стороне сервера в ASP.NET Core (Blazor Server), но две эти модели нельзя использовать одновременно. Подробнее о моделях размещения написано в документации. В статье я расскажу о том, как
TL;DR: Gif с демонстрацией полученного результатаSPLПример доступен на github. Введение: зачем это нужно Обе модели размещения имеют свои преимущества и свои недостатки: Преимущества Blazor Server:
Недостатки Blazor Server:
Преимущества Blazor WebAssembly
Недостатки Blazor WebAssembly
Нужно отметить, что пререндеринг доступен для обеих моделей размещения, и здорово улучшает отзывчивость, мы будем его использовать. Но даже со включенным пререндерингом для 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.js window.addEventListener = addServerEvent; document.addEventListener = addServerEvent; // ... // После загрузки blazor.server.js, // но перед загрузкой blazor.webassembly.js window.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 =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 08:56
Часовой пояс: UTC + 5