搞定 AngularJS JSON 导出 CSV/XLSX (含嵌套数据)
2025-05-01 10:34:57
AngularJS 实现 JSON 导出 CSV/XLSX:一步步教你搞定
写 AngularJS 应用的时候,经常会碰到要把页面上或者程序里的数据导出来的需求,特别是导成 Excel 能打开的 XLSX 文件或者通用的 CSV 文件。这事儿吧,说难不难,说简单也不简单,特别是当你的 JSON 数据结构有点小复杂的时候。
这不,就有朋友遇到了这么个问题:他用 AngularJS,想把一个包含嵌套数组的 JSON 数据导出成 XLSX,试了半天,用了 AlaSQL 库,但导出的文件里没数据。咱们一起来看看这到底是咋回事,该怎么解决。
问题来了:想在 AngularJS 里导出 JSON 到 Excel 或 CSV?
这位朋友的代码大概是这样:
HTML 部分: (引入了 FileSaver.js, alasql.min.js, xlsx.core.min.js)
<html ng-app="myApp">
<head>
<!-- 确保这些库正确引入 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/alasql/0.5.5/alasql.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.16.9/xlsx.core.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.min.js"></script> <!-- 假设使用 AngularJS -->
</head>
<body ng-controller="exportreportController">
<button ng-click="exportData()" class="btn btn-primary" style="float:right;padding-right:10px;">导出 <i class="fa fa-arrow-down"></i></button>
<!-- 这里可以加个表格展示数据,方便看 -->
<h2>原始数据预览</h2>
<div ng-repeat="inverter in inverters">
<h3>{{ inverter.InvDetails }}</h3>
<table border="1" cellpadding="5">
<thead>
<tr>
<th>Id</th>
<th>Invertor_Id</th>
<th>Time_of_Reading</th>
<th>Lastreading</th>
<th>Readingby</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="record in inverter.LstRecords">
<td>{{ record.Id }}</td>
<td>{{ record.Invertor_Id }}</td>
<td>{{ record.Time_of_Reading }}</td>
<td>{{ record.Lastreading }}</td>
<td>{{ record.Readingby }}</td>
</tr>
</tbody>
</table>
</div>
<script>
// JS 代码放后面
</script>
</body>
</html>
JavaScript (AngularJS Controller) 部分:
var app = angular.module('myApp', []);
app.controller('exportreportController', ['$scope', '$timeout', // 简化依赖注入
function ($scope, $timeout) {
$scope.inverters = [
// ... (和问题中一样的 JSON 数据) ...
{
"InvDetails": "UPS",
"LstRecords": [
{ "Id": 1, "Invertor_Id": 1, "Time_of_Reading": "20170214", "Lastreading": 0, "Readingby": 0 },
{ "Id": 87, "Invertor_Id": 1, "Time_of_Reading": "20170215", "Lastreading": 5, "Readingby": 10 },
{ "Id": 110, "Invertor_Id": 1, "Time_of_Reading": "20170216", "Lastreading": 10, "Readingby": 92 },
{ "Id": 111, "Invertor_Id": 1, "Time_of_Reading": "20170216", "Lastreading": 92, "Readingby": 95 }
]
},
{
"InvDetails": "Power Supply",
"LstRecords": [
{ "Id": 2, "Invertor_Id": 2, "Time_of_Reading": "20170214", "Lastreading": 0, "Readingby": 0 },
{ "Id": 88, "Invertor_Id": 2, "Time_of_Reading": "20170215", "Lastreading": 7, "Readingby": 13 },
{ "Id": 109, "Invertor_Id": 2, "Time_of_Reading": "20170216", "Lastreading": 13, "Readingby": 25 },
{ "Id": 112, "Invertor_Id": 2, "Time_of_Reading": "20170216", "Lastreading": 25, "Readingby": 49 }
]
}
];
$scope.exportData = function () {
// alert("exporting"); // 这行可以注释掉或删掉
var data = "";
$scope.headers = [];
angular.forEach($scope.inverters, function (value, key) {
// debugger // 这行也可以注释掉或删掉
var we = value.InvDetails;
$scope.headers.push(we); // 收集 InvDetails 到 headers 数组
$scope.last = value.LstRecords;
angular.forEach($scope.last, function (value, key) {
// 问题在这里:每次循环都覆盖了 data 变量
data = {
"Id": value.Id,
"Invertor_Id": value.Invertor_Id,
"Time_of_Reading": value.Time_of_Reading,
"Lastreading": value.Lastreading,
"Readingby": value.Readingby
};
})
})
$scope.result = [];
$scope.result0 = []; // 这个变量好像没用到
$scope.result.push({
"Invertor1": JSON.stringify(data) // 问题:只把最后一个记录转成 JSON 字符串放进去了
})
// debugger // 这行也可以注释掉或删掉
// 问题:传递给 alasql 的数据格式不对,只包含一个字段 "Invertor1",其值是最后一个记录的 JSON 字符串
alasql('SELECT * INTO XLSX("john.xlsx",{headers:true}) FROM ?', [$scope.result]);
};
}]);
目标是点击“导出”按钮,把 $scope.inverters
里的数据,特别是 LstRecords
数组里的那些记录,整理好输出成一个 XLSX 文件或者 CSV 文件。
为啥我的代码跑不通?—— 原因分析
仔细看看 exportData
函数,就能发现几个问题:
-
数据处理逻辑不对:
- 代码里用了一个
data
变量,在内层循环 (forEach $scope.last
) 里,每次都会被重新赋值。这意味着循环结束后,data
变量里存的永远是 最后一个LstRecords
里的 最后一条 记录。比如上面例子里,它最后会是{"Id": 112, "Invertor_Id": 2, ...}
这条。 - 然后,代码把这个
data
对象JSON.stringify
处理,再放到一个只包含"Invertor1"
这个键的临时对象里,最后push
进$scope.result
数组。$scope.result
最后只包含一个元素,类似[{ "Invertor1": "{\"Id\":112,\"Invertor_Id\":2,...}" }]
。
- 代码里用了一个
-
AlaSQL 的用法不匹配:
alasql('SELECT * INTO XLSX(...) FROM ?', [$scope.result])
这句是想让 AlaSQL 把数据导出。但是,你传给它的数据$scope.result
是[{ "Invertor1": "..." }]
这种结构的。AlaSQL 看到这个,默认会导出一个只有一列(列名叫Invertor1
)并且只有一行数据的表格,这一行数据就是那个 JSON 字符串。这显然不是我们想要的,我们想要的是多行多列的数据,每一行对应一条LstRecords
记录。
-
数据结构没“拍平”:
- 原始数据
$scope.inverters
是一个嵌套结构:一个逆变器数组,每个逆变器下面又有一个记录数组 (LstRecords
)。要想把它变成适合表格展示(比如 CSV 或 XLSX)的格式,通常需要把这种嵌套结构“拍平”(Flatten)。意思就是,把子记录和它的父信息(比如InvDetails
)组合在一起,形成一条条独立的记录。比如,我们可能想要这样的数据格式(一个扁平的对象数组):
[ { "InvDetails": "UPS", "Id": 1, "Invertor_Id": 1, "Time_of_Reading": "20170214", "Lastreading": 0, "Readingby": 0 }, { "InvDetails": "UPS", "Id": 87, "Invertor_Id": 1, "Time_of_Reading": "20170215", "Lastreading": 5, "Readingby": 10 }, // ... 其他 UPS 的记录 ... { "InvDetails": "Power Supply", "Id": 2, "Invertor_Id": 2, "Time_of_Reading": "20170214", "Lastreading": 0, "Readingby": 0 }, { "InvDetails": "Power Supply", "Id": 88, "Invertor_Id": 2, "Time_of_Reading": "20170215", "Lastreading": 7, "Readingby": 13 } // ... 其他 Power Supply 的记录 ... ]
只有准备好这种扁平化的数据结构,AlaSQL 或者手动生成 CSV 才能正确工作。
- 原始数据
解决方案:动手改造!
知道了问题在哪,改起来就容易多了。下面提供两种主要的解决方案:
方案一:利用 AlaSQL 库 (推荐)
既然已经引入了 AlaSQL,那咱们就用好它。核心思路就是先按上面说的,把数据“拍平”,然后再交给 AlaSQL 处理。
原理与作用
AlaSQL 是一个强大的 JavaScript SQL 数据库库,它能在浏览器里直接对 JavaScript 对象数组执行 SQL 查询,还能方便地把查询结果导出成 XLSX, CSV, JSON 等多种格式。咱们的目标就是构造一个符合 AlaSQL 要求的、扁平化的 JavaScript 对象数组。
操作步骤与代码
修改 exportData
函数如下:
app.controller('exportreportController', ['$scope', '$timeout',
function ($scope, $timeout) {
$scope.inverters = [
// ... (原始 JSON 数据不变) ...
{
"InvDetails": "UPS",
"LstRecords": [
{ "Id": 1, "Invertor_Id": 1, "Time_of_Reading": "20170214", "Lastreading": 0, "Readingby": 0 },
{ "Id": 87, "Invertor_Id": 1, "Time_of_Reading": "20170215", "Lastreading": 5, "Readingby": 10 },
{ "Id": 110, "Invertor_Id": 1, "Time_of_Reading": "20170216", "Lastreading": 10, "Readingby": 92 },
{ "Id": 111, "Invertor_Id": 1, "Time_of_Reading": "20170216", "Lastreading": 92, "Readingby": 95 }
]
},
{
"InvDetails": "Power Supply",
"LstRecords": [
{ "Id": 2, "Invertor_Id": 2, "Time_of_Reading": "20170214", "Lastreading": 0, "Readingby": 0 },
{ "Id": 88, "Invertor_Id": 2, "Time_of_Reading": "20170215", "Lastreading": 7, "Readingby": 13 },
{ "Id": 109, "Invertor_Id": 2, "Time_of_Reading": "20170216", "Lastreading": 13, "Readingby": 25 },
{ "Id": 112, "Invertor_Id": 2, "Time_of_Reading": "20170216", "Lastreading": 25, "Readingby": 49 }
]
}
];
$scope.exportData = function () {
console.log("开始导出数据..."); // 加个日志方便调试
// 1. 数据拍平处理
var flattenedData = [];
angular.forEach($scope.inverters, function (inverter) {
var details = inverter.InvDetails; // 获取父级信息
angular.forEach(inverter.LstRecords, function (record) {
// 创建一个新的对象,包含父级信息和子记录信息
var flatRecord = {
'设备详情': details, // 可以自定义列名
'记录ID': record.Id,
'逆变器ID': record.Invertor_Id,
'读取时间': record.Time_of_Reading,
'上次读数': record.Lastreading,
'读取人': record.Readingby // 或者 '本次读数'? 根据业务理解调整
};
flattenedData.push(flatRecord);
});
});
console.log("拍平后的数据:", flattenedData); // 看看数据对不对
if (flattenedData.length === 0) {
alert("没有数据可以导出!");
return;
}
// 2. 使用 AlaSQL 导出 XLSX
var options = {
headers: true, // 让 AlaSQL 自动使用对象的键作为表头
// 可以添加其他 AlaSQL 选项,比如指定 sheet 名称
// sheetid: '我的报告'
};
try {
// SELECT * 表示选择所有列
// INTO XLSX("文件名.xlsx", 配置)
// FROM ? 表示数据源来自后面的参数数组
alasql('SELECT * INTO XLSX("导出报告.xlsx", ?) FROM ?', [options, flattenedData]);
console.log("XLSX 文件导出成功!");
} catch (error) {
console.error("导出 XLSX 时出错:", error);
alert("导出失败,请查看控制台错误信息。");
}
// 如果也想支持导出 CSV
// 可以加一个类似的按钮或判断逻辑
// try {
// alasql('SELECT * INTO CSV("导出报告.csv", {headers: true, separator:","}) FROM ?', [flattenedData]);
// console.log("CSV 文件导出成功!");
// } catch (error) {
// console.error("导出 CSV 时出错:", error);
// alert("导出 CSV 失败,请查看控制台错误信息。");
// }
};
}]);
代码解释:
- 我们创建了一个空数组
flattenedData
。 - 通过两层
angular.forEach
遍历原始的$scope.inverters
数据。 - 在内层循环中,对于
LstRecords
里的每一条record
,我们创建了一个新的对象flatRecord
。这个对象不仅包含了record
自身的所有属性(比如Id
,Time_of_Reading
等),还把外层的inverter.InvDetails
也加了进去,并给了它一个有意义的键名,比如'设备详情'
。 - 你可以按需调整
flatRecord
里的键名,这些键名会成为 Excel 文件里的列标题(因为我们设置了headers: true
)。 - 把每个
flatRecord
添加到flattenedData
数组里。 - 最后,调用
alasql('SELECT * INTO XLSX("导出报告.xlsx", ?) FROM ?', [options, flattenedData])
。这里:SELECT *
表示选择flattenedData
里每个对象的所有属性作为列。INTO XLSX("导出报告.xlsx", ?)
表示输出格式为 XLSX,文件名是 "导出报告.xlsx",第二个?
代表options
对象。FROM ?
表示数据来源是第三个?
代表的flattenedData
数组。options
对象里的headers: true
告诉 AlaSQL 使用flattenedData
里对象的键作为 Excel 的表头。
这样,点击按钮后,就会生成一个名为 "导出报告.xlsx" 的文件,里面的数据就是我们期望的扁平化表格。
进阶技巧
- 自定义列名和顺序: 如果不想用对象本身的键名,或者想控制列的顺序,可以在 AlaSQL 查询里明确指定列和别名:
var sql = 'SELECT 设备详情 AS "设备名称", `记录ID` AS "记录编号", 读取时间, 上次读数, 读取人 \ INTO XLSX("自定义报告.xlsx", ?) FROM ?'; // 注意:如果列名包含空格或特殊字符,建议用反引号或双引号包起来。 alasql(sql, [options, flattenedData]);
- 数据格式化: AlaSQL 可能不直接支持复杂的 Excel 单元格格式化(比如日期格式、数字精度)。如果需要精细控制,可能要考虑更专业的库,或者导出 CSV 后在 Excel 里手动调整。对于日期,确保
Time_of_Reading
字段是 Excel 能识别的格式(比如YYYY-MM-DD
),或者在导出前转换一下。 - 错误处理: 加上
try...catch
块是个好习惯,可以在导出失败时给用户提示,并在控制台输出详细错误。
安全建议
- 如果
$scope.inverters
的数据是用户输入的或者来自不可信的外部 API,导出前最好做一下数据校验和清理(Sanitization),防止潜在的注入攻击(虽然导出到 XLSX/CSV 的风险相对较低,但仍需注意,比如避免内容被 Excel 误解析为公式执行)。
方案二:手动生成 CSV 文件
如果你不想依赖 AlaSQL 的导出功能,或者只想导出简单的 CSV 格式,可以自己动手拼字符串。这需要用到 FileSaver.js
(用户已经引入了)。
原理与作用
CSV (Comma-Separated Values) 是一种纯文本格式,用逗号(或其他分隔符)分隔不同的字段,用换行符分隔不同的记录行。手动生成 CSV 就是构造这么一个符合规范的大字符串,然后利用 FileSaver.js
把它包装成文件让用户下载。
操作步骤与代码
同样需要先做数据“拍平”。我们沿用方案一里的 flattenedData
。
app.controller('exportreportController', ['$scope', '$timeout',
function ($scope, $timeout) {
$scope.inverters = [ /* ... 原始数据 ... */ ];
// --- 这里是方案一的数据拍平逻辑,保持不变 ---
function getFlattenedData() {
var flattenedData = [];
angular.forEach($scope.inverters, function (inverter) {
var details = inverter.InvDetails;
angular.forEach(inverter.LstRecords, function (record) {
var flatRecord = {
'设备详情': details,
'记录ID': record.Id,
'逆变器ID': record.Invertor_Id,
'读取时间': record.Time_of_Reading,
'上次读数': record.Lastreading,
'读取人': record.Readingby
};
flattenedData.push(flatRecord);
});
});
return flattenedData;
}
// --- 数据拍平逻辑结束 ---
$scope.exportCsv = function () { // 新增一个导出 CSV 的函数
console.log("开始导出 CSV 数据...");
var flattenedData = getFlattenedData(); // 获取拍平后的数据
if (flattenedData.length === 0) {
alert("没有数据可以导出!");
return;
}
// 1. 准备 CSV 内容
var csvContent = "";
// 准备表头 (从第一个数据对象的键获取)
var headers = Object.keys(flattenedData[0]);
csvContent += headers.join(',') + '\n'; // 用逗号连接,并添加换行符
// 准备数据行
angular.forEach(flattenedData, function (row) {
var values = [];
angular.forEach(headers, function (header) {
// 处理单元格内容,特别是包含逗号或引号的情况
var cellValue = row[header] === null || row[header] === undefined ? '' : String(row[header]);
// 简单的处理:如果值包含逗号、双引号或换行符,用双引号括起来,并将内部双引号替换为两个双引号
if (cellValue.includes(',') || cellValue.includes('"') || cellValue.includes('\n')) {
cellValue = '"' + cellValue.replace(/"/g, '""') + '"';
}
values.push(cellValue);
});
csvContent += values.join(',') + '\n'; // 用逗号连接,并添加换行符
});
console.log("生成的 CSV 字符串:", csvContent.substring(0, 200) + "..."); // 只显示部分内容
// 2. 创建 Blob 对象
// IMPORTANT: 为了让 Excel 正确识别 UTF-8 编码(特别是中文),通常需要在文件开头加上 BOM (Byte Order Mark)
var bom = new Uint8Array([0xEF, 0xBB, 0xBF]); // UTF-8 BOM
var blob = new Blob([bom, csvContent], { type: 'text/csv;charset=utf-8;' });
// 3. 使用 FileSaver.js 下载
try {
saveAs(blob, "导出报告.csv"); // FileSaver.js 提供的函数
console.log("CSV 文件导出成功!");
} catch (error) {
console.error("导出 CSV 时出错:", error);
alert("导出 CSV 失败,请查看控制台错误信息。");
// 可能需要提示用户浏览器是否支持,或者 FileSaver.js 是否正确加载
}
};
// 你可以保留原来的 exportData 调用 AlaSQL,或者把它也改成调用 exportCsv
$scope.exportData = $scope.exportCsv; // 例如,让原来的按钮触发 CSV 导出
}]);
// 别忘了在 HTML 里加一个调用 exportCsv 的按钮,或者修改现有按钮的 ng-click
// <button ng-click="exportCsv()" class="btn btn-info">导出 CSV</button>
代码解释:
- 同样先调用
getFlattenedData()
获得扁平数据。 - 准备表头: 从
flattenedData
的第一个对象里获取所有的键 (Object.keys()
),用逗号 (join(',')
) 把它们连接起来,最后加上换行符 (\n
)。 - 准备数据行: 遍历
flattenedData
中的每个对象 (row
)。对于每一行:- 我们再次遍历表头 (
headers
),确保按正确的顺序取值。 - 获取
row[header]
的值。 - 重要:处理特殊字符。 这是手动生成 CSV 最麻烦的地方。如果单元格的值本身包含逗号、双引号或换行符,需要按 CSV 标准进行转义:整个值要用双引号包起来,并且值里面原有的双引号要替换成两个双引号 (
""
)。代码里提供了一个简单的实现。 - 把处理过的值
push
到values
数组。 - 用逗号连接
values
数组,并在末尾加换行符。
- 我们再次遍历表头 (
- 创建 Blob: 把表头和所有数据行拼成的
csvContent
字符串,连同一个 UTF-8 BOM(\uFEFF
或者new Uint8Array([0xEF, 0xBB, 0xBF])
),一起放进一个Blob
对象。指定 MIME 类型为text/csv;charset=utf-8;
。加上 BOM 对中文 Excel 打开 CSV 文件至关重要,否则可能乱码。 - 下载: 调用
saveAs(blob, "导出报告.csv")
,浏览器就会提示用户下载文件。
进阶技巧
- 分隔符: 如果不想用逗号,可以用别的分隔符(比如分号
;
或制表符\t
),只要在join()
和Blob
类型里相应修改就行。但逗号是最通用的。 - 大型数据: 如果数据量非常大,一次性生成整个 CSV 字符串可能会消耗很多内存,甚至卡死浏览器。这时可以考虑流式生成或分片处理,但实现起来复杂得多,通常前端不太会遇到需要处理这么大导出的场景。
- 库: 其实也有专门用于在前端生成 CSV 的库(比如 PapaParse 反过来用,或者简单的
json2csv
类型的库),可以帮你处理特殊字符转义等细节。如果觉得手动拼接太繁琐,可以考虑引入它们。
安全建议
- 和方案一类似,注意输入数据的校验和清理。
- 确保
FileSaver.js
库已经正确加载并且是可信来源。
现在,你应该能根据自己的需求,选择用 AlaSQL 还是手动生成 CSV 的方式,来解决 AngularJS 应用里 JSON 导出文件的问题了。两种方法都需要先把嵌套数据“拍平”,这是关键一步。