一、問題剖析
那是一個(gè)傾盆大雨的早上,花瓣隨風(fēng)雨落在我的肩膀上,是五顏六色的花朵。
我輕輕撫摸著他,隨后撥開第一朵花瓣,她不愛我。
撥開第二朵,她愛我。
正當(dāng)我沉迷于甜蜜的幻想中,后端小白 🙋 喊道:這個(gè)導(dǎo)出你前端應(yīng)該就能做的吧!
🙋🏻♂️ 那是自然,有什么功能是我大前端做不了的,必須得讓你們大開眼界。
二、為什么導(dǎo)出要前端做?
前端導(dǎo)出的場(chǎng)景:
- 輕量級(jí)數(shù)據(jù):如果要導(dǎo)出的表格數(shù)據(jù)相對(duì)較小,可以直接在前端生成和導(dǎo)出,避免服務(wù)器端的處理和通信開銷。
- 數(shù)據(jù)已存在于前端:如果表格數(shù)據(jù)已經(jīng)以 JSON 或其他形式存在于前端,可以直接利用前端技術(shù)將其導(dǎo)出為 Excel、CSV 或其他格式。
- 實(shí)時(shí)生成/計(jì)算:如果導(dǎo)出的表格需要根據(jù)用戶輸入或動(dòng)態(tài)生成,可以使用前端技術(shù)基于用戶操作實(shí)時(shí)生成表格,并提供導(dǎo)出功能。
- 快速響應(yīng):前端導(dǎo)出表格可以提供更快的響應(yīng)速度,避免等待服務(wù)器端的處理和下載時(shí)間。
后端導(dǎo)出的場(chǎng)景:
- 大量數(shù)據(jù):如果要導(dǎo)出的表格數(shù)據(jù)量很大,超過了前端處理能力或網(wǎng)絡(luò)傳輸限制,那么在服務(wù)器端進(jìn)行導(dǎo)出會(huì)更高效。
- 安全性和數(shù)據(jù)保護(hù):敏感數(shù)據(jù)不適合在前端暴露,因此在服務(wù)器端進(jìn)行導(dǎo)出可以更好地控制和保護(hù)數(shù)據(jù)的安全。
- 復(fù)雜的業(yè)務(wù)邏輯:如果導(dǎo)出涉及復(fù)雜的業(yè)務(wù)邏輯、數(shù)據(jù)處理或數(shù)據(jù)查詢,使用服務(wù)器端的計(jì)算能力和數(shù)據(jù)庫(kù)訪問更合適。
- 跨平臺(tái)支持:如果需要支持多個(gè)前端平臺(tái)(如 Web、移動(dòng)應(yīng)用等),將導(dǎo)出功能放在服務(wù)器端可以提供一致的導(dǎo)出體驗(yàn)。
三、講解一下在前端做的導(dǎo)出
xlsx、xlsx-style
如果是只做表格導(dǎo)出:www.npmjs.com/package/xls…[4]
如果導(dǎo)出要包含樣式:www.npmjs.com/package/xls…[5]
import XLSX from"xlsx";
exportData() {
let tableName = '表格'
if(!getVal(this.dataList, 'length')){
this.$message.info("暫時(shí)數(shù)據(jù)");
return
}
// 處理頭部
let headers = {
"B2": "字段-B2",
"E2": "字段-E2",
}
const props = [ "B2", "E2" ]
let tmp_dataListFilter = [
{
"B2": "字段-B2",
"E2": "字段-E2",
},
{
"E2": "2",
"B2": "2",
}
]
tmp_dataListFilter.unshift(headers) // 將表頭放入數(shù)據(jù)源前面
let wb = XLSX.utils.book_new();
let contentWs = XLSX.utils.json_to_sheet(tmp_dataListFilter, {
skipHeader: true, // 是否忽略表頭,默認(rèn)為false
origin: "A2"// 設(shè)置插入位置
});
// /單獨(dú)設(shè)置某個(gè)單元格內(nèi)容
contentWs["A1"]={
t:"s",
v:tableName,
};
// /設(shè)置單元格合并!merges為一個(gè)對(duì)象數(shù)組,每個(gè)對(duì)象設(shè)定了單元格合并的規(guī)側(cè),
// /{s:{r:0,c:},e:{r:0,c:2}為一個(gè)規(guī)則,s:起始位置,e:結(jié)束位置,r:行,c:列
contentWs["!merges"]=[{ s:{r:0,c:0 },e:{r:0,c:props.length - 1 }}]
// 設(shè)置單元格的寬度
contentWs["!cols"] = []
props.forEach(p => contentWs["!cols"].push({wch: 35}))
XLSX.utils.book_append_sheet(wb,contentWs,tableName) // 表格內(nèi)的下面的tab
XLSX.writeFile(wb,tableName + ".xlsx"); // 導(dǎo)出文件名字
},
package.json
"xlsx": "^0.15.5",
"xlsx-style": "^0.8.13"
大概效果如下:
感覺前端導(dǎo)出也很容易。
哦哦,那你別高興太早。
四、需求升級(jí):?jiǎn)卧褚又泻图哟帧?/span>
xlsx
嘗試使用 xlsx-style 設(shè)樣式。
官方文檔:github.com/rockboom/Sh…[6]
文檔說給單元格設(shè)置 s 為對(duì)象
let contentWs = XLSX.utils.json_to_sheet(tmp_dataListFilter, {
skipHeader: true, // 是否忽略表頭,默認(rèn)為false
origin: "A2", // 設(shè)置插入位置
});
// /單獨(dú)設(shè)置某個(gè)單元格內(nèi)容
contentWs["A1"] = {
t: "s",
v: tableName,
s:{ // 這個(gè)是關(guān)鍵s
font: { bold: true },
alignment: { horizontal: 'center' }
}
};
發(fā)現(xiàn)設(shè)置無效。
有人說要改 xlsx、xlsx-style 源碼:
大概的意思是:修改 xlsx.extendscript.js、xlsx.full.min.js 更改文件變量。
發(fā)現(xiàn)仍然無效。
使用 binary 方式保存
- 首先保存的時(shí)候 type 要改成 binary 方式
- 保存的時(shí)候需要使用 xlsx-style 模塊
var writingOpt = {
bookType: 'xlsx',
bookSST: true,
type: 'binary'// <--- 1.改這里
}
/*
2. type:'array'改為'binary' 后因?yàn)橄旅娲a會(huì)報(bào)錯(cuò), 打不開excel
new Blob([wbout], { type: 'application/octet-stream' }
要文本轉(zhuǎn)換成數(shù)組緩存后再生成二進(jìn)制對(duì)象
*/
// 添加String To ArrayBuffer
function s2ab(s) {
var buf = newArrayBuffer(s.length);
var view = newUint8Array(buf);
for (var i = 0; i < s.length; i++) {
view[i] = s.charCodeAt(i) & 0xFF;
}
return buf;
}
let blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' })
FileSaver.saveAs(blob, exportName)
可以下載了。但依然樣式?jīng)]起作用。
使用 xlsx-style 模塊生成文件
首先安裝模塊
npm install xlsx-style
在項(xiàng)目里安裝報(bào)好多錯(cuò)誤直接強(qiáng)制安裝,不檢查依賴。
npm install xlsx-style -force
安裝完成后 找不到 cptable 模塊會(huì)報(bào)錯(cuò)
報(bào)錯(cuò)內(nèi)容如下:
./node_modules/xlsx-style/dist/cpexcel.js Module not found: Error: Can't resolve './cptable' in
這個(gè)問題在 vue.config.js 里配置一下就可以解決。
其他框架自己找找方法,反正只要不讓他報(bào)錯(cuò)能啟動(dòng)就行。
module.exports = {
// ...其他配置省略
configureWebpack: {
// ...其他配置省略
externals:{
'./cptable':'var cptable'
},
},
安裝完 xlsx-style 后改代碼
import XLSX2 from"xlsx-style"; // 1. 引入模塊
// 2. 使用`xlsx-style` 生成。 XLSX.write => XLSX2.write
var wbout = XLSX2.write(wb, writingOpt)
仍然無效。
總結(jié) xlsx
大概的意思是說:默認(rèn)不支持改變樣式,想要支持改變樣式,需要使用它的收費(fèi)版本。
本著勤儉節(jié)約的原則,很多人使用了另一個(gè)第三方庫(kù):xlsx-style[4] ,但是使用起來極其復(fù)雜,還需要改 node_modules 源碼,這個(gè)庫(kù)最后更新時(shí)間也定格在了 6 年前。還有一些其他的第三方樣式拓展庫(kù),質(zhì)量參差不齊。
使用成本和后期的維護(hù)成本很高,不得不放棄。
ExcelJS
ExcelJS 終于可以了
ExcelJS[5] 周下載量 450k,github star 9k,并且擁有中文文檔,對(duì)國(guó)內(nèi)開發(fā)者很友好。雖然文檔是以 README 的形式,可讀性不太好,但重在內(nèi)容,常用的功能基本都有覆蓋。
最近更新時(shí)間是 6 個(gè)月內(nèi),試用了一下,集成很簡(jiǎn)單,再加之文檔豐富,就選它了。
npm install exceljs
npm install file-saver // 下載到本地還需要另一個(gè)庫(kù):file-saver
基本操作
//導(dǎo)入ExcelJS
import ExcelJS from"exceljs";
//下載文件
download_file(buffer, fileName) {
console.log("導(dǎo)出");
let fileURL = window.URL.createObjectURL(new Blob([buffer]));
let fileLink = document.createElement("a");
fileLink.href = fileURL;
fileLink.setAttribute("download", fileName);
document.body.appendChild(fileLink);
fileLink.click();
}
導(dǎo)出 xlsx 表格的代碼
//下面是導(dǎo)出的函數(shù)
asyncexport() {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Sheet1");
//這里是數(shù)據(jù)列表
const data = [
{ id: 1, name: "艾倫", age: 20, sex: "男", achievement: 90 },
{ id: 2, name: "柏然", age: 25, sex: "男", achievement: 86 },
];
// 設(shè)置列,這里的width就是列寬
worksheet.columns = [
{ header: "序號(hào)", key: "id", width: 10},
{ header: "姓名", key: "name", width: 10 },
];
// 批量插入數(shù)據(jù)
data.forEach(item => worksheet.addRow(item));
// 寫入文件
const buffer = await workbook.xlsx.writeBuffer();
//下載文件
this.download_file(buffer, "填報(bào)匯總.xlsx");
}
設(shè)置行高和列寬
列寬上面已經(jīng)有了,這里說明一下行高怎么設(shè)置
worksheet.getRow(2).height = 30;
合并單元格
worksheet.mergeCells("B1:C1");
自定義表格樣式
//設(shè)置樣式表格樣式,font里面設(shè)置字體大小,顏色(這里是argb要注意),加粗
//alignment 設(shè)置單元格的水平和垂直居中
const B1 = worksheet.getCell('B1')
B1.font = { size: 20, color:{ argb: 'FF8B008B' }, bold: true }
B1.alignment = { horizontal: 'center', vertical: 'middle' }
ExcelJS 實(shí)戰(zhàn)
import ExcelJS from"exceljs";
//下載文件
download_file(buffer, fileName) {
console.log("導(dǎo)出");
let fileURL = window.URL.createObjectURL(new Blob([buffer]));
let fileLink = document.createElement("a");
fileLink.href = fileURL;
fileLink.setAttribute("download", fileName);
document.body.appendChild(fileLink);
fileLink.click();
},
async exportClick() {
const loading = this.$loading({
lock: true,
text: "數(shù)據(jù)導(dǎo)出中,請(qǐng)耐心等待!",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)",
});
this.tableData = [
{ a: 1, b:2 }
]
const enterpriseVisitsColumns = [
{
prop: "a",
label: "銀行",
},
{
prop: "b",
label: "企業(yè)數(shù)",
}
]
// 表格數(shù)據(jù):this.tableData
if (!(this.tableData && this.tableData.length)) {
this.$message.info("暫無數(shù)據(jù)");
loading.close();
return;
}
let tableName = this.tableName; // 表格名
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet(tableName);
const props = enterpriseVisitsColumns();
//這里是數(shù)據(jù)列表
const data = this.tableData;
// 設(shè)置列,這里的width就是列寬
let arr = [];
props.forEach((p) => {
arr.push({
header: p.label,
key: p.prop,
width: 25,
});
});
worksheet.columns = arr;
// 插入一行到指定位置,現(xiàn)在我往表格最前面加一行,值為表名
const rowIndex = 1; // 要插入的行位置
const newRow = worksheet.insertRow(rowIndex);
// 設(shè)置新行的單元格值
newRow.getCell(1).value = tableName; // 值為表名
// 批量插入數(shù)據(jù),上面插一條,這里就是從第二行開始加
data.forEach((item) => worksheet.addRow(item));
//設(shè)置樣式表格樣式,font里面設(shè)置字體大小,顏色(這里是argb要注意),加粗
//alignment 設(shè)置單元格的水平和垂直居中
// const B1 = worksheet.getCell("B1");
// B1.font = { size: 20, color: { argb: "FF8B008B" }, bold: true };
// B1.alignment = { horizontal: "center", vertical: "middle" };
// 合并單元格,就是把A1開始到J1的單元格合并
worksheet.mergeCells("A1:J1");
// 批量設(shè)置所有表格數(shù)據(jù)的樣式
worksheet.eachRow((row, rowNumber) => {
let size = rowNumber == 1 ? 16 : rowNumber == 2 ? 12 : "";
//設(shè)置表頭樣式
row.eachCell((cell) => {
cell.font = {
size,
// color:{ argb: 'FF8B008B' },
bold: true,
};
cell.alignment = { horizontal: "center", vertical: "middle" };
});
//設(shè)置所有行高
row.height = 30;
});
// 寫入文件
const buffer = await workbook.xlsx.writeBuffer();
//下載文件
this.download_file(buffer, tableName + ".xlsx");
loading.close();
},
后記
導(dǎo)出功能并不是說都是前端或者后端實(shí)現(xiàn),要具體情況,具體分析,我相信哪方都可以做,但誰適合做,這個(gè)才是我們需要去思考的。
就如同我們項(xiàng)目中,該例子后面也是前端實(shí)現(xiàn)的,大數(shù)據(jù)分頁(yè)當(dāng)然還是得后端同學(xué)來實(shí)現(xiàn)較好。
該文章在 2024/3/30 0:40:03 編輯過