大家好,我是码农刚子。本文讲解如何在 Blazor Server 项目中优雅地实现 Excel 导出和导入功能。技术栈:Blazor Server + Radzen UI + MiniExcel + Entity Framework Core。
一、为什么选择 MiniExcel?
在 .NET 生态中,处理 Excel 的主流库有:
| 库 | 特点 | 适用场景 |
|---|---|---|
| EPPlus | 功能强大,支持图表/公式 | 复杂报表,需商业授权(v5+) |
| NPOI | 老牌库,兼容 .xls/.xlsx | 需要兼容旧格式 |
| ClosedXML | API 友好,基于 OpenXML | 中等复杂度报表 |
| MiniExcel | 轻量、高性能、零依赖 | 大数据量导出/导入 |
选择 MiniExcel 的理由:
- 零外部依赖 --- 不需要安装 Office,不需要 COM 组件
- 极低内存占用 --- 基于流式读写,100 万行数据仅需几十 MB 内存
- API 极简 --- 导出只需一行
SaveAs,导入只需一行Query<T> - MIT 开源协议 --- 商业项目无顾虑
xml
<PackageReference Include="MiniExcel" Version="1.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 层:构建数据并生成 Excel
csharp
public async Task<byte[]> 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表达式做枚举映射 :将数字类型(如Type=1)转换为可读文本(如"类型A"),避免导出后用户看不懂- 空值保护 :所有可空字段使用
?? ""兜底,避免 Excel 中出现空白单元格
3.2 Blazor 页面:触发下载
razor
@page "/DataList"
@inject IDataService DataService
@inject IJSRuntime JSRuntime
@inject NotificationService NotificationService
<DataGridToolbar>
<RadzenButton Text="导出" Icon="download"
ButtonStyle="ButtonStyle.Info"
Click="@ExportData" />
</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 中定义全局函数:
html
<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 定义导入 DTO
csharp
internal 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 层:解析并入库
csharp
public async Task<(int success, int failed, List<string> errors)>
ImportDataAsync(Stream stream)
{
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
memoryStream.Position = 0;
// MiniExcel 自动将 Excel 列映射到 DTO 属性
var rows = MiniExcel.Query<DataImportDto>(memoryStream);
var success = 0;
var failed = 0;
var errors = new List<string>();
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 页面:文件上传
razor
<!-- 隐藏的文件输入框 -->
<InputFile id="importFileInput"
OnChange="@HandleImportFile"
accept=".xlsx,.xls"
style="display:none" />
<RadzenButton Text="导入" Icon="upload" Click="@TriggerImport" />
@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() 会将所有数据加载到内存。可以改用流式写入:
csharp
public async Task<byte[]> ExportLargeDataAsync()
{
using var memoryStream = new MemoryStream();
// 使用 IEnumerable 延迟执行,MiniExcel 会逐行写入
var data = GetLargeDataEnumerable(); // 返回 IEnumerable<T>,不要 ToList()
MiniExcel.SaveAs(memoryStream, data);
return memoryStream.ToArray();
}
5.2 自定义表头样式
MiniExcel 支持通过 ExcelAttribute 自定义列名和样式:
csharp
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 导出
csharp
var data = new Dictionary<string, object>
{
["列表A"] = listA,
["列表B"] = listB,
["列表C"] = listC
};
MiniExcel.SaveAs(memoryStream, data);
5.4 导出时保持与表格一致的列顺序
在 Blazor 页面中,RadzenDataGrid 的列顺序由 Razor 标记决定。导出时匿名类型的属性顺序就是 Excel 列顺序,确保两者一致即可。
六、常见问题与解决方案
Q1: 导出的 Excel 打开时报"文件格式损坏"
原因:文件扩展名与内容不匹配,或传输过程中 Base64 编码被截断。
解决 :确保文件名以 .xlsx 结尾,检查 SignalR 消息大小限制(默认 32KB,需调大)。
csharp
// Program.cs 中配置
builder.Services.AddServerSideBlazor()
.AddHubOptions(options =>
{
options.MaximumReceiveMessageSize = 1024 * 1024 * 32; // 32MB
});
Q2: 中文列名乱码
MiniExcel 默认使用 UTF-8 编码,一般不会出现乱码。如果仍有问题,确保系统区域设置正确。
Q3: 导入时日期格式解析失败
解决 :在 DTO 中使用 string 类型接收,再手动解析:
csharp
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.Query<T>(stream) |
1 行 |
| 浏览器下载 | JSRuntime.InvokeVoidAsync("saveAsFile", ...) |
1 行 |
| 文件上传 | <InputFile OnChange="@HandleImportFile" /> |
1 行 |
整个 Excel 导出/导入功能,核心代码不超过 50 行,这就是 MiniExcel 的魅力。
完整流程:
用户点击"导出" → Blazor 调用 Service → Service 查询数据库
→ 投影为匿名类型 → MiniExcel.SaveAs 生成 byte[]
→ Base64 编码 → JS 创建 <a> 标签触发下载
用户点击"导入" → JS 触发 <InputFile> → 用户选择文件
→ Blazor 获取 Stream → Service 用 MiniExcel.Query<T> 解析
→ 逐行校验/入库 → 返回成功/失败统计
原文连接:Blazor Server 中使用 MiniExcel 实现 Excel 导出与导入 --- 实战教程 - 码农刚子的开发笔记