ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Blazor Server 中使用 MiniExcel 实现 Excel 导出与导入 — 实战教程

Blazor Server 中使用 MiniExcel 实现 Excel 导出与导入 — 实战教程 目录一、为什么选择 MiniExcel二、项目架构三、Excel 导出实现3.1 Service 层构建数据并生成 Excel3.2 Blazor 页面触发下载3.3 JS 辅助函数Base64 → 文件下载四、Excel 导入实现4.1 定义导入 DTO4.2 Service 层解析并入库4.3 Blazor 页面文件上传五、进阶技巧5.1 大数据量导出避免内存溢出5.2 自定义表头样式5.3 多 Sheet 导出5.4 导出时保持与表格一致的列顺序六、常见问题与解决方案Q1: 导出的 Excel 打开时报文件格式损坏Q2: 中文列名乱码Q3: 导入时日期格式解析失败Q4: 大文件导入超时Q5: 导出文件过大导致 SignalR 断开七、总结一、为什么选择 MiniExcel在 .NET 生态中处理 Excel 的主流库有库特点适用场景EPPlus功能强大支持图表/公式复杂报表需商业授权v5NPOI老牌库兼容 .xls/.xlsx需要兼容旧格式ClosedXMLAPI 友好基于 OpenXML中等复杂度报表MiniExcel轻量、高性能、零依赖大数据量导出/导入选择MiniExcel的理由零外部依赖— 不需要安装 Office不需要 COM 组件极低内存占用— 基于流式读写100 万行数据仅需几十 MB 内存API 极简— 导出只需一行SaveAs导入只需一行QueryTMIT 开源协议— 商业项目无顾虑PackageReference IncludeMiniExcel Version1.42.0 /二、项目架构采用经典的三层架构BlazorApp.Server ← Blazor Server 前端Razor 页面 Radzen 组件 ├── Pages/ │ └── DataList.razor ← 数据列表页面含导出/导入按钮 └── wwwroot/ └── _Layout.cshtml ← 包含 saveAsFile JS 函数 BlazorApp.Application ← 业务逻辑层Service └── Services/ └── DataService.cs ← 数据导出/导入逻辑 BlazorApp.Core ← 实体模型 接口定义 BlazorApp.Infrastructure ← EF Core 数据访问层核心设计原则Service 层返回byte[]Blazor 页面负责触发浏览器下载。三、Excel 导出实现3.1 Service 层构建数据并生成 Excelpublic async Taskbyte[] ExportDataAsync( string? keyword null, int? categoryId null, string? status null, DateTime? startDate null, DateTime? endDate null) { // 1. 根据筛选条件查询数据 var items await GetDataAsync(keyword, categoryId, status, startDate, endDate); // 2. 投影为匿名类型属性名即为 Excel 列名 var exportData items.Select(x new { 编号 x.Code ?? , 名称 x.Name, 简称 x.ShortName ?? , 联系人 x.Contact ?? , 电话 x.Phone ?? , 类型 x.Type switch { 1 类型A, 2 类型B, 3 类型C, _ }, 等级 x.Level switch { 1 A, 2 B, 3 C, 4 D, _ }, 状态 x.IsLocked true ? 锁定 : 正常, 金额 x.Amount, 创建时间 x.CreatedTime.ToString(yyyy-MM-dd HH:mm) }).ToList(); // 3. 使用 MiniExcel 生成 Excel 字节数组 using var memoryStream new MemoryStream(); MiniExcel.SaveAs(memoryStream, exportData); return memoryStream.ToArray(); }关键点匿名类型属性名 Excel 列名MiniExcel 会自动将匿名类型的属性名作为表头支持中文属性名无需额外配置switch表达式做枚举映射将数字类型如Type1转换为可读文本如类型A避免导出后用户看不懂空值保护所有可空字段使用?? 兜底避免 Excel 中出现空白单元格3.2 Blazor 页面触发下载page /DataList inject IDataService DataService inject IJSRuntime JSRuntime inject NotificationService NotificationService DataGridToolbar RadzenButton Text导出 Icondownload ButtonStyleButtonStyle.Info ClickExportData / /DataGridToolbar code { private string? searchKeyword; private int? searchCategoryId; private string? searchStatus; private async Task ExportData() { try { // 调用 Service 获取 Excel 字节数组 var bytes await DataService.ExportDataAsync( searchKeyword, searchCategoryId, searchStatus); // 生成带时间戳的文件名 var fileName $数据列表_{DateTime.Now:yyyyMMddHHmmss}.xlsx; // 通过 JS 触发浏览器下载 await JSRuntime.InvokeVoidAsync(saveAsFile, fileName, Convert.ToBase64String(bytes)); NotificationService.Notify(NotificationSeverity.Success, 成功, 导出完成); } catch (Exception ex) { NotificationService.Notify(NotificationSeverity.Error, 导出失败, ex.Message); } } }3.3 JS 辅助函数Base64 → 文件下载在_Layout.cshtml中定义全局函数script window.saveAsFile function (fileName, base64Content) { const link document.createElement(a); link.download fileName; link.href data:application/octet-stream;base64, base64Content; link.click(); }; /script为什么用 Base64 传输Blazor Server 通过 SignalR 连接通信二进制数据需要编码传输。Base64 是最简单可靠的方式。对于大文件10MB可以考虑改用流式下载或临时文件 URL 的方式。四、Excel 导入实现导入比导出稍复杂需要定义 DTO 来映射 Excel 列。4.1 定义导入 DTOinternal class DataImportDto { public string 编号 { get; set; } ; public string 名称 { get; set; } ; public string 简称 { get; set; } ; public string 联系人 { get; set; } ; public string 电话 { get; set; } ; public string 类型 { get; set; } ; public string 等级 { get; set; } ; public decimal 金额 { get; set; } public string 状态 { get; set; } ; // ... 与导出列名一一对应 }关键DTO 属性名必须与 Excel 表头完全一致包括中文。4.2 Service 层解析并入库public async Task(int success, int failed, Liststring errors) ImportDataAsync(Stream stream) { using var memoryStream new MemoryStream(); await stream.CopyToAsync(memoryStream); memoryStream.Position 0; // MiniExcel 自动将 Excel 列映射到 DTO 属性 var rows MiniExcel.QueryDataImportDto(memoryStream); var success 0; var failed 0; var errors new Liststring(); await using var dbContext await _dbContextFactory.CreateDbContextAsync(); var rowIndex 0; foreach (var row in rows) { rowIndex; try { // 数据校验 if (string.IsNullOrWhiteSpace(row.名称)) { errors.Add($第{rowIndex 1}行名称不能为空); failed; continue; } // 检查是否已存在按编码去重 var existing await dbContext.Items .FirstOrDefaultAsync(x x.Code row.编号); if (existing ! null) { // 更新已有记录 existing.Name row.名称; existing.ShortName row.简称; existing.Contact row.联系人; } else { // 插入新记录 dbContext.Items.Add(new Item { Code row.编号, Name row.名称, ShortName row.简称, Contact row.联系人 }); } success; } catch (Exception ex) { errors.Add($第{rowIndex 1}行{ex.Message}); failed; } } await dbContext.SaveChangesAsync(); return (success, failed, errors); }4.3 Blazor 页面文件上传!-- 隐藏的文件输入框 -- InputFile idimportFileInput OnChangeHandleImportFile accept.xlsx,.xls styledisplay:none / RadzenButton Text导入 Iconupload ClickTriggerImport / code { private async Task TriggerImport() { // 通过 JS 触发隐藏的文件输入框 await JSRuntime.InvokeVoidAsync(triggerFileInput, importFileInput); } private async Task HandleImportFile(InputFileChangeEventArgs e) { var file e.File; if (file null) return; try { // 限制文件大小 5MB using var stream file.OpenReadStream(maxAllowedSize: 5 * 1024 * 1024); var (success, failed, errors) await DataService.ImportDataAsync(stream); if (errors.Count 0) { var errorMsg string.Join(\n, errors.Take(5)); NotificationService.Notify(NotificationSeverity.Warning, $导入完成成功{success}条失败{failed}条, errorMsg); } else { NotificationService.Notify(NotificationSeverity.Success, 成功, $导入完成共{success}条记录); } // 刷新列表 await LoadData(); } catch (Exception ex) { NotificationService.Notify(NotificationSeverity.Error, 导入失败, ex.Message); } } }五、进阶技巧5.1 大数据量导出避免内存溢出当数据量超过 10 万行时ToList()会将所有数据加载到内存。可以改用流式写入public async Taskbyte[] ExportLargeDataAsync() { using var memoryStream new MemoryStream(); // 使用 IEnumerable 延迟执行MiniExcel 会逐行写入 var data GetLargeDataEnumerable(); // 返回 IEnumerableT不要 ToList() MiniExcel.SaveAs(memoryStream, data); return memoryStream.ToArray(); }5.2 自定义表头样式MiniExcel 支持通过ExcelAttribute自定义列名和样式public class ExportDto { [ExcelColumnName(编号)] public string Code { get; set; } [ExcelColumnName(日期)] [ExcelFormat(yyyy-MM-dd)] public DateTime Date { get; set; } [ExcelColumnName(金额)] [ExcelFormat(#,##0.00)] public decimal Amount { get; set; } }5.3 多 Sheet 导出var data new Dictionarystring, object { [列表A] listA, [列表B] listB, [列表C] listC }; MiniExcel.SaveAs(memoryStream, data);5.4 导出时保持与表格一致的列顺序在 Blazor 页面中RadzenDataGrid的列顺序由 Razor 标记决定。导出时匿名类型的属性顺序就是 Excel 列顺序确保两者一致即可。六、常见问题与解决方案Q1: 导出的 Excel 打开时报文件格式损坏原因文件扩展名与内容不匹配或传输过程中 Base64 编码被截断。解决确保文件名以.xlsx结尾检查 SignalR 消息大小限制默认 32KB需调大。// Program.cs 中配置 builder.Services.AddServerSideBlazor() .AddHubOptions(options { options.MaximumReceiveMessageSize 1024 * 1024 * 32; // 32MB });Q2: 中文列名乱码MiniExcel 默认使用 UTF-8 编码一般不会出现乱码。如果仍有问题确保系统区域设置正确。Q3: 导入时日期格式解析失败解决在 DTO 中使用string类型接收再手动解析public class ImportDto { public string 日期 { get; set; } ; public DateTime? ParsedDate DateTime.TryParse(日期, out var d) ? d : null; }Q4: 大文件导入超时Blazor Server 的 SignalR 连接有超时限制。对于大文件导入建议前端显示进度条后端使用后台任务处理分批保存每 100 行SaveChanges一次Q5: 导出文件过大导致 SignalR 断开当 Excel 文件超过 SignalR 默认消息大小限制时连接会断开。解决方案调大 SignalR 消息限制见 Q1改用 HTTP 端点下载Service 将文件写入临时目录返回文件路径前端通过window.open(url)下载分页导出让用户选择导出范围避免一次性导出全部数据七、总结功能核心代码行数导出MiniExcel.SaveAs(stream, data)1 行导入MiniExcel.QueryT(stream)1 行浏览器下载JSRuntime.InvokeVoidAsync(saveAsFile, ...)1 行文件上传InputFile OnChangeHandleImportFile /1 行整个 Excel 导出/导入功能核心代码不超过 50 行这就是 MiniExcel 的魅力。完整流程用户点击导出 → Blazor 调用 Service → Service 查询数据库 → 投影为匿名类型 → MiniExcel.SaveAs 生成 byte[] → Base64 编码 → JS 创建 a 标签触发下载用户点击导入 → JS 触发 InputFile → 用户选择文件 → Blazor 获取 Stream → Service 用 MiniExcel.QueryT 解析 → 逐行校验/入库 → 返回成功/失败统计引入地址
返回列表