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

Angular向C#异步方法上传Excel并获取进度失败的解决方法

修复Angular上传Excel到C#的实时进度反馈问题

嘿,我明白你遇到的问题了——普通的ASP.NET Core Action是一次性返回完整响应的,没法在处理Excel的过程中逐步把进度推给Angular。这就是为什么你的进度只停在第一次返回的原因。下面给你两种靠谱的修复方案,都是生产环境常用的:

方案一:用Server-Sent Events(SSE)实现流式进度推送

这是最轻量的方案,不需要额外依赖,直接利用HTTP响应流分块发送数据。

C#后端修改

把你的UploadFiles方法改成直接操作响应流,用SSE格式发送进度:

[HttpPost("[action]")]
public async Task UploadFiles(IFormFile file)
{
    // 配置响应头,确保实时推送不缓存
    Response.Headers.Add("Cache-Control", "no-cache");
    Response.Headers.Add("Content-Type", "text/event-stream");
    Response.Headers.Add("Connection", "keep-alive");

    try
    {
        using var stream = file.OpenReadStream();
        // 这里用EPPlus读取Excel,你可以替换成自己的读取逻辑
        using var package = new ExcelPackage(stream);
        var worksheet = package.Workbook.Worksheets.First();
        var totalRows = worksheet.Dimension.Rows;
        var processedRows = 0;

        // 逐行处理并推送进度
        for (int row = 1; row <= totalRows; row++)
        {
            // 模拟你的业务处理逻辑,替换成实际代码
            await Task.Delay(30); // 模拟耗时操作
            processedRows++;
            int progress = (int)((double)processedRows / totalRows * 100);

            // 按SSE格式发送进度数据
            var progressData = new { 
                Progress = progress, 
                Message = $"已处理 {processedRows}/{totalRows} 行",
                IsCompleted = false
            };
            string json = System.Text.Json.JsonSerializer.Serialize(progressData);
            await Response.WriteAsync($"data: {json}\n\n");
            await Response.Body.FlushAsync(); // 强制立即发送到客户端
        }

        // 发送完成信号
        var completionData = new { 
            Progress = 100, 
            Message = "处理完成!", 
            IsCompleted = true 
        };
        string completionJson = System.Text.Json.JsonSerializer.Serialize(completionData);
        await Response.WriteAsync($"data: {completionJson}\n\n");
        await Response.Body.FlushAsync();
    }
    catch (Exception ex)
    {
        // 发送错误信息
        var errorData = new { 
            Progress = 0, 
            Message = $"处理失败:{ex.Message}", 
            IsError = true 
        };
        string errorJson = System.Text.Json.JsonSerializer.Serialize(errorData);
        await Response.WriteAsync($"data: {errorJson}\n\n");
        await Response.Body.FlushAsync();
    }
}

Angular前端修改

因为HttpClient.post会等待完整响应,所以我们用XMLHttpRequest来流式接收SSE数据:

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class FileUploadService {
  uploadWithProgress(file: File, updateProgress: (progress: any) => void) {
    const formData = new FormData();
    formData.append('file', file);

    const xhr = new XMLHttpRequest();
    xhr.open('POST', '/api/UploadFiles', true);
    xhr.setRequestHeader('Accept', 'text/event-stream');

    // 监听上传进度(文件发送到服务器的进度)
    xhr.upload.onprogress = (event) => {
      if (event.lengthComputable) {
        const uploadProgress = Math.round((event.loaded / event.total) * 100);
        updateProgress({ type: 'upload', progress: uploadProgress });
      }
    };

    // 监听服务器返回的处理进度
    xhr.onreadystatechange = () => {
      if (xhr.readyState === XMLHttpRequest.LOADING) {
        const responseText = xhr.responseText;
        // 分割SSE消息(每个消息以\n\n结尾)
        const messages = responseText.split('\n\n').filter(msg => msg.trim());
        messages.forEach(msg => {
          if (msg.startsWith('data: ')) {
            const dataStr = msg.slice(6);
            try {
              const progress = JSON.parse(dataStr);
              updateProgress({ type: 'process', ...progress });
            } catch (e) {
              console.error('解析进度数据失败:', e);
            }
          }
        });
      }
    };

    xhr.send(formData);
  }
}

在组件里使用这个服务:

// 组件内
uploadFile(event: any) {
  const file = event.target.files[0];
  this.fileUploadService.uploadWithProgress(file, (progress) => {
    if (progress.type === 'upload') {
      console.log(`上传进度:${progress.progress}%`);
    } else {
      console.log(`处理进度:${progress.progress}% - ${progress.Message}`);
      if (progress.IsCompleted) {
        alert('文件处理完成!');
      }
      if (progress.IsError) {
        alert(`出错了:${progress.Message}`);
      }
    }
  });
}

方案二:用SignalR实现双向实时进度推送

如果你的项目需要更复杂的实时通信场景(比如双向交互),SignalR是ASP.NET官方推荐的方案,稳定性更好。

步骤1:配置SignalR后端

  1. 安装NuGet包:Install-Package Microsoft.AspNetCore.SignalR
  2. 创建Hub类:
public class ProgressHub : Hub
{
}
  1. 在Program.cs中注册SignalR:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();
// ...其他服务注册

var app = builder.Build();
app.MapHub<ProgressHub>("/progressHub");
// ...其他中间件配置

步骤2:修改UploadFiles方法

[HttpPost("[action]")]
public async Task<IActionResult> UploadFiles(IFormFile file, [FromServices] IHubContext<ProgressHub> hubContext)
{
    // 获取客户端的SignalR连接ID
    var connectionId = HttpContext.Request.Query["connectionId"];
    if (string.IsNullOrEmpty(connectionId))
    {
        return BadRequest("缺少SignalR连接ID");
    }

    try
    {
        using var stream = file.OpenReadStream();
        using var package = new ExcelPackage(stream);
        var worksheet = package.Workbook.Worksheets.First();
        var totalRows = worksheet.Dimension.Rows;
        var processedRows = 0;

        for (int row = 1; row <= totalRows; row++)
        {
            await Task.Delay(30);
            processedRows++;
            int progress = (int)((double)processedRows / totalRows * 100);

            // 通过SignalR推送给指定客户端
            await hubContext.Clients.Client(connectionId).SendAsync("ReceiveProgress", new {
                Progress = progress,
                Message = $"已处理 {processedRows}/{totalRows} 行",
                IsCompleted = false
            });
        }

        // 发送完成信号
        await hubContext.Clients.Client(connectionId).SendAsync("ReceiveProgress", new {
            Progress = 100,
            Message = "处理完成!",
            IsCompleted = true
        });

        return Ok(new { Message = "上传请求已提交,处理中" });
    }
    catch (Exception ex)
    {
        await hubContext.Clients.Client(connectionId).SendAsync("ReceiveProgress", new {
            Progress = 0,
            Message = $"处理失败:{ex.Message}",
            IsError = true
        });
        return BadRequest(ex.Message);
    }
}

步骤3:Angular前端配置

  1. 安装SignalR包:npm install @microsoft/signalr
  2. 创建服务处理SignalR连接:
import { Injectable } from '@angular/core';
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
import { HttpClient } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class SignalRUploadService {
  private hubConnection!: HubConnection;

  constructor(private http: HttpClient) {
    this.initHubConnection();
  }

  private async initHubConnection() {
    this.hubConnection = new HubConnectionBuilder()
      .withUrl('/progressHub')
      .build();

    try {
      await this.hubConnection.start();
      console.log('SignalR连接成功');
    } catch (err) {
      console.error('SignalR连接失败:', err);
      setTimeout(() => this.initHubConnection(), 5000); // 重试连接
    }
  }

  uploadFile(file: File, onProgress: (progress: any) => void) {
    if (!this.hubConnection.connectionId) {
      alert('SignalR未连接,请稍后重试');
      return;
    }

    const formData = new FormData();
    formData.append('file', file);

    // 发送上传请求,携带SignalR连接ID
    this.http.post('/api/UploadFiles', formData, {
      params: { connectionId: this.hubConnection.connectionId }
    }).subscribe({
      next: () => console.log('上传请求已发送'),
      error: (err) => console.error('上传请求失败:', err)
    });

    // 监听进度推送
    this.hubConnection.on('ReceiveProgress', (progress) => {
      onProgress(progress);
      if (progress.IsCompleted || progress.IsError) {
        // 完成或错误后取消监听
        this.hubConnection.off('ReceiveProgress');
      }
    });
  }
}
  1. 在组件中使用:
uploadFile(event: any) {
  const file = event.target.files[0];
  this.signalRUploadService.uploadFile(file, (progress) => {
    console.log(`处理进度:${progress.Progress}% - ${progress.Message}`);
    if (progress.IsCompleted) {
      alert('文件处理完成!');
    }
    if (progress.IsError) {
      alert(`出错了:${progress.Message}`);
    }
  });
}

为什么原有代码失效?

你的原有代码返回Task<IActionResult>,这意味着ASP.NET Core会把所有处理逻辑执行完,才把完整的响应发送给Angular。中间的进度更新根本没法被Angular接收到,因为响应还没完成。而上面的两种方案都是让服务器在处理过程中逐步发送数据,Angular可以实时接收并更新UI。

内容的提问来源于stack exchange,提问作者Sa'ed Z.Sukkar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.21 04:28:07