-
C#网络编程之WebAssembly网络编程(Wasm、Blazor)
第77章 WebAssembly网络编程(Wasm、Blazor)
一、我踩过的坑:Blazor Wasm中用HttpClient直接请求后端,浏览器报“跨域被拦截”
第一次做Blazor Wasm项目时,直接在组件里用HttpClient.GetAsync("https://api.mydomain.com/data"),浏览器控制台直接报“Access-Control-Allow-Origin”错误——查了半天才知道,Wasm运行在浏览器沙箱里,受同源策略限制,后端必须vb.net教程C#教程python教程SQL教程access 2010教程配置CORS(跨域资源共享)才能访问!还有一次想在Wasm里用Socket类直接连TCP服务器,结果编译报错,因为Wasm不支持直接访问系统级Socket API,只能通过浏览器提供的HTTP、WebSocket等API。这节我把这些真实坑点揉进去,用大白话讲透Wasm的网络限制,结合Blazor Wasm的代码逐行拆解,拓展生产级优化技巧,让你一次搞定WebAssembly网络编程!
二、WebAssembly核心概念大白话:运行在浏览器里的“.NET虚拟机”
-
什么是WebAssembly(Wasm)?
大白话:Wasm是一种二进制指令集,像一个轻量级的虚拟机,运行在浏览器里,和JS平级,但比JS快10-100倍(因为是编译型,JS是解释型);
.NET与Wasm:Blazor Wasm是把.NET代码编译成Wasm二进制文件,在浏览器里运行,用C#写前端代码,不用写JS;
沙箱限制:Wasm运行在浏览器沙箱里,不能直接访问系统资源(比如文件、Socket、CPU),只能通过浏览器提供的API(比如fetch、WebSocket)访问网络。 -
Blazor Wasm的网络底层原理
HttpClient:Blazor Wasm的HttpClient不是直接调用系统Socket,而是调用浏览器的fetch API,受同源策略限制;
WebSocket:Blazor Wasm的ClientWebSocket调用浏览器的WebSocket API,支持双向通信,不受同源策略限制(但后端要配置CORS);
JS互操作:如果浏览器API满足不了需求,比如要做P2P通信,可以通过JS互操作调用浏览器的WebRTC API。
三、Blazor Wasm HTTP编程:从跨域配置到缓存优化 -
我踩过的坑:直接用HttpClient请求后端,跨域被拦截
错误写法:不配置CORS,直接请求不同域名的后端
csharp
// Blazor组件中的错误代码
@inject HttpClient Http
@code {
private Listdata; protected override async Task OnInitializedAsync()
{
// 浏览器会报跨域错误,因为前端域名是localhost:5000,后端是localhost:5001
data = await Http.GetFromJsonAsync<List>("https://localhost:5001/api/data");
}
}
正确写法:后端配置CORS,前端用HttpClient请求 -
后端ASP.NET Core配置CORS
csharp
// 后端Program.cs
var builder = WebApplication.CreateBuilder(args);
// 添加CORS服务
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowBlazorWasm", policy =>
{
// 允许前端域名(比如localhost:5000)跨域请求
policy.WithOrigins("https://localhost:5000")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials(); // 允许携带Cookie
});
});
builder.Services.AddControllers();
var app = builder.Build();
// 启用CORS中间件,要在UseRouting之后,UseAuthorization之前
app.UseCors("AllowBlazorWasm");
app.UseAuthorization();
app.MapControllers();
app.Run("https://localhost:5001");
-
前端Blazor Wasm配置HttpClient
razor
<!-- Blazor组件Pages/Index.razor -->
@page "/"
@inject HttpClient Http
@inject NavigationManager NavManager
<h1>Blazor Wasm HTTP请求示例</h1>
@if (data == null)
{
<p>加载中...</p>
}
else
{
<ul>
@foreach (var item in data)
{
<li>@item.Name: @item.Value</li>
}
</ul>
}
@code {
private List<DataItem> data;
protected override async Task OnInitializedAsync()
{
try
{
// 1. 用相对路径请求,避免硬编码域名,方便部署
string apiUrl = NavManager.BaseUri + "api/data";
// 2. 用GetFromJsonAsync,自动反序列化JSON
data = await Http.GetFromJsonAsync<List<DataItem>>(apiUrl);
}
catch (HttpRequestException ex)
{
Console.WriteLine($"HTTP请求错误:{ex.Message}");
}
}
// 定义数据模型,和后端返回的JSON结构一致
private class DataItem
{
public string Name { get; set; }
public int Value { get; set; }
}
}
代码逐行拆解(结合浏览器同源策略)
-
同源策略大白话
同源:域名、协议、端口都相同,比如https://localhost:5000和https://localhost:5000是同源,https://localhost:5000和https://localhost:5001是不同源;
跨域:不同源的请求,浏览器会拦截,除非后端配置CORS允许前端域名访问;
生产级技巧:生产环境不要用AllowAnyOrigin,要指定具体的前端域名,避免安全风险。 -
Blazor Wasm HttpClient的底层实现
csharp
data = await Http.GetFromJsonAsync<List<DataItem>>(apiUrl);
底层原理:Blazor Wasm的HttpClient封装了浏览器的fetch API,不是直接调用系统Socket,所以受浏览器沙箱限制;
不支持的特性:HttpCompletionOption.ResponseHeadersRead(因为fetch API不支持流式读取响应头)、Timeout(浏览器fetch API不支持超时,Blazor Wasm的Timeout是模拟的,会取消请求但不会中断网络连接);
拓展知识:用IHttpClientFactory创建HttpClient,避免频繁创建和销毁HttpClient导致的Socket泄漏(虽然Blazor Wasm的HttpClient是基于fetch,但还是推荐用工厂模式)。
2. Blazor Wasm HTTP缓存优化:减少重复请求,提升性能
我踩过的坑:每次刷新页面都重新请求数据,加载慢
正确写法:用浏览器本地存储缓存数据
csharp
@inject HttpClient Http
@inject NavigationManager NavManager
@inject IJSRuntime JsRuntime
@code {
private List<DataItem> data;
protected override async Task OnInitializedAsync()
{
// 1. 先从本地存储读取缓存
string cachedData = await JsRuntime.InvokeAsync<string>("localStorage.getItem", "dataCache");
if (!string.IsNullOrEmpty(cachedData))
{
data = JsonSerializer.Deserialize<List<DataItem>>(cachedData);
Console.WriteLine("从本地存储加载缓存数据");
}
else
{
// 2. 缓存不存在,请求后端
string apiUrl = NavManager.BaseUri + "api/data";
data = await Http.GetFromJsonAsync<List<DataItem>>(apiUrl);
// 3. 把数据缓存到本地存储,过期时间1小时
await JsRuntime.InvokeVoidAsync("localStorage.setItem", "dataCache", JsonSerializer.Serialize(data));
await JsRuntime.InvokeVoidAsync("localStorage.setItem", "dataCacheExpire", DateTime.Now.AddHours(1).ToString());
Console.WriteLine("从后端加载数据并缓存");
}
}
}
代码逐行拆解(结合浏览器存储)
-
浏览器本地存储的类型
localStorage:永久存储,除非用户手动清除,适合缓存不经常变的数据;
sessionStorage:会话存储,关闭浏览器后清除,适合缓存会话数据;
IndexedDB:大容量存储,适合缓存大文件(比如图片、视频);
生产级技巧:缓存时要加过期时间,避免缓存数据过期,比如上面的代码加了1小时的过期时间,下次加载时先检查是否过期。
四、Blazor Wasm WebSocket编程:从双向通信到实时聊天 -
我踩过的坑:Blazor Wasm的ClientWebSocket不能直接连接ws://localhost:8080,跨域被拦截
正确写法:后端配置CORS,前端用ClientWebSocket连接 -
后端ASP.NET Core配置WebSocket和CORS
csharp
// 后端Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowBlazorWasm", policy =>
{
policy.WithOrigins("https://localhost:5000")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
var app = builder.Build();
app.UseCors("AllowBlazorWasm");
// 配置WebSocket中间件
app.UseWebSockets();
// 处理WebSocket连接
app.Map("/ws", async context =>
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
return;
}
using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
var buffer = new byte[1024 * 1024];
WebSocketReceiveResult result;
do
{
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
Console.WriteLine($"收到消息:{message}");
// 发送响应
string response = $"收到消息:{message},当前时间:{DateTime.Now}";
byte[] responseBytes = Encoding.UTF8.GetBytes(response);
await webSocket.SendAsync(new ArraySegment<byte>(responseBytes), result.MessageType, result.EndOfMessage, CancellationToken.None);
} while (!result.CloseStatus.HasValue);
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
});
app.Run("https://localhost:5001");
-
前端Blazor Wasm用ClientWebSocket连接
razor
<!-- Blazor组件Pages/Chat.razor -->
@page "/chat"
@inject IJSRuntime JsRuntime
@implements IAsyncDisposable
<h1>Blazor Wasm WebSocket聊天</h1>
<input type="text" @bind="message" placeholder="输入消息" />
<button @onclick="SendMessage">发送</button>
<div>
<h3>聊天记录</h3>
@foreach (var msg in messages)
{
<p>@msg</p>
}
</div>
@code {
private ClientWebSocket webSocket;
private string message;
private List<string> messages = new List<string>();
private CancellationTokenSource cts = new CancellationTokenSource();
protected override async Task OnInitializedAsync()
{
// 1. 创建ClientWebSocket
webSocket = new ClientWebSocket();
// 2. 配置WebSocket选项,比如设置子协议
webSocket.Options.AddSubProtocol("chat");
// 3. 连接后端WebSocket服务,注意用wss://(HTTPS对应的WebSocket协议)
string wsUrl = "wss://localhost:5001/ws";
try
{
await webSocket.ConnectAsync(new Uri(wsUrl), cts.Token);
messages.Add("连接WebSocket服务器成功");
// 4. 启动后台任务接收消息
_ = ReceiveMessagesAsync();
}
catch (Exception ex)
{
messages.Add($"连接失败:{ex.Message}");
}
}
private async Task ReceiveMessagesAsync()
{
var buffer = new byte[1024 * 1024];
while (webSocket.State == WebSocketState.Open)
{
try
{
// 5. 接收消息
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cts.Token);
if (result.MessageType == WebSocketMessageType.Close)
{
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "关闭连接", cts.Token);
messages.Add("服务器关闭连接");
break;
}
string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
messages.Add($"服务器:{message}");
// 6. 通知UI更新
StateHasChanged();
}
catch (Exception ex)
{
messages.Add($"接收消息错误:{ex.Message}");
break;
}
}
}
private async Task SendMessage()
{
if (string.IsNullOrEmpty(message) || webSocket.State != WebSocketState.Open)
{
return;
}
try
{
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
await webSocket.SendAsync(new ArraySegment<byte>(messageBytes), WebSocketMessageType.Text, true, cts.Token);
messages.Add($"我:{message}");
message = string.Empty;
}
catch (Exception ex)
{
messages.Add($"发送消息错误:{ex.Message}");
}
}
// 7. 释放资源,关闭WebSocket连接
public async ValueTask DisposeAsync()
{
cts.Cancel();
if (webSocket.State == WebSocketState.Open)
{
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "页面关闭", CancellationToken.None);
}
webSocket.Dispose();
cts.Dispose();
}
}
代码逐行拆解(结合浏览器WebSocket API)
-
WebSocket协议的类型
ws://:对应HTTP,不安全,适合开发环境;
wss://:对应HTTPS,安全,适合生产环境,浏览器会加密传输数据;
生产级技巧:生产环境必须用wss://,避免数据被窃听。 -
Blazor Wasm ClientWebSocket的限制
不能设置超时:浏览器的WebSocket API不支持超时,Blazor Wasm的ClientWebSocket也不支持;
不能直接访问底层Socket:ClientWebSocket是封装了浏览器的WebSocket API,不能设置Socket选项(比如NoDelay);
拓展知识:用JS互操作调用浏览器的WebSocket API,可以实现更多功能,比如设置心跳检测,避免连接被服务器断开。
五、Wasm与JS互操作的网络优化:从Web Worker到P2P通信 -
用Web Worker处理网络请求,避免阻塞UI
我踩过的坑:大文件上传时,UI卡顿,因为网络请求在主线程执行
正确写法:用JS Web Worker处理网络请求,Blazor Wasm通过JS互操作调用 -
创建JS Web Worker文件(wwwroot/js/uploadWorker.js)
javascript
// Web Worker文件,在后台线程执行网络请求
self.onmessage = async (e) => {
const file = e.data;
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('https://localhost:5001/api/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
// 把结果发送回主线程
self.postMessage({ success: true, data: result });
} catch (error) {
self.postMessage({ success: false, error: error.message });
}
};
2. Blazor Wasm组件调用Web Worker
razor
<!-- Blazor组件Pages/Upload.razor -->
@page "/upload"
@inject IJSRuntime JsRuntime
<h1>大文件上传示例</h1>
<input type="file" @onchange="HandleFileChange" />
<button @onclick="UploadFile" disabled="@(file == null || isUploading)">上传</button>
@if (isUploading)
{
<p>上传中...</p>
}
@if (uploadResult != null)
{
<p>@uploadResult</p>
}
@code {
private IJSObjectReference worker;
private IBrowserFile file;
private bool isUploading;
private string uploadResult;
protected override async Task OnInitializedAsync()
{
// 1. 创建Web Worker
worker = await JsRuntime.InvokeAsync<IJSObjectReference>(
"new Worker", "./js/uploadWorker.js");
// 2. 监听Web Worker的消息
await worker.InvokeVoidAsync("addEventListener", "message", DotNet.CreateJSCallback((object msg) =>
{
var result = msg as Dictionary<string, object>;
isUploading = false;
if ((bool)result["success"])
{
uploadResult = "上传成功!";
}
else
{
uploadResult = $"上传失败:{result["error"]}";
}
StateHasChanged();
}));
}
private void HandleFileChange(InputFileChangeEventArgs e)
{
file = e.File;
}
private async Task UploadFile()
{
if (file == null) return;
isUploading = true;
uploadResult = null;
// 3. 把文件发送给Web Worker
await worker.InvokeVoidAsync("postMessage", file);
}
// 4. 释放Web Worker
public async ValueTask DisposeAsync()
{
await worker.InvokeVoidAsync("terminate");
await worker.DisposeAsync();
}
}
代码逐行拆解(结合Web Worker原理)
-
Web Worker的作用
后台线程执行:Web Worker在后台线程执行,不会阻塞主线程(UI线程),适合处理大文件上传、下载等耗时操作;
沙箱限制:Web Worker不能直接访问DOM,只能通过消息和主线程通信;
生产级技巧:用Web Worker处理网络请求,提升UI流畅度,避免用户操作时卡顿。 -
用WebRTC实现P2P通信:浏览器之间直接传数据
大白话解释WebRTC:浏览器之间直接建立连接,不需要经过服务器(除了信令服务器),适合做视频通话、文件共享等场景;
Blazor Wasm通过JS互操作调用WebRTC API,实现P2P通信(代码示例略,核心是通过JS互操作调用浏览器的RTCPeerConnection API)。
六、生产级Blazor Wasm网络优化技巧 -
Wasm文件压缩:减少加载时间
用Blazor Wasm的压缩功能:在Program.cs中添加builder.WebHost.UseStaticWebAssets();,然后在csproj文件中设置true ;
用Gzip/Brotli压缩:在服务器上配置Gzip或Brotli压缩,比如Nginx配置gzip on;,可以把Wasm文件压缩50%以上;
生产级技巧:用dotnet publish -c Release发布,会自动压缩Wasm文件。 -
性能监控:用浏览器DevTools监控网络请求
Network面板:查看HTTP请求的时间、大小、缓存情况;
Performance面板:查看UI线程的阻塞情况,优化Web Worker的使用;
Blazor Wasm性能监控:用BlazorProfiler监控组件的渲染时间,优化UI性能。 -
安全优化:避免XSS攻击
输入验证:对用户输入的内容进行验证,避免注入恶意脚本;
CSP(内容安全策略):在index.html中添加CSP头,比如,限制只能加载同源的资源;
生产级技巧:用Microsoft.AspNetCore.Components.WebAssembly.Authentication包实现身份验证,避免手动处理JWT令牌导致的安全问题。
七、总结与选型建议 - Blazor Wasm网络编程选型表
| 场景 | 推荐技术 |
|---|---|
| 普通Web请求 | Blazor Wasm HttpClient + 后端CORS配置 |
| 实时通信(聊天、通知) | Blazor Wasm ClientWebSocket + 后端WebSocket服务 |
| 大文件上传/下载 | JS Web Worker + Blazor Wasm JS互操作 |
| P2P通信(视频通话、文件共享) | WebRTC + Blazor Wasm JS互操作 |
-
Blazor Wasm网络编程流程
1.前端开发:用Blazor Wasm的HttpClient或ClientWebSocket实现网络请求,注意跨域配置;
2.后端配置:配置CORS、WebSocket服务,支持前端的网络请求;
3.性能优化:用Web Worker处理耗时操作,压缩Wasm文件,添加缓存策略;
4.安全优化:配置CSP、输入验证,实现身份验证;
5.测试部署:在浏览器中测试,部署到CDN或云服务器,提升加载速度。
现在你已经掌握了Blazor Wasm网络编程的核心用法,从HTTP请求到WebSocket通信,从JS互操作到Web Worker优化,以后WebAssembly网络开发不用慌,按照这个流程来,90%的问题都能提前避免!下一节我们会学习“Wasm与.NET互操作的高级技巧”,结合Wasm的内存模型,教你实现更高效的网络通信!
转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49598.html










