Vue中如何批量導(dǎo)出文件(圖片/PDF)并打包壓縮成ZIP

最近遇到了一個(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)出效果如下:


效果示意圖.png

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();
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容