前端實現PDF導出功能

基於Vue框架開發,前端實現PDF文件導出功能,有多種實現方案,現大概介紹下如何使用html2canvas + jspdf來實現。html

需求簡介

點擊「導出按鈕」,導出並下載指定內容的PDF文件。前端

需求分析

調研了下,決定採用html2canvas + jspdf插件來實現,大體原理是使用html2canvas將dom內容截圖生成爲圖片格式,而後使用jspdf插件將圖片轉換成pdf格式。vue

代碼實現

1. 定義須要生成PDF文件的模板內容

由於項目是基於Vue開發,因此生成PDF文件的模板內容基於vue語法就行,只要最終能拿到節點就能夠。例如 export.vue:node

<template>
  <div class="m-export">
    <div class="logo">
      <img crossOrigin="Anonymous" :src="getPath(logoPath)" />
    </div>
    <div class="f-tac f-fw0">{{ customerName }}</div>
    <div class="date s-fc3">{{ currDate }}</div>
  </div>
</template>

<script>
export default {
  props: ['logoPath', 'customerName', 'currDate']
  methods: {
    getPath(url) {
      let str = Date.now();
      return url.indexOf('?') > -1 ? `${url}&v=${str}` : `${url}?v=${str}`;
    }
  }
};
</script>

<style lang="less" scoped>
.m-export {
  position: fixed;
  left: 99999px;
  top: 99999px;
  width: 1000px;
  min-height: 1000px;
}
</style>
複製代碼

2. 調用生成PDF文件的方法

模板定義好後,此時點擊導出按鈕,執行導出邏輯。此處用了定時器,是由於調用pdf生成方法前須要保證模板內容渲染完成,節點內容若是沒渲染好會致使生成的pdf內容缺失。canvas

import { getPdf } from '@/config/htmlToPdf';

setTimeout(() => {
  getPdf({
    element: this.$refs.export.$el,  // pdf模板節點:上面第一步中的模板內容節點
    title: '文件導出',  // pdf文件名
    isFullPage: true,   // pdf尺寸:true爲不分頁的長文件,false爲A4分頁的文件
    canvasOptions: {
      width: 1000   // 畫布尺寸
    }
  })
}, 500);
複製代碼

3. 核心getPdf() 方法定義

首先安裝html2canvas插件和jspdf插件。若是包拉不下來,可以使用腳本外鏈的方式引用。此處jspdf最新5.1.3的包經過yarn下載失敗,先經過腳本的方式加載,注意外鏈腳本最好放在本地cdn上,避免由於網絡問題或其餘緣由致使腳本加載失敗。跨域

html2Canvas參數選項中allowTaint: true和useCORS: true選項,兩者互斥。當定義了allowTaint: true後,跨域的不安全的圖會被加載,可是出於安全性的考慮,html2Canvas內部不少方法會被禁用,好比toDataURL();當定義了useCORS: true選項後,圖片響應頭裏須要加上容許跨域的參數,客戶端圖片img標籤裏也須要加上crossOrigin="Anonymous",容許被跨域加載。瀏覽器

大體代碼以下:緩存

import html2Canvas from 'html2canvas';

const appendJs = () => {
  let script = document.createElement('script');
  script.src = 'https://unpkg.com/jspdf@1.5.3/dist/jspdf.min.js';
  let node = document.getElementsByTagName('script')[0];
  node.parentNode.insertBefore(script, node);
};
appendJs();

const getPdf = ({ element, title, isFullPage, canvasOptions = {} }) => {
  return new Promise((resolve, reject) => {
    // 定義canvas畫布的屬性,避免生成的pdf文件尺寸不統一
    let { scale = 2, width, height } = canvasOptions;
    width = width || element.clientWidth;
    height = height || element.clientHeight;
    element.ownerDocument.defaultView.devicePixelRatio = scale;
    element.ownerDocument.defaultView.innerWidth = width;
    element.ownerDocument.defaultView.innerHeigth = height;
    
    html2Canvas(element, {
      // allowTaint: true,
      useCORS: true,
      scale,
      width,
      height
    })
      .then(function(canvas) {
        let contentWidth = canvas.width;
        let contentHeight = canvas.height;
        let pageData = canvas.toDataURL('image/jpeg', 1.0);
        let PDF;
        let imgWidth;
        let imgHeight;

        if (isFullPage) {
          // 全屏長圖
          imgWidth = (contentWidth / scale) * 0.75;
          imgHeight = (contentHeight / scale) * 0.75;
          PDF = new jsPDF('', 'pt', [imgWidth, imgHeight]);  // [imgWidth, imgHeight] 爲PDF寬高
          PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight);
        } else { 
          // A4紙截圖
          imgWidth = 595;
          imgHeight = (imgWidth / contentWidth) * contentHeight;
          let position = 0;
          let pageHeight = (contentWidth / imgWidth) * 842; // A4一頁的高度
          PDF = new jsPDF('', 'pt', 'a4');
          if (contentHeight < pageHeight) {
            PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight);
          } else {
            while (contentHeight > 0) {
              PDF.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight);
              contentHeight -= pageHeight;
              position -= 842;
              if (contentHeight > 0) {
                PDF.addPage();
              }
            }
          }
        }
        PDF.save(title + '.pdf');  // 保存pdf文件
        resolve();
      })
      .catch(err => reject(err));
  });
};

export { getPdf };
複製代碼

踩坑總結

1. pdf文件中,內容較少時寬度會被截斷

解決辦法:在模板樣式中定義最小高度,把容器撐開。安全

.m-export {
  width: 1000px;
  min-height: 1000px;
}
複製代碼

2. pdf中圖片不顯示,報跨域問題

測試時發現圖片會報跨域問題,若是使用跨域的資源畫到canvas中,而且資源沒有使用CORS去請求,canvas會被認爲是被污染了, canvas能夠正常展現,可是沒辦法使用toDataURL()或者toBlob()導出數據。bash

圖片已經經過img標籤加載過,瀏覽器默認會緩存下來,下次再請求會直接返回緩存的圖片,若是緩存中的圖片不是經過CORS請求或者響應頭中不存在Access-Control-Allow-Origin,都會致使報錯。

解決辦法:

  • 生成PDF文件時,可在圖片連接後加時間戳加載,避免使用本地緩存。
  • 經過在img標籤設置crossOrigin="Anonymous",在CORS請求時不會發送認證信息。
  • 在啓用CORS請求跨域資源時,服務端資源必須容許跨域,才能正常返回,因此還須要在服務端設置容許跨域的響應頭Access-Control-Allow-Origin等。

3. pdf尺寸不統一

設置pdf的尺寸。

element.ownerDocument.defaultView.devicePixelRatio = scale;
element.ownerDocument.defaultView.innerWidth = width;
element.ownerDocument.defaultView.innerHeigth = height;
複製代碼

4. 不可見元素的導出

意思是pdf文件的內容在頁面是不可見的,靜默導出。這種狀況下,可使用固定或絕對定位顯示頁面元素,注意不能使用display:none。

.m-export {
  position: fixed;
  left: 99999px;
  top: 99999px;
}
複製代碼
相關文章
相關標籤/搜索