You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 00:13:10