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后端
- 安装NuGet包:
Install-Package Microsoft.AspNetCore.SignalR - 创建Hub类:
public class ProgressHub : Hub { }
- 在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前端配置
- 安装SignalR包:
npm install @microsoft/signalr - 创建服务处理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'); } }); } }
- 在组件中使用:
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
相关产品推荐
相关产品推荐

