最近遇到了一個(gè)需求:
1.前端根據(jù)后端提供的圖片url批量下載,并壓縮成zip包
2.根據(jù)后端提供的數(shù)據(jù)批量生成pdf文件,并壓縮成zip包
本文記錄了這個(gè)需求的實(shí)現(xiàn)過程
1.批量下載圖片并打包壓縮
思路
因?yàn)閳D片是靜態(tài)資源,根據(jù)url直接獲得二進(jìn)制數(shù)據(jù),然后壓縮成zip格式就行了
利用到的插件:JSZip和FileSaver
將圖片的鏈接和名字改成下面這種形式的數(shù)組,注意
1.title不能重復(fù),否則不會(huì)正常生成期望數(shù)量的圖片
2.title要帶正確的圖片格式后綴,否則會(huì)導(dǎo)致文件打不開
數(shù)組參考如下:
const downArray = [
{
title: '小貓.jpg',
href: 'https://pic.com/pic1'
},
{
title: '小豬.jpg',
href: 'https://pic.com/pic2'
}
]
// 批量下載并壓縮圖片
this.downImg(downArray);
downImg和getImgArrayBuffer函數(shù)代碼如下:
//通過url 轉(zhuǎn)為blob格式的數(shù)據(jù)
getImgArrayBuffer(url) {
let _this = this;
return new Promise((resolve, reject) => {
//通過請(qǐng)求獲取文件blob格式
let xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", url, true);
xmlhttp.responseType = "blob";
xmlhttp.onload = function () {
if (this.status == 200) {
resolve(this.response);
} else {
reject(this.status);
}
};
xmlhttp.send();
});
},
// imgDataUrl 數(shù)據(jù)的url數(shù)組
downImg(imagesParams) {
let _this = this;
let zip = new JSZip();
let cache = {};
let promises = [];
_this.title = "正在加載壓縮文件";
for (let item of imagesParams) {
const promise = _this.getImgArrayBuffer(item.href).then((data) => {
// 下載文件, 并存成ArrayBuffer對(duì)象(blob)
zip.file(item.title, data, { binary: true }); // 逐個(gè)添加文件
cache[item.title] = data;
});
promises.push(promise);
}
Promise.all(promises)
.then(() => {
zip.generateAsync({ type: "blob" }).then((content) => {
_this.title = "正在壓縮";
// 生成二進(jìn)制流
FileSaver.saveAs(
content,
`服務(wù)確認(rèn)函-${dayjs(new Date()).format("YYYY-MM-DD")}`
); // 利用file-saver保存文件 自定義文件名
_this.title = "壓縮完成";
});
})
.catch((res) => {
_this.$message.error("文件壓縮失敗");
});
},
2.批量生成PDF并打包壓縮
2.1. 踩坑記錄
本來是打算生成PDF這一步是前端根據(jù)接口返回的list數(shù)據(jù)生成的,但是嘗試了幾個(gè)方案,都是有很大的缺點(diǎn)
方案一:html2canvas+jspdf
npm install --save html2canvas // 頁面轉(zhuǎn)圖片
npm install jspdf --save // 圖片轉(zhuǎn)pdf
網(wǎng)上出現(xiàn)最多的就是這個(gè)前端導(dǎo)出pdf的方法,先html2canvas 將html元素轉(zhuǎn)換為圖片,然后利用jspdf將圖片轉(zhuǎn)為pdf
版本一:(只能導(dǎo)出一頁圖片內(nèi)容)
const canvas2PDF = (canvas) => {
// 原版
let contentWidth = canvas.width;
let contentHeight = canvas.heigh
//a4紙的尺寸[595.28,841.89],html頁面生成的canvas在pdf中圖片的寬高
let imgWidth = 595.28;
let imgHeight = (592.28 / contentWidth) * contentHeigh
// 第一個(gè)參數(shù): l:橫向 p:縱向
// 第二個(gè)參數(shù):測(cè)量單位("pt","mm", "cm", "m", "in" or "px")
let pdf = new jsPDF("p", "pt"
pdf.addImage(
canvas.toDataURL("image/jpeg", 1.0),
"JPEG",
0,
0,
imgWidth,
imgHeight
);
pdf.save("導(dǎo)出.pdf");
};
html2canvas(this.$refs.pdf).then(function (canvas) {
// page.appendChild(canvas);
canvas2PDF(canvas);
});
版本二:(可以導(dǎo)出多頁)
var contentWidth = canvas.width;
var contentHeight = canvas.heig
//一頁pdf顯示html頁面生成的canvas高度;
var pageHeight = (contentWidth / 592.28) * 841.89;
//未生成pdf的html頁面高度
var leftHeight = contentHeight;
//頁面偏移
var position = 0;
//a4紙的尺寸[595.28,841.89],html頁面生成的canvas在pdf中圖片的寬高
var imgWidth = 595.28;
var imgHeight = (592.28 / contentWidth) * contentHeig
var pageData = canvas.toDataURL("image/jpeg", 1.
var pdf = new jsPDF("", "pt", "a4
//有兩個(gè)高度需要區(qū)分,一個(gè)是html頁面的實(shí)際高度,和生成pdf的頁面高度(841.89)
//當(dāng)內(nèi)容未超過pdf一頁顯示的范圍,無需分頁
if (leftHeight < pageHeight) {
pdf.addImage(pageData, "JPEG", 0, 0, imgWidth, imgHeight);
} else {
while (leftHeight > 0) {
pdf.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight);
leftHeight -= pageHeight;
position -= 841.89;
//避免添加空白頁
if (leftHeight > 0) {
pdf.addPage();
}
}
}
但是上面的兩個(gè)版本都會(huì)很大的局限性,因?yàn)槭菍?dǎo)出圖片的原因,導(dǎo)致導(dǎo)出的文件體積很大,而且清晰度不高,版本二即使支持分頁,經(jīng)過試驗(yàn),最多也就只能導(dǎo)出40頁,再多就全是空白頁了。另外還有很大的缺點(diǎn),就是導(dǎo)出的內(nèi)容無法復(fù)制,這就失去了pdf文件的意義了。
有個(gè)vue插件vue-html2pdf也可以實(shí)現(xiàn)將html元素導(dǎo)出為pdf,但也是圖片形式的,和html2canvas+jspdf效果是一樣的,這里就不再贅述
可以直接使用jspdf插件將html元素導(dǎo)出為pdf文件,試過試驗(yàn),發(fā)現(xiàn)是可以的,但是存在著更巨大的缺陷,首先是中文會(huì)亂碼,這個(gè)貌似是可以全局寫入字體解決的,但是導(dǎo)出的pdf內(nèi)容每個(gè)元素都需要通過坐標(biāo)去手動(dòng)定位,這個(gè)太麻煩了,示例代碼:
var pdf = new jsPDF("", "pt", "a4");
pdf.text('hello world !', 10, 10);
pdf.text('哈哈哈 !', 40, 40);
pdf.text('哈哈哈 !', 80, 80);
pdf.text('哈哈哈 !', 160, 160);
pdf.text('哈哈哈 !', 220, 220);
pdf.addPage();
pdf.save("content.pdf");
導(dǎo)出效果如下:

2.2. 解決方案
至此,只能優(yōu)先考慮尋找后端解決方案了,最終確定的方案是后端通過Freemarker模板引擎生成pdf(體積小,文字形式的),模板語法的常見用法記錄如下
<#-- 如果值為null/空,則設(shè)置為空值 -->
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Freemarker</title>
<style type="text/css">
#all {
width: 600px;
font-family: SimSun;
<#-- 使文檔在pdf頁面居中 --> margin: auto;
}
table {
width: 100%;
height: auto;
<#-- @@提醒@@:此處必須指定字體,不然不識(shí)別中文 --> font-family: SimSun;
text-align: center;
<#-- table中單元格自動(dòng)換行 --> table-layout: fixed;
word-wrap: break-word;
}
</style>
</head>
<body>
<div id="all">
<#if model.businessTypes??>
<#if model.businessTypes?size??>
<table border="1" cellspacing="0" cellpadding="0" align="left" style="margin-top: 10px;">
<#list model.businessTypes as typeI>
<tr>
<#list typeIs as typeInner>
<td colspan="2">
<#if typeInner !='Empty'>
<span class="label">${ typeInner?split(':')[0] }:</span>
<span class="content">${ typeInner?split(':')[1] }</span>
</#if>
</td>
</#list>
</tr>
</#list>
</table>
</#if>
</#if>
</div>
</body>
</html>
和后端接口約定好,瀏覽器通過二進(jìn)制流數(shù)據(jù)的形式拿到pdf數(shù)據(jù),然后將剛才下載打包圖片的downImg函數(shù)改造成downBlob函數(shù),把拿到的二進(jìn)制數(shù)據(jù)直接塞進(jìn)promise,然后生成壓縮包,需求順利實(shí)現(xiàn)。
const blob = new Blob([res], { type: "application/pdf" });
// 從響應(yīng)頭的content-disposition獲取文件名稱
const fileName = decodeURI(resALL.headers["content-disposition"]).split('filename=')[1] || `檢查報(bào)告-${dayjs(new Date()).format("YYYY-MM-DD")}.pdf`;
this.PDFOriginObject.push({
title: fileName,
href: blob,
});
_this.downBlob(_this.PDFOriginObject);
downBlob(imagesParams) {
let _this = this;
let zip = new JSZip();
let cache = {};
let promises = [];
_this.title = "正在加載壓縮文件";
for (let item of imagesParams) {
const promise = new Promise((resolve) => {
resolve();
}).then(() => {
zip.file(item.title, item.href, { binary: true });
cache[item.title] = item.href;
});
promises.push(promise);
}
Promise.all(promises)
.then(() => {
zip.generateAsync({ type: "blob" }).then((content) => {
_this.title = "正在壓縮";
// 生成二進(jìn)制流
FileSaver.saveAs(
content,
`質(zhì)檢報(bào)告-${dayjs(new Date()).format("YYYY-MM-DD")}`
); // 利用file-saver保存文件 自定義文件名
_this.title = "壓縮完成";
});
})
.catch((res) => {
_this.$message.error("文件壓縮失敗");
});
},
注意點(diǎn)一:
1.前端接口請(qǐng)求的responseType設(shè)置為blob才能拿到二進(jìn)制流數(shù)據(jù)
2.如果是想要在拿到的流數(shù)據(jù)的時(shí)候還要拿到后端返回的返回文件名稱,則要在響應(yīng)攔截器service.interceptors.response中將整個(gè)response都返給接口進(jìn)行處理,這樣才能拿到響應(yīng)頭的content-disposition
3.后端在content-disposition就算返回了文件名字,但是因?yàn)榘踩珕栴}我們只能在network中看到,無法通過js在請(qǐng)求頭中拿到,需要后端增加一段代碼
response.setHeader("Access-Control-Expose-Headers","Content-Disposition");
4.后端添加的content-disposition是經(jīng)過URLEncoder處理的,js還要通過decodeURI處理一下
5.拿pdf流數(shù)據(jù)的接口因?yàn)樾枰喸冋?qǐng)求,接口只有在能生成pdf數(shù)據(jù)的時(shí)候返回blob二進(jìn)制流數(shù)據(jù),其他還約定了一些json格式的返回?cái)?shù)據(jù),需要前端進(jìn)行處理,而這時(shí)候因?yàn)榻涌诘膔esponseType已經(jīng)設(shè)置成了blob,js已經(jīng)無法拿到正常的json了,我用的解決方案是根據(jù)size進(jìn)行判斷,因?yàn)閖son格式的返回的數(shù)據(jù)轉(zhuǎn)換為blob后size最多只有幾百,而pdf文件的size最少有一兩萬
if (res.size < 1000) {
const reader = new FileReader();
reader.readAsText(res, "utf-8");
reader.onload = function () {
_this.resObj = JSON.parse(reader.result);
if (_this.resObj && _this.resObj.code === 0) {
....some code
}else if (_this.resObj && _this.resObj.code === 99999){
....some code
}
}
}else{
....some code
}
6.本次需求的批量導(dǎo)出因?yàn)楹臅r(shí)太長(zhǎng),所以是彈出新窗口進(jìn)行處理的
let routeUrl = this.$router.resolve({
path: "/taskInfo/task/patrol/record/pdfexport",
});
window.open(routeUrl.href, "_blank");
新窗口自定義一個(gè)特殊的loading圖
import { Loading } from "element-ui";
this.loadingInstance = Loading.service({
text: "導(dǎo)出中,請(qǐng)等待",
spinner: "el-icon-loading",
background: "rgba(255, 255, 255, 0.8)",
fullscreen: true,
});
this.loadingInstance.close();