注释
此版本不是本文的最新版本。 要查看当前版本,请参阅本文的.NET 9 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 要查看当前版本,请参阅本文的.NET 9 版本。
本文介绍如何使用 ASP.NET Core 托管和部署服务器端 Blazor 应用(Blazor Web Apps 和 Blazor Server 应用)。
主机配置值
服务器端 Blazor 应用可以接受 通用主机配置值。
部署
使用服务器端托管模型, Blazor 从 ASP.NET Core 应用内在服务器上执行。 UI 更新、事件处理和 JavaScript 调用通过 SignalR 连接进行处理。
需要能够托管 ASP.NET Core 应用的 Web 服务器。 Visual Studio 包含服务器端应用项目模板。 有关 Blazor 项目模板的详细信息,请参阅 ASP.NET Core Blazor 项目结构。
在Release配置中发布应用并部署bin/Release/{TARGET FRAMEWORK}/publish文件夹的内容,其中{TARGET FRAMEWORK}占位符是目标框架。
可伸缩性
考虑单个服务器的可伸缩性时(纵向扩展),应用可用的内存可能是随着用户需求增加而耗尽的第一个资源。 服务器上的可用内存会影响:
- 服务器可支持的活动线路数。
- 客户端上的 UI 延迟。
有关构建安全且可缩放服务器端 Blazor 应用的指南,请参阅以下资源:
每个线路使用大约 250 KB 的内存,用于最小的 Hello World 风格应用。 线路的大小取决于应用的代码和与每个组件关联的状态维护要求。 我们建议你在开发应用和基础结构期间测量资源需求,但以下基线可能是规划部署目标的起点:如果希望应用支持 5,000 个并发用户,请考虑将至少 1.3 GB 的服务器内存预算用于应用(即每个用户大约 273 KB)。
Blazor WebAssembly 静态资源预加载
在ResourcePreloader组件的头内容(App)中,App.razor组件用于引用Blazor静态资产。 组件放置在基本 URL 标记之后(<base>):
<ResourcePreloader />
使用Razor组件,而不是使用<link>元素,因为选择Razor组件有以下原因:
- 该组件允许基础 URL(
<base>标签的href属性值)正确识别 ASP.NET Core 应用程序中的 Blazor 应用程序根目录。 - 可以通过从
ResourcePreloader组件中删除App组件标记来删除该功能。 这在应用使用loadBootResource回调修改 URL 的场景中尤其有用。
SignalR 配置
SignalR的托管和缩放条件 适用于 Blazor 使用 SignalR的应用。
有关SignalRBlazor应用中的详细信息(包括配置指南)请参阅 ASP.NET 核心BlazorSignalR指南。
运输工具
Blazor 使用 WebSocket 作为 SignalR 传输时,效果最佳,因为延迟较低、可靠性更好, 安全性更高。 当 WebSocket 不可用时,或在将应用显式配置为使用长轮询时, 将使用SignalR。
如果使用长轮询,则会出现控制台警告:
无法通过 WebSocket 连接,已切换到使用 Long Polling 作为备用传输方式。 这可能是由于 VPN 或代理阻止了连接。
全局部署和连接失败
全球部署到地理数据中心的建议:
- 将应用部署到大多数用户所在的区域。
- 考虑到跨大洲流量的延迟增加。 若要控制重新连接 UI 的外观,请参阅 ASP.NET 核心 BlazorSignalR 指南。
- 请考虑使用 Azure SignalR 服务。
Azure App 服务
在 Azure 应用服务上托管需要配置 WebSocket 和会话相关性,也称为应用程序请求路由(ARR)相关性。
注释
Blazor Azure 应用服务上的应用不需要 Azure SignalR 服务。
在 Azure 应用服务中为应用的注册启用以下内容:
- WebSocket,以允许 WebSockets 传输正常工作。 默认设置为“关”。
- 用于将请求从用户路由回同一应用程序服务实例的会话亲和性。 默认设置为“开”。
- 在 Azure 门户中,导航到“应用程序服务”中的 Web 应用。
- 打开设置>配置。
- 将“Web 套接字”设置为“开”。
- 验证“会话亲和性”是否设置为“开启”。
Azure SignalR 服务
可选的 Azure SignalR 服务可与应用的 SignalR 中心配合使用,以将服务器端应用纵向扩展到大量并发连接。 此外, 服务的全球覆盖和高性能数据中心可帮助显著减少由于地理位置造成的延迟。
Azure 应用服务或 Azure 容器应用中托管的 Blazor 应用不需要该服务,但在其他托管环境中可能会有所帮助:
- 为了便于连接横向扩展。
- 处理全局分发。
具有 SDK SignalR 或更高版本的 Azure 服务支持 SignalR 有状态重新连接 (WithStatefulReconnect)。
如果应用使用长轮询或回退到长轮询而不是 WebSocket,可能需要配置最大轮询间隔(MaxPollIntervalInSeconds默认值:5 秒,限制:1-300 秒),这定义了 Azure SignalR 服务中长轮询连接允许的最大轮询间隔。 如果下一个轮询请求未在最大轮询间隔内到达,服务将关闭客户端连接。
有关如何将服务作为依赖项添加到生产部署的指导,请参阅 将 ASP.NET Core SignalR 应用发布到 Azure 应用服务。
有关详细信息,请参见:
- Azure SignalR 服务
- 什么是 Azure SignalR 服务?
- ASP.NET Core SignalR 生产托管和缩放
- 将 ASP.NET Core SignalR 应用发布到 Azure 应用服务
Azure 容器应用
若要深入了解如何在 Azure 容器应用服务上缩放服务器端 Blazor 应用,请参阅 在 Azure 上缩放 ASP.NET 核心应用。 本教程介绍如何创建和集成在 Azure 容器应用上托管应用所需的服务。 本节中还提供了基本步骤。
必须将 ASP.NET Core Data Protection (DP) 服务配置为将所有容器实例都可访问的集中位置保存密钥。 密钥可以存储在 Azure Blob 存储中,并使用 Azure Key Vault 进行保护。 DP 服务使用密钥反序列化 Razor 组件。 若要将 DP 服务配置为使用 Azure Blob 存储和 Azure Key Vault,请引用以下 NuGet 包:
-
Azure.Identity:提供用于 Azure 标识和访问管理服务的类。 -
Microsoft.Extensions.Azure:提供有用的扩展方法来执行核心 Azure 配置。 -
Azure.Extensions.AspNetCore.DataProtection.Blobs:允许在 Azure Blob 存储中存储 ASP.NET 核心数据保护密钥,以便可以在 Web 应用的多个实例之间共享密钥。 -
Azure.Extensions.AspNetCore.DataProtection.Keys:使用 Azure Key Vault 密钥加密/包装功能启用静态密钥保护。
注释
有关将包添加到 .NET 应用的指南,请参阅包使用工作流(NuGet 文档)中“安装和管理包”下的文章。 在 NuGet.org 中确认正确的包版本。
-
使用以下突出显示的代码更新
Program.cs:using Azure.Identity; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Azure; var builder = WebApplication.CreateBuilder(args); var BlobStorageUri = builder.Configuration["AzureURIs:BlobStorage"]; var KeyVaultURI = builder.Configuration["AzureURIs:KeyVault"]; builder.Services.AddRazorPages(); builder.Services.AddHttpClient(); builder.Services.AddServerSideBlazor(); builder.Services.AddAzureClientsCore(); builder.Services.AddDataProtection() .PersistKeysToAzureBlobStorage(new Uri(BlobStorageUri), new DefaultAzureCredential()) .ProtectKeysWithAzureKeyVault(new Uri(KeyVaultURI), new DefaultAzureCredential()); var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapRazorPages(); app.Run();上述更改允许应用使用集中式可缩放体系结构管理 DP 服务。 DefaultAzureCredential 将代码部署到 Azure 后发现容器应用托管标识,并使用它连接到 Blob 存储和应用的密钥保管库。
若要创建容器应用托管标识并向其授予对 Blob 存储和密钥保管库的访问权限,请完成以下步骤:
- 在 Azure 门户中,导航到容器应用的概述页。
- 从左侧导航中选择 服务连接器 。
- 从顶部导航中选择 “+ 创建 ”。
- 在 “创建连接 ”浮出控件菜单中,输入以下值:
- 容器:选择您创建的容器应用来托管您的应用。
- 服务类型:选择 Blob 存储器。
- 订阅:选择拥有容器应用的订阅。
-
连接名称:输入名称
scalablerazorstorage。 - 客户端类型:选择 .NET ,然后选择“ 下一步”。
- 选择 系统分配的托管标识 ,然后选择“ 下一步”。
- 使用默认网络设置并选择“ 下一步”。
- Azure 验证设置后,选择“创建”。
对密钥保管库重复上述设置。 在 “基本信息 ”选项卡中选择相应的密钥保管库服务和密钥。
注释
前面的示例用于 DefaultAzureCredential 简化身份验证,同时开发部署到 Azure 的应用,方法是将 Azure 托管环境中使用的凭据与本地开发中使用的凭据组合在一起。 转移到生产环境时,可以选择更好的替代方案,例如 ManagedIdentityCredential。 有关详细信息,请参阅 使用系统分配的托管标识向 Azure 资源验证 Azure 托管的 .NET 应用。
IIS
使用 IIS 时,启用:
有关详细信息,请参阅将 ASP.NET Core 应用发布到 IIS 中的指南和外部 IIS 资源交叉链接。
Kubernetes
创建一个入口定义,使用以下 Kubernetes 注释来实现会话亲和性。
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: <ingress-name>
annotations:
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/session-cookie-name: "affinity"
nginx.ingress.kubernetes.io/session-cookie-expires: "14400"
nginx.ingress.kubernetes.io/session-cookie-max-age: "14400"
Linux 与 Nginx
按照 ASP.NET Core SignalR 应用的指南操作,并做出以下更改:
- 将
location路径从/hubroute(location /hubroute { ... }) 更改为根路径/(location / { ... })。 - 删除代理缓冲 (
proxy_buffering off;) 的配置,因为改设置仅适用于服务器发送事件 (SSE),这与 Blazor 应用客户端-服务器交互无关。
有关详细信息和配置指南,请参阅以下资源:
- ASP.NET Core SignalR 生产托管和缩放
- 使用 Nginx 在 Linux 上托管 ASP.NET Core
- 配置 ASP.NET Core 以使用代理服务器和负载均衡器
- 作为 WebSocket 代理的 NGINX
- WebSocket 代理
- 在非Microsoft支持论坛上咨询开发人员:
Linux 与 Apache
若要在 Linux 上托管在 Apache 之后的应用,配置 Blazor 以处理 HTTP 和 WebSockets 流量。
在下面的示例中:
- Kestrel 服务器在主机上运行。
- 应用侦听端口 5000 上的流量。
ProxyPreserveHost On
ProxyPassMatch ^/_blazor/(.*) http://localhost:5000/_blazor/$1
ProxyPass /_blazor ws://localhost:5000/_blazor
ProxyPass / http://localhost:5000/
ProxyPassReverse / http://localhost:5000/
启用以下模块:
a2enmod proxy
a2enmod proxy_wstunnel
检查浏览器控制台中是否存在 WebSocket 错误。 示例错误:
- Firefox 无法与服务器建立连接 ws://the-domain-name.tld/_blazor?id=XXX
- 错误:无法启动传输“WebSocket”:错误:传输出错。
- 错误:未能启动传输“LongPolling”:TypeError:未定义 this.transport
- 错误:无法使用任何可用传输连接到服务器。 WebSocket 失败
- 错误:如果连接未处于“已连接”状态,则无法发送数据。
有关详细信息和配置指南,请参阅以下资源:
- 配置 ASP.NET Core 以使用代理服务器和负载均衡器
- Apache 文档
- 在非Microsoft支持论坛上咨询开发人员:
测量网络延迟
JS 互操作性 可以用于测量网络延迟,下面的示例进行了演示。
MeasureLatency.razor:
@inject IJSRuntime JS
<h2>Measure Latency</h2>
@if (latency is null)
{
<span>Calculating...</span>
}
else
{
<span>@(latency.Value.TotalMilliseconds)ms</span>
}
@code {
private DateTime startTime;
private TimeSpan? latency;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
startTime = DateTime.UtcNow;
var _ = await JS.InvokeAsync<string>("toString");
latency = DateTime.UtcNow - startTime;
StateHasChanged();
}
}
}
@inject IJSRuntime JS
<h2>Measure Latency</h2>
@if (latency is null)
{
<span>Calculating...</span>
}
else
{
<span>@(latency.Value.TotalMilliseconds)ms</span>
}
@code {
private DateTime startTime;
private TimeSpan? latency;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
startTime = DateTime.UtcNow;
var _ = await JS.InvokeAsync<string>("toString");
latency = DateTime.UtcNow - startTime;
StateHasChanged();
}
}
}
@inject IJSRuntime JS
<h2>Measure Latency</h2>
@if (latency is null)
{
<span>Calculating...</span>
}
else
{
<span>@(latency.Value.TotalMilliseconds)ms</span>
}
@code {
private DateTime startTime;
private TimeSpan? latency;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
startTime = DateTime.UtcNow;
var _ = await JS.InvokeAsync<string>("toString");
latency = DateTime.UtcNow - startTime;
StateHasChanged();
}
}
}
@inject IJSRuntime JS
<h2>Measure Latency</h2>
@if (latency is null)
{
<span>Calculating...</span>
}
else
{
<span>@(latency.Value.TotalMilliseconds)ms</span>
}
@code {
private DateTime startTime;
private TimeSpan? latency;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
startTime = DateTime.UtcNow;
var _ = await JS.InvokeAsync<string>("toString");
latency = DateTime.UtcNow - startTime;
StateHasChanged();
}
}
}
@inject IJSRuntime JS
@if (latency is null)
{
<span>Calculating...</span>
}
else
{
<span>@(latency.Value.TotalMilliseconds)ms</span>
}
@code {
private DateTime startTime;
private TimeSpan? latency;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
startTime = DateTime.UtcNow;
var _ = await JS.InvokeAsync<string>("toString");
latency = DateTime.UtcNow - startTime;
StateHasChanged();
}
}
}
对于合理的 UI 体验,我们建议持续 UI 延迟为 250 毫秒或更少。