在做系統后臺的時候,一般Excel表導出的方案是在服務器生成Excel表,返回給瀏覽器Excel文件地址,觸發(fā)下載。當要導出的Excel行數達到數萬行時,占用大量的服務器CPU或內存不說,還容易請求超時,還要改nginx配置。我就想,能否在前端瀏覽器創(chuàng)建Excel表呢?之后找到一個合適的js庫可以實現這個功能:js-xlsx。
官方github地址:https://github.com/SheetJS/js-xlsx
實現思路
1、 瀏覽器創(chuàng)建xlsx
2、 每次ajax向后端請求若干行的數據,直到請求完成
3、 觸發(fā)下載
本文省略php,web服務器等環(huán)境搭建步驟。看懂接下來代碼實現,需要有一些js,html,PHP,ajax的一些相關知識。
1、js-xlsx庫安裝使用
本文使用npm安裝js-xlsx,如果不用npm,可以直接去https://github.com/SheetJS/js-xlsx/releases下載壓縮包(很卡,我的網下不動)。
創(chuàng)建項目目錄后,執(zhí)行以下命令,下載js-xlsx包:
npm init
npm install xlsx
創(chuàng)建index.html在header中引入
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>demo</title>
<script src="node_modules/xlsx/jszip.js"></script>
<script src="node_modules/xlsx/xlsx.js"></script>
</head>
2、創(chuàng)建html頁面元素
index.html
<body>
<body>
<!-- 進度顯示-->
<span>導出進度:</span><span id="progressText">0%</span>
<br>
<!-- 開始導出按鈕-->
<input type="button" id="startBtn" value="開始導出">
</body>
</body>
瀏覽器顯示:

3、添加js代碼,初始化excel工作簿全局變量/excel sheet全局變量,綁定按鈕點擊事件
<script>
//每次請求多少行數據
const ROWS_PER_REQ = 1000;
//總行數
let totalRows = 0;
//excel工作簿全局變量
let workBook = null;
//excel sheet全局變量
let workSheet = null;
//開始導出按鈕綁定事件
document.getElementById("startBtn").addEventListener('click',()=>{
//初始化
initWorkBook();
console.log("導出開始!");
getTotal();
});
function initWorkBook(){
//創(chuàng)建excel工作簿對象
workBook = XLSX.utils.book_new();
//創(chuàng)建excel第一行,頭部
let headArr = [
['第一列', '第二列', '第三列', '第四列', '第五列', '時間'],
];
//根據頭部數組創(chuàng)建excel sheet
workSheet = XLSX.utils.aoa_to_sheet(headArr);
//把excel sheet添加到工作簿
XLSX.utils.book_append_sheet(workBook, workSheet, 'sheet1');
}
getTotal()函數見下章。
這里的js-xlsx庫的幾個用法解釋:
XLSX.utils.book_new();創(chuàng)建并返回一個excel工作簿對象,可以理解為excel文件
XLSX.utils.aoa_to_sheet(headArr);接受一個二維數組作為參數,創(chuàng)建一個excel sheet對象(一個excel標簽頁)
XLSX.utils.book_append_sheet(workBook, workSheet, 'sheet1');
把excel sheet對象添加到工作簿對象,并命名為'sheet1'
4、發(fā)送Ajax請求從服務器獲取數據總行數
index.html:
//發(fā)送Ajax請求從服務器獲取數據總行數
function getTotal() {
let xhr = new XMLHttpRequest();
xhr.open('GET', 'get_data.php?getTotal=true');
xhr.responseType = "json";
xhr.onload = function(){
//接收到總行數
totalRows = xhr.response.total;
console.log("總行數:",totalRows);
//開始接收數據
getRowsFromServer(0, ROWS_PER_REQ);
};
xhr.send();
}
獲取到總行數后,保存到全局變量totalRows, getRowsFromServer函數見下章
后端get_data.php:
<?php
//設置返回json頭
header('Content-type: application/json');
//總行數
const TOTAL_ROW = 50000;
//獲取總行數請求
if(isset($_GET['getTotal'])){
//此處一般情況是從數據庫獲取總行數,這里為了簡化直接返回
$json = json_encode([
'total'=>TOTAL_ROW,
]);
echo $json;
exit;
}
5、使用遞歸依次獲取數據
index.html
/**發(fā)送Ajax請求分批從服務器拿數據
*
* @param offset 從第幾行數據開始
* @param limit 每次取出多少行
*/
function getRowsFromServer(offset, limit) {
let xhr = new XMLHttpRequest();
//拼接get字段
xhr.open('GET', `get_data.php?offset=${offset}&limit=${limit}`);
xhr.responseType = "json";
//接收到數據
xhr.onload = function(){
/*格式:
* [
* [第一列數據,第二列數據,第三列數據,.....],
* [第一列數據,第二列數據,第三列數據,.....],
* .......
* ]
* */
//寫入到excel sheet
XLSX.utils.sheet_add_aoa(workSheet, xhr.response ,{origin:-1});
//寫入excel后釋放內存
xhr.response = null;
let hasGetNum = offset + limit; //當前已取到的行數
console.log(`已寫入${hasGetNum}行`);
//更新進度顯示
updateProgress(hasGetNum, totalRows);
//如果沒取完,遞歸獲取下一批數據
if(hasGetNum < totalRows){
//剩余沒傳行數
let rest = totalRows - hasGetNum;
//計算下一批數據的數量
let nextLimit = rest > ROWS_PER_REQ ? ROWS_PER_REQ : rest;
//獲取下一批數據
getRowsFromServer(hasGetNum, nextLimit);
}else{
//收取完成,下載excel
XLSX.writeFile(workBook, 'export.xlsx');
//總行數制0
totalRows = 0;
}
};
xhr.send();
}
/**更新進度顯示
*
* @param now 當前已更新行數
* @param total 總行數
*/
function updateProgress(now, total){
//計算百分比
let percent = (now/total * 100).toFixed(2);
//更新dom
document.getElementById("progressText").innerHTML = percent;
}
每次獲取完數據使用updateProgress更新進度
這里多了幾個js-xlsx庫的用法:
XLSX.utils.sheet_add_aoa(workSheet, xhr.response ,{origin:-1})
作用:把數據添加到excel sheet
第一個參數:要添加的excel sheet 實例
第二個參數:數據二維數組(本例是從服務器返回)
第三個參數:從哪里插入數據,{origin:-1}代表從最后一行的下一行開始新增數據,其他用法本例用不到,想進一步了解可以參考官網
XLSX.writeFile(workBook, 'export.xlsx')
作用:生成一個excel文件,并觸發(fā)瀏覽器下載
第一個參數:excel工作簿實例
第二個參數:下載的文件名,文件格式會根據文件后綴自動識別,本例是xlsx, 其他格式本例用不到,想進一步了解可以參考官網
后端發(fā)送數據的邏輯get_data.php:
//獲取數據請求
$offset = intval($_GET['offset']);
$limit = intval($_GET['limit']);
//返回
echo json_encode(getData($offset, $limit));
//從數據庫獲取數據
function getData($offset, $limit)
{
//此處一般情況是從數據庫獲取數據,這里為了簡化直接生成數據
$data = [];
for ($i = 0; $i < $limit; $i++) {
$lineNum = $offset + $i + 1; //行號
$row = ["第${lineNum}行第1列數據", "第${lineNum}行第2列數據", "第${lineNum}行第3列數據", "第${lineNum}行第4列數據", "第${lineNum}行第5列數據", date('Y-m-d H:i:s')];
$data[] = $row;
}
usleep(300000);//模擬耗時操作,暫停300ms
return $data;
}
本demo完整的前后端代碼:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>demo</title>
<script src="node_modules/xlsx/jszip.js"></script>
<script src="node_modules/xlsx/xlsx.js"></script>
</head>
<body>
<!-- 進度顯示-->
<span>導出進度:</span><span id="progressText">0%</span>
<br>
<!-- 開始導出按鈕-->
<input type="button" id="startBtn" value="開始導出">
</body>
<script>
//每次請求多少行數據
const ROWS_PER_REQ = 1000;
//總行數
let totalRows = 0;
//excel工作簿全局變量
let workBook = null;
//excel sheet全局變量
let workSheet = null;
//開始導出按鈕綁定事件
document.getElementById("startBtn").addEventListener('click',()=>{
//初始化
initWorkBook();
console.log("導出開始!");
getTotal();
});
function initWorkBook(){
//創(chuàng)建excel工作簿對象
workBook = XLSX.utils.book_new();
//創(chuàng)建excel第一行,頭部
let headArr = [
['第一列', '第二列', '第三列', '第四列', '第五列', '時間'],
];
//根據頭部數組創(chuàng)建excel sheet
workSheet = XLSX.utils.aoa_to_sheet(headArr);
//把excel sheet添加到工作簿
XLSX.utils.book_append_sheet(workBook, workSheet, 'sheet1');
}
//發(fā)送Ajax請求從服務器獲取數據總行數
function getTotal() {
let xhr = new XMLHttpRequest();
xhr.open('GET', 'get_data.php?getTotal=true');
xhr.responseType = "json";
xhr.onload = function(){
//接收到總行數
totalRows = xhr.response.total;
console.log("總行數:",totalRows);
//開始接收數據
getRowsFromServer(0, ROWS_PER_REQ);
};
xhr.send();
}
/**發(fā)送Ajax請求分批從服務器拿數據
*
* @param offset 從第幾行數據開始
* @param limit 每次取出多少行
*/
function getRowsFromServer(offset, limit) {
let xhr = new XMLHttpRequest();
//拼接get字段
xhr.open('GET', `get_data.php?offset=${offset}&limit=${limit}`);
xhr.responseType = "json";
//接收到數據
xhr.onload = function(){
/*格式:
* [
* [第一列數據,第二列數據,第三列數據,.....],
* [第一列數據,第二列數據,第三列數據,.....],
* .......
* ]
* */
//寫入到excel sheet
XLSX.utils.sheet_add_aoa(workSheet, xhr.response ,{origin:-1});
//寫入excel后釋放內存
xhr.response = null;
let hasGetNum = offset + limit; //當前已取到的行數
console.log(`已寫入${hasGetNum}行`);
//更新進度顯示
updateProgress(hasGetNum, totalRows);
//如果沒取完,遞歸獲取下一批數據
if(hasGetNum < totalRows){
//剩余沒傳行數
let rest = totalRows - hasGetNum;
//計算下一批數據的數量
let nextLimit = rest > ROWS_PER_REQ ? ROWS_PER_REQ : rest;
//獲取下一批數據
getRowsFromServer(hasGetNum, nextLimit);
}else{
//收取完成,下載excel
XLSX.writeFile(workBook, 'export.xlsx');
//總行數制0
totalRows = 0;
}
};
xhr.send();
}
/**更新進度顯示
*
* @param now 當前已更新行數
* @param total 總行數
*/
function updateProgress(now, total){
//計算百分比
let percent = (now/total * 100).toFixed(2);
//更新dom
document.getElementById("progressText").innerHTML = percent;
}
</script>
</html>
get_data.php
<?php
//設置返回json頭
header('Content-type: application/json');
//總行數
const TOTAL_ROW = 50000;
//獲取總行數請求
if(isset($_GET['getTotal'])){
//此處一般情況是從數據庫獲取總行數,這里為了簡化直接返回
$json = json_encode([
'total'=>TOTAL_ROW,
]);
echo $json;
exit;
}
//獲取數據請求
$offset = intval($_GET['offset']);
$limit = intval($_GET['limit']);
//返回
echo json_encode(getData($offset, $limit));
//從數據庫獲取數據
function getData($offset, $limit)
{
//此處一般情況是從數據庫獲取數據,這里為了簡化直接生成數據
$data = [];
for ($i = 0; $i < $limit; $i++) {
$lineNum = $offset + $i + 1; //行號
$row = ["第${lineNum}行第1列數據", "第${lineNum}行第2列數據", "第${lineNum}行第3列數據", "第${lineNum}行第4列數據", "第${lineNum}行第5列數據", date('Y-m-d H:i:s')];
$data[] = $row;
}
usleep(300000);//模擬耗時操作,暫停300ms
return $data;
}
簡單性能測試一下:
5w行數據,占用內存200MB,導出無壓力
xlsx50w.PNG
20w行數據,占用內存600MB,導出無壓力
xlsx20w.PNG
image.png
100w行數據,js報錯,內存不夠
結束語:
對比服務端生成excel的方案,我覺得有以下一些優(yōu)點:
- 性能更好,導出速度快
- 有進度指示器,對用戶友好
- 分批生成,不會觸發(fā)請求超時
- 服務器壓力大大降低
- 和后端查詢列表頁面的邏輯基本一致,后端幾乎不用增加功能直接用
缺點:前端邏輯比較復雜
注意:
- 本文為原創(chuàng),代碼可以隨意使用,無版權,轉載請注明原地址
- 本文代碼不能直接用在生產環(huán)境,只用作流程演示,如需使用需要修改增加安全性等等。
- 如果使用es6的
async,await語法,數據接收的代碼比本例中的遞歸更簡潔 - 能生成excel表格最大行數只和客戶端配置有關
- js-xlsx還要很多用法,如調整單元格格式等,更多的用法可以參考官網
- 本文前端代碼在
Chrome 77內核的瀏覽器下運行正常,其他瀏覽器兼容性本文不做探討
