许多现代数据源都提供了应用程序接口(API),用于下载 JSON 格式的数据。但许多用户更喜欢使用电子表格软件。SheetJS 库通过将便于程序员使用的 JSON 转换为便于用户使用的工作簿,帮助弥合了这一差距。
本示例的目标是生成一个包含美国总统姓名和出生日期的XLSX工作簿。我们将使用标准的JavaScript函数下载并处理一个JSON数据集。一旦我们得到一个简单的姓名和出生日期列表,我们将使用SheetJS API函数来构建工作簿对象并导出为XLSX格式。
以下序列图展示了该流程:

原始数据以JSON格式提供,下载地址:https://www.hxstrive.com/cdn/resources/sheetjs/sheetjs_executive.json
使用 fetch 获取数据很简单,代码如下:
fetch("https://www.hxstrive.com/cdn/resources/sheetjs/sheetjs_executive.json").then(resp => {
// 检查请求是否成功(状态码 200-299)
if (!resp.ok) {
throw new Error(`请求失败:状态码 ${resp.status}`);
}
// 解析响应体为完整的 JSON 数据
return resp.json();
}).then(data => {
// data 是获取的数据,后续业务在这里进行
}).catch(err => {
// 拉取数据失败
console.log(err);
});注意,原始数据是一个对象数组。下面是“John Adams”的相关数据,格式如下:
[
{
"name": {
"first": "John", // <-- 名字(名)
"last": "Adams" // <-- 姓氏
},
"bio": {
"birthday": "1735-10-19", // <-- 生日
},
"terms": [ // <-- 总统任期数组
{ "type": "viceprez", "start": "1789-04-21", }, // viceprez 副总统
{ "type": "viceprez", "start": "1793-03-04", },
{ "type": "prez", "start": "1797-03-04", } // <-- 总统任期,prez 总统
]
},...
]该数据集包含副总统,从未担任过总统的数据,我们需要筛选出担任过总统的数据。
每个对象的 terms 字段是一个任期数组。如果 type 属性为 "prez",则该任期为总统任期。我们关注的是至少担任过一个任期的总统。下面这行代码创建了一个总统数组:
// 筛选总统:从原始数据中过滤出有总统任期的条目
const prez = data.filter(row => {
// 检查当前人物的所有任期(terms)中,是否存在总统类型的任期
// some()方法:只要有一个任期满足条件,就返回true,当前人物会被保留
return row.terms.some(term => {
// 任期类型(type)为 "prez" 表示该任期是总统任期
return term.type === "prez";
});
});该数据集按首次担任总统或副总统的任期以时间顺序排序。在特定任期内,副总统和总统按字母顺序排序。
巴拉克·奥巴马于 2009 年当选总统,约瑟夫·拜登当选副总统。由于按字母顺序“拜登”排在“奥巴马”之前,所以拜登的数据会先出现。
目标是按照总统首次任期的顺序对他们进行排序。
第一步是将第一个总统任期的开始日期添加到数据集中。下面的代码会查看每位总统,并创建一个 start 属性,该属性代表第一个总统任期的开始。
prez.forEach(row => {
// find()方法:返回第一个满足条件的任期对象(总统通常只有连续的总统任期,取第一个即可)
row.start = row.terms.find(term => term.type === "prez").start
});此时,prez 数组中的每一行都有一个 start 属性。由于这些 start 属性都是字符串,因此下面这行代码对数组进行排序:
prez.sort((l,r) => l.start.localeCompare(r.start));在这个示例中,姓名将是名和姓的组合(row.name.first + " " + row.name.last),而生日可以在row.bio.birthday 处获取。使用 Array#map,可以通过一次调用对数据集进行处理:
const rows = prez.map(row => ({
name: row.name.first + " " + row.name.last,
birthday: row.bio.birthday
}));结果是一个由“简单”对象组成的数组,这些对象没有嵌套:
[
{ name: "George Washington", birthday: "1732-02-22" },
{ name: "John Adams", birthday: "1735-10-19" },
// ... one row per President
]json_to_sheet() 方法可以从清理后的数据集生成一个 SheetJS 工作表:
const worksheet = XLSX.utils.json_to_sheet(rows);XLSX.utils.book_new() 方法会创建一个新的工作簿,而 XLSX.utils.book_append_sheet() 方法会将一个工作表附加到该工作簿中。这个新工作表将被命名为 “Dates”:
// 创建一个新的工作簿
const workbook = XLSX.utils.book_new();
// 将工作表添加到工作簿,名称为 “Dates”
XLSX.utils.book_append_sheet(workbook, worksheet, "Dates");数据已经在工作簿中,并且可以导出。如下图:

上述表格有多个改进项,如可以重命名标题,也可以调整列宽。
默认情况下,json_to_sheet() 会创建一个带有标题行的工作表。在这种情况下,标题来自 JS 对象的键:“name” 和 “birthday”,标题会被写入第一行。
XLSX.utils.sheet_add_aoa() 方法可以覆盖工作表中的数据,下面这行代码会将 A1设置为 “Name”,并将 B1设置为 “Birthday”:
XLSX.utils.sheet_add_aoa(worksheet, [["Name", "Birthday"]], { origin: "A1" });上述代码中,“origin”用来配置写入位置,指定数据写入的起始单元格。其中,"A1" 表示从 A1 单元格开始写入(也可传数字索引,如 0 代表 A1,{r:0, c:0} 也等价于 A1)。
有些名称比默认列宽长,列宽存储在 "!cols" 工作表属性中。
以下代码行将 A 列的宽度设置为大约 10 个字符:
worksheet["!cols"] = [ { wch: 10 } ]; // 将A列宽度设置为10个字符对 rows 执行一次 Array#reduce 调用可以计算出最大宽度:
// 计算姓名列的最大宽度:基于所有总统姓名的字符长度,取最大值(最小宽度保底为10)
// rows:包含总统数据的数组;r:遍历的每个总统对象;r.name:总统姓名
const max_width = rows.reduce((w, r) => {
// 比较当前累计的最大宽度(w)和当前总统姓名的长度,保留较大值
return Math.max(w, r.name.length);
}, 10); // 初始值10:确保列宽至少为10,避免姓名过短时列宽过小
// 设置列宽为姓名列的最大宽度
worksheet["!cols"] = [ { wch: max_width } ];清理后,生成的工作簿如下方截图所示:

XLSX.writeFile() 方法会创建一个电子表格文件并尝试将其写入系统。在浏览器中,它会尝试提示用户下载该文件。在 NodeJS 中,它会写入本地目录:
XLSX.writeFile(workbook, "Presidents.xlsx", { compression: true });其中,compression 表示是否开启压缩,默认不开启:
true:开启 ZIP 压缩(.xlsx 本质是压缩的 XML 包),生成的文件体积更小;
false:关闭压缩,文件体积更大,但写入 / 读取速度略快。
XLSX、XLSB、NUMBERS 和 ODS 文件使用 ZIP 容器。ZIP 容器格式支持不同级别的压缩。电子表格软件在导出文件时通常会使用压缩功能。
默认情况下,SheetJS 写入器会优先考虑速度。导出速度更快,但生成的文件更大。
如果设置了compression选项,写入器会优先考虑文件大小。导出的文件更小,但会花费更多时间。
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SheetJS 教程</title>
<!-- 引入 SheetJS 库 -->
<script src="https://www.hxstrive.com/cdn/libs/SheetJS/0.20.3/xlsx.full.min.js"></script>
</head>
<body>
<p>
<button type="button" onclick="exportData()">导出数据</button>
</p>
<script type="text/javascript">
// 示例数据的格式:
// [
// {
// "id": {
// "bioguide": "W000178",
// "govtrack": 411351,
// "icpsr_prez": 99869
// },
// "name": {
// "first": "George",
// "last": "Washington"
// },
// "bio": {
// "birthday": "1732-02-22",
// "gender": "M"
// },
// "terms": [
// {
// "type": "prez",
// "start": "1789-04-30",
// "end": "1793-03-04",
// "party": "no party",
// "how": "election"
// },
// {
// "type": "prez",
// "start": "1793-03-04",
// "end": "1797-03-04",
// "party": "no party",
// "how": "election"
// }
// ]
// },...
// ]
function exportData() {
// 获取 JSON 数据并解析
fetch("https://www.hxstrive.com/cdn/resources/sheetjs/sheetjs_executive.json").then(resp => {
// 检查请求是否成功(状态码 200-299)
if (!resp.ok) {
throw new Error(`请求失败:状态码 ${resp.status}`);
}
// 解析响应体为完整的 JSON 数据
return resp.json();
}).then(data => {
// 筛选总统
const prez = data.filter(row => {
// 检查是否有总统任期
return row.terms.some(term => {
return term.type === "prez";
});
});
// 按首次总统任期排序
prez.forEach(row => {
row.start = row.terms.find(term => term.type === "prez").start
});
prez.sort((l,r) => l.start.localeCompare(r.start));
// 扁平化对象
const rows = prez.map(row => ({
name: row.name.first + " " + row.name.last,
birthday: row.bio.birthday
}));
// 生成工作表和工作簿
const worksheet = XLSX.utils.json_to_sheet(rows);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Dates");
// 修复标题
XLSX.utils.sheet_add_aoa(worksheet, [["姓名", "生日"]], { origin: "A1" });
// 计算列宽
const max_width = rows.reduce((w, r) => Math.max(w, r.name.length), 10);
worksheet["!cols"] = [ { wch: max_width } ];
// 创建一个XLSX文件并尝试保存为Presidents.xlsx
XLSX.writeFile(workbook, "Presidents.xlsx", { compression: true });
}).catch(err => {
// 拉取数据失败
console.log(err);
});
}
</script>
</body>
</html>点击“导出数据”按钮,导出表格如下:

参考资料:https://docs.sheetjs.com/docs/getting-started/examples/export/