让前端飞前端启示录零基础转行前端

Vue中如何批量导出文件(图片/PDF)并打包压缩成ZIP

2022-11-06  本文已影响0人  前端辉羽

最近遇到了一个需求:
1.前端根据后端提供的图片url批量下载,并压缩成zip包
2.根据后端提供的数据批量生成pdf文件,并压缩成zip包
本文记录了这个需求的实现过程

1.批量下载图片并打包压缩

思路
因为图片是静态资源,根据url直接获得二进制数据,然后压缩成zip格式就行了
利用到的插件:JSZip和FileSaver
将图片的链接和名字改成下面这种形式的数组,注意
1.title不能重复,否则不会正常生成期望数量的图片
2.title要带正确的图片格式后缀,否则会导致文件打不开
数组参考如下:

const downArray = [
  {
    title: '小猫.jpg',
    href: 'https://pic.com/pic1'
  },
  {
    title: '小猪.jpg',
    href: 'https://pic.com/pic2'
  }
]
// 批量下载并压缩图片
this.downImg(downArray);

downImg和getImgArrayBuffer函数代码如下:

//通过url 转为blob格式的数据
getImgArrayBuffer(url) {
      let _this = this;
      return new Promise((resolve, reject) => {
        //通过请求获取文件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 数据的url数组
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对象(blob)
          zip.file(item.title, data, { binary: true }); // 逐个添加文件
          cache[item.title] = data;
        });
        promises.push(promise);
      }

      Promise.all(promises)
        .then(() => {
          zip.generateAsync({ type: "blob" }).then((content) => {
            _this.title = "正在压缩";
            // 生成二进制流
            FileSaver.saveAs(
              content,
              `服务确认函-${dayjs(new Date()).format("YYYY-MM-DD")}`
            ); // 利用file-saver保存文件  自定义文件名
            _this.title = "压缩完成";
          });
        })
        .catch((res) => {
          _this.$message.error("文件压缩失败");
        });
},

2.批量生成PDF并打包压缩

2.1. 踩坑记录

本来是打算生成PDF这一步是前端根据接口返回的list数据生成的,但是尝试了几个方案,都是有很大的缺点
方案一:html2canvas+jspdf

npm install --save html2canvas  // 页面转图片
npm install jspdf --save  // 图片转pdf

网上出现最多的就是这个前端导出pdf的方法,先html2canvas 将html元素转换为图片,然后利用jspdf将图片转为pdf
版本一:(只能导出一页图片内容)

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
  // 第一个参数: l:横向  p:纵向
  // 第二个参数:测量单位("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("导出.pdf");
};
html2canvas(this.$refs.pdf).then(function (canvas) {
  // page.appendChild(canvas);
  canvas2PDF(canvas);
});

版本二:(可以导出多页)

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
//有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)
//当内容未超过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();
    }
  }
}

但是上面的两个版本都会很大的局限性,因为是导出图片的原因,导致导出的文件体积很大,而且清晰度不高,版本二即使支持分页,经过试验,最多也就只能导出40页,再多就全是空白页了。另外还有很大的缺点,就是导出的内容无法复制,这就失去了pdf文件的意义了。

有个vue插件vue-html2pdf也可以实现将html元素导出为pdf,但也是图片形式的,和html2canvas+jspdf效果是一样的,这里就不再赘述

可以直接使用jspdf插件将html元素导出为pdf文件,试过试验,发现是可以的,但是存在着更巨大的缺陷,首先是中文会乱码,这个貌似是可以全局写入字体解决的,但是导出的pdf内容每个元素都需要通过坐标去手动定位,这个太麻烦了,示例代码:

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");

导出效果如下:


效果示意图.png

2.2. 解决方案

至此,只能优先考虑寻找后端解决方案了,最终确定的方案是后端通过Freemarker模板引擎生成pdf(体积小,文字形式的),模板语法的常见用法记录如下

<#-- 如果值为null/空,则设置为空值 -->
<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;
        <#-- @@提醒@@:此处必须指定字体,不然不识别中文  --> font-family: SimSun;
            text-align: center;
        <#-- table中单元格自动换行 --> 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>

和后端接口约定好,浏览器通过二进制流数据的形式拿到pdf数据,然后将刚才下载打包图片的downImg函数改造成downBlob函数,把拿到的二进制数据直接塞进promise,然后生成压缩包,需求顺利实现。

const blob = new Blob([res], { type: "application/pdf" });
// 从响应头的content-disposition获取文件名称
const fileName = decodeURI(resALL.headers["content-disposition"]).split('filename=')[1] || `检查报告-${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 = "正在压缩";
            // 生成二进制流
            FileSaver.saveAs(
              content,
              `质检报告-${dayjs(new Date()).format("YYYY-MM-DD")}`
            ); // 利用file-saver保存文件  自定义文件名
            _this.title = "压缩完成";
          });
        })
        .catch((res) => {
          _this.$message.error("文件压缩失败");
        });
},

注意点一:
1.前端接口请求的responseType设置为blob才能拿到二进制流数据
2.如果是想要在拿到的流数据的时候还要拿到后端返回的返回文件名称,则要在响应拦截器service.interceptors.response中将整个response都返给接口进行处理,这样才能拿到响应头的content-disposition
3.后端在content-disposition就算返回了文件名字,但是因为安全问题我们只能在network中看到,无法通过js在请求头中拿到,需要后端增加一段代码

response.setHeader("Access-Control-Expose-Headers","Content-Disposition");

4.后端添加的content-disposition是经过URLEncoder处理的,js还要通过decodeURI处理一下
5.拿pdf流数据的接口因为需要轮询请求,接口只有在能生成pdf数据的时候返回blob二进制流数据,其他还约定了一些json格式的返回数据,需要前端进行处理,而这时候因为接口的responseType已经设置成了blob,js已经无法拿到正常的json了,我用的解决方案是根据size进行判断,因为json格式的返回的数据转换为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.本次需求的批量导出因为耗时太长,所以是弹出新窗口进行处理的

let routeUrl = this.$router.resolve({
  path: "/taskInfo/task/patrol/record/pdfexport",
});
window.open(routeUrl.href, "_blank");

新窗口自定义一个特殊的loading图

import { Loading } from "element-ui";

this.loadingInstance = Loading.service({
  text: "导出中,请等待",
  spinner: "el-icon-loading",
  background: "rgba(255, 255, 255, 0.8)",
  fullscreen: true,
});

this.loadingInstance.close();
上一篇下一篇

猜你喜欢

热点阅读