ASP.NET Core Blazor下载控制器如何更新页面进度条?
ASP.NET Core Blazor 下载进度条实现方案
问题分析
之前使用NavManager.NavigateTo无法获取下载进度,而自定义HTTP头(X-Total-Size、X-Processed-Size)的方式不可行——因为HTTP响应头在响应启动时就已发送,服务器无法在文件流式传输过程中动态更新响应头,导致前端无法实时获取读取进度。
以下是两种可靠的实现方案:
方案一:SignalR 实时推送进度 + HttpClient 流式下载
SignalR 是 Blazor 生态中适配性极佳的实时通信框架,可直接在文件读取过程中向前端推送进度。
1. 搭建 SignalR Hub
public class DownloadProgressHub : Hub { }
2. 注册 SignalR 服务
在Program.cs中添加:
builder.Services.AddSignalR(); // 配置端点 app.MapHub<DownloadProgressHub>("/downloadProgressHub");
3. 下载控制器实现
注入IHubContext,读取文件时计算并推送进度:
[ApiController] [Route("api/[controller]")] public class DownloadController : ControllerBase { private readonly IHubContext<DownloadProgressHub> _hubContext; public DownloadController(IHubContext<DownloadProgressHub> hubContext) { _hubContext = hubContext; } [HttpGet("file/{fileName}")] public async Task<IActionResult> DownloadFile(string fileName) { var filePath = Path.Combine("你的文件存储路径", fileName); if (!System.IO.File.Exists(filePath)) return NotFound(); var fileInfo = new FileInfo(filePath); var totalSize = fileInfo.Length; long bytesRead = 0; // 获取前端传递的SignalR连接ID var connectionId = Request.Query["connectionId"].ToString(); if (string.IsNullOrEmpty(connectionId)) return BadRequest("缺少connectionId参数"); using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); using var reader = new BinaryReader(stream); var buffer = new byte[8192]; var memoryStream = new MemoryStream(); while (bytesRead < totalSize) { var read = await reader.ReadAsync(buffer, 0, buffer.Length); if (read == 0) break; bytesRead += read; await memoryStream.WriteAsync(buffer, 0, read); // 计算并推送进度 var progress = (int)((bytesRead / (double)totalSize) * 100); await _hubContext.Clients.Client(connectionId).SendAsync("ReceiveProgress", progress); // 控制推送频率,避免频繁更新 await Task.Delay(50); } memoryStream.Position = 0; return File(memoryStream, "application/octet-stream", fileName); } }
4. Blazor 组件实现
连接 SignalR 接收进度,同时发起下载请求:
@page "/download" @inject HttpClient Http @inject NavigationManager NavManager @inject IJSRuntime JSRuntime @using Microsoft.AspNetCore.SignalR @implements IAsyncDisposable <h3>文件下载进度</h3> <div class="progress" style="height: 20px;"> <div class="progress-bar" role="progressbar" style="width: @_progress%" aria-valuenow="@_progress" aria-valuemin="0" aria-valuemax="100">@_progress%</div> </div> <button class="btn btn-primary mt-3" @onclick="StartDownload">开始下载</button> @code { private HubConnection? _hubConnection; private int _progress; protected override async Task OnInitializedAsync() { _hubConnection = new HubConnectionBuilder() .WithUrl(NavManager.ToAbsoluteUri("/downloadProgressHub")) .Build(); // 监听进度推送 _hubConnection.On<int>("ReceiveProgress", progress => { _progress = progress; StateHasChanged(); }); await _hubConnection.StartAsync(); } private async Task StartDownload() { _progress = 0; var fileName = "testfile.zip"; // 替换为目标文件名 var connectionId = _hubConnection.ConnectionId; // 发起下载请求,携带连接ID var response = await Http.GetAsync($"api/download/file/{fileName}?connectionId={connectionId}", HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); // 通过JS互操作保存文件到客户端 using var stream = await response.Content.ReadAsStreamAsync(); await JSRuntime.InvokeVoidAsync("saveFile", fileName, stream); } public async ValueTask DisposeAsync() { if (_hubConnection != null) { await _hubConnection.DisposeAsync(); } } }
5. 客户端JS保存函数
在wwwroot/js/site.js中添加:
function saveFile(fileName, stream) { const blob = new Blob([stream]); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); }
方案二:服务器推送事件(SSE)+ 共享进度服务
SSE 是轻量级的单向推送方案,无需额外依赖,适合简单的进度推送场景。
1. 进度共享服务
用于在下载控制器和SSE控制器之间传递进度:
public interface IDownloadProgressService { void UpdateProgress(string downloadId, int progress); int GetProgress(string downloadId); } public class DownloadProgressService : IDownloadProgressService { private readonly Dictionary<string, int> _progressDict = new(); private readonly object _lock = new(); public void UpdateProgress(string downloadId, int progress) { lock (_lock) _progressDict[downloadId] = progress; } public int GetProgress(string downloadId) { lock (_lock) { _progressDict.TryGetValue(downloadId, out var progress); return progress; } } }
2. 注册进度服务
在Program.cs中添加:
builder.Services.AddSingleton<IDownloadProgressService>();
3. SSE 进度推送控制器
[ApiController] [Route("api/[controller]")] public class ProgressController : ControllerBase { private readonly IDownloadProgressService _progressService; public ProgressController(IDownloadProgressService progressService) { _progressService = progressService; } [HttpGet("stream/{downloadId}")] public async Task GetProgressStream(string downloadId) { Response.Headers.Add("Content-Type", "text/event-stream"); Response.Headers.Add("Cache-Control", "no-cache"); Response.Headers.Add("Connection", "keep-alive"); while (true) { var progress = _progressService.GetProgress(downloadId); await Response.WriteAsync($"data: {progress}\n\n"); await Response.Body.FlushAsync(); if (progress >= 100) break; await Task.Delay(100); } } }
4. 下载控制器实现
[ApiController] [Route("api/[controller]")] public class DownloadController : ControllerBase { private readonly IDownloadProgressService _progressService; public DownloadController(IDownloadProgressService progressService) { _progressService = progressService; } [HttpGet("file/{fileName}")] public async Task<IActionResult> DownloadFile(string fileName, string downloadId) { var filePath = Path.Combine("你的文件存储路径", fileName); if (!System.IO.File.Exists(filePath)) return NotFound(); var fileInfo = new FileInfo(filePath); var totalSize = fileInfo.Length; long bytesRead = 0; _progressService.UpdateProgress(downloadId, 0); using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); using var reader = new BinaryReader(stream); var buffer = new byte[8192]; var memoryStream = new MemoryStream(); while (bytesRead < totalSize) { var read = await reader.ReadAsync(buffer, 0, buffer.Length); if (read == 0) break; bytesRead += read; await memoryStream.WriteAsync(buffer, 0, read); var progress = (int)((bytesRead / (double)totalSize) * 100); _progressService.UpdateProgress(downloadId, progress); await Task.Delay(50); } _progressService.UpdateProgress(downloadId, 100); memoryStream.Position = 0; return File(memoryStream, "application/octet-stream", fileName); } }
5. Blazor 组件实现
@page "/download" @inject HttpClient Http @inject NavigationManager NavManager @inject IJSRuntime JSRuntime @implements IAsyncDisposable <h3>文件下载进度</h3> <div class="progress" style="height: 20px;"> <div class="progress-bar" role="progressbar" style="width: @_progress%" aria-valuenow="@_progress" aria-valuemin="0" aria-valuemax="100">@_progress%</div> </div> <button class="btn btn-primary mt-3" @onclick="StartDownload">开始下载</button> @code { private int _progress; private EventSource? _eventSource; private readonly string _downloadId = Guid.NewGuid().ToString(); private async Task StartDownload() { _progress = 0; var fileName = "testfile.zip"; // 启动SSE监听进度 _eventSource = new EventSource(NavManager.ToAbsoluteUri($"api/progress/stream/{_downloadId}").ToString()); _eventSource.Onmessage += (sender, e) => { if (int.TryParse(e.Data, out var progress)) { _progress = progress; StateHasChanged(); if (progress >= 100) _eventSource?.Close(); } }; // 发起下载请求 var response = await Http.GetAsync($"api/download/file/{fileName}?downloadId={_downloadId}", HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); // 保存文件到客户端 using var stream = await response.Content.ReadAsStreamAsync(); await JSRuntime.InvokeVoidAsync("saveFile", fileName, stream); } public ValueTask DisposeAsync() { _eventSource?.Close(); return ValueTask.CompletedTask; } }
方案对比
- SignalR:功能全面,支持双向通信,适合复杂交互场景,Blazor集成度高,但需要额外配置Hub。
- SSE:轻量无依赖,仅需HTTP协议,适合单向进度推送,兼容性覆盖现代浏览器(IE除外)。
内容的提问来源于stack exchange,提问作者Quinn Nash
相关产品推荐
相关产品推荐

