web前端pdf導出

做一個項目需要實現瀏覽器端pdf的導出功能,在此記錄一下整個實現過程以及遇到的一些坑:)

當然,解決這個問題有以下幾個步驟:

  1. 確定要導出的dom元素
  2. 將dom元素轉化成canvas( 使用html2canvas庫)
  3. 將canvas轉化成圖片jpeg,png等都可以
  4. 將圖片導出pdf (使用jspdf庫)

確定要導出的dom元素

如果是原生寫法可以直接使用document.getElementById來獲取,如果是用vue或者react可以設置要ref, 此處我使用的是react框架

  const reportRef = useRef();
  // canvasEle 爲需要獲取的dom元素
  const canvasEle = reportRef.current

將dom元素轉化成canvas

這裏其實有幾個坑:

  1. 如果導出的dom元素裏存在svg,則需要首先把所有dom中的svg先轉化成canvas,否則svg部分會變成空白,svg轉canvas 可以使用canvg庫來操作
// 此處封裝了一個轉化函數
import Canvg from 'canvg';
const svg2Canvas = (domEle: HTMLElement, cb: (node: any) => void) => {
  // 在臨時div上將svg都轉換成canvas,並刪除原有的svg節點
  const svgElem = domEle.querySelectorAll("svg");

  svgElem.forEach(async (node: any) => {
    const { parentNode } = node;
    if (cb && typeof cb === 'function') {
      cb(node)
    }

    const svg = node.outerHTML.trim();
    const canvas = document.createElement('canvas');
    const width = parentNode.clientWidth
    const height = parentNode.clientHeight
    const ctx = canvas.getContext('2d');
    const v = Canvg.fromString(ctx, svg,
      {
        ignoreMouse: true,   // 不理會鼠標事件
        ignoreAnimation: true,  // 不理會動畫
      });

    // 保持canvas大小和原來的svg一致
    v.resize(width, height);

    // 將svg替換成canvas
    v.render();

	// 拷貝位置的樣式
    if (node.style.position) {
      canvas.style.position += node.style.position;
      canvas.style.left += node.style.left;
      canvas.style.top += node.style.top;
    }
    parentNode.removeChild(node);
    parentNode.appendChild(canvas);
  });
  return domEle
}
// svg2Canvas 使用方式
const tempEle = svg2Canvas(
  cloneEle,
  (node) => {
    const circleTrailColor = '#f5f5f5'
    const circleTrailPath = '#20486c'
    /* 
    此處注意!!! 
    我們這裏是使用到了ant-design的Progress進度條組件,
    Progress中設置的顏色是通過class來設置的。
    這會導致顏色無法導出,
    因爲canvas是不會帶class的樣式的,
    我們需要把顏色寫到stroke纔會正常顯示。
    */
    if (node.className.baseVal === 'ant-progress-circle') {
      node.querySelector('.ant-progress-circle-trail').style.stroke = circleTrailColor
      node.querySelector('.ant-progress-circle-path').style.stroke = circleTrailPath
    } else {
      /*
      此處的處理是由於我們使用了阿里的字體圖標,字體圖標使用的是use標籤如下所示:
      <use xlink:href="#iconfendianbu"></use>
      這個use標籤下面的元素實際上是引用symbol模板的元素
      <symbol id="iconfendianbu" viewBox="0 0 1024 1024"></symbol>
      這會導致無法正常導出元素,所以我們需要把use標籤轉化成真正的svg元素
      注意在轉化的時候一定要使用cloneNode()方法,否則symbol下的svg元素會被掛載上去,會影響到其他使用該圖標的地方無法正常顯示
	 */
      const use = node.querySelector('use')
      if (use) {
        node.style.fill = circleTrailPath
        node.setAttribute('viewBox', '0 0 1024 1024')
        const href = use.getAttribute('xlink:href')
        const id = href.replace('#', '')
        const svgSymbolEle = document.getElementById(id)
        node.removeChild(use)
        const path = svgSymbolEle.querySelectorAll('path')
        path.forEach(pathNode => {
          const clonePathNode = pathNode.cloneNode(true)
          node.appendChild(clonePathNode)
        })
      }
    }
  })
         // 設置放大倍數,處理畫布導出圖片模糊的問題
          const scale = 2.5
          const contentWidth = parseInt(tempEle.scrollWidth)
          const contentHeight = parseInt(tempEle.scrollHeight)
          // 獲取到canvas元素
          const canvas = await html2canvas(tempEle, {
            dpi: window.devicePixelRatio * scale,
            scale, // 放大倍數
            width: contentWidth,
            heigth: contentHeight,
            useCORS: true // 【重要】開啓跨域配置
          })

將canvas轉化成圖片jpeg

這一步驟比較簡單,只是在之前生成的canvas上調用toDataURL()方法即可

 const pageData = canvas.toDataURL('image/jpeg')

將圖片導出pdf

此處需要使用jspdf庫來操作

// 封裝了單頁導出的方法,直接調用即可
 exportPdfOnePage({
   pageData,
   contentWidth,
   contentHeight,
   fileName,
   scale
 })
// 這裏使用單例模式異步導入jspdf庫,直接導入會導致項目啓動報錯
const importJsPDF = () => {
  let jspdf: any = null
  return async () => {
    if (!jspdf) {
      const { default: JsPDF } = await import('jspdf')
      jspdf = JsPDF
    }
    return jspdf
  }
}
export const getJsPDF = importJsPDF()
// 單頁導出psf
export const exportPdfOnePage = async (options: any) => {
  const {
    pageData,
    contentWidth,
    contentHeight,
    fileName,
    scale
  } = options

  // 設置pdf的尺寸,pdf要使用pt單位 已知 1pt/1px = 0.75   pt = (px/scale)* 0.75
  const pdfX = (contentWidth) * 0.75
  const pdfY = (contentHeight + 100) * 0.75// 爲底部留白

  // 設置內容圖片的尺寸,img是pt單位
  const imgX = pdfX;
  const imgY = (contentHeight) * 0.75; // 內容圖片這裏不需要留白的距離

  // 初始化jspdf 第一個參數方向:默認''時爲縱向,第二個參數設置pdf內容圖片使用的長度單位爲pt,第三個參數爲PDF的大小,單位是pt
  const JsPDF = await getJsPDF()
  const pdf = new JsPDF('', 'pt', [pdfX, pdfY])
  pdf.addImage(pageData, 'jpeg', 0, 0, imgX, imgY)
  pdf.save(`${fileName}.pdf`)
}

// 多頁導出pdf
export const exportPdfMutiPage = async (options: any) => {
  const {
    canvas,
    contentWidth,
    contentHeight,
    fileName,
  } = options

  const a4Width = 592.28
  const a4Height = 841.89

  // 一頁pdf顯示html頁面生成的canvas高度;
  const pageHeight = contentWidth / a4Width * a4Height
  // 未生成pdf的html頁面高度
  let leftHeight = contentHeight
  // pdf頁面偏移
  let position = 0
  // a4紙的尺寸[595.28,841.89],html頁面生成的canvas在pdf中圖片的寬高
  const imgWidth = a4Width
  const imgHeight = a4Width / contentWidth * contentHeight
  const pageData = canvas.toDataURL('image/jpeg', 1.0)
  const JsPDF = await getJsPDF()
  const pdf = new JsPDF('', 'pt', 'a4')
  // 有兩個高度需要區分,一個是html頁面的實際高度,和生成pdf的頁面高度(841.89)
  // 當內容未超過pdf一頁顯示的範圍,無需分頁
  if (leftHeight < pageHeight) {
    pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth + 20, imgHeight)
  } else {
    while (leftHeight > 0) {
      pdf.addImage(pageData, 'JPEG', 0, position, imgWidth + 20, imgHeight)
      leftHeight -= pageHeight
      position -= 841.89
      // 避免添加空白頁
      if (leftHeight > 0) {
        pdf.addPage()
      }
    }
  }
  pdf.save(`${fileName}.pdf`)
}

其他的坑

  1. 在所有的代碼都寫完之後發現一個問題,就是偶爾導出的pdf上半部分會留空白,截圖不完整,而且很多時候留的空白距離還不一樣,後來才發現,其實是滾動條的問題,因爲html2canvas截圖的時候,是根據body從上往下截圖的,所以後來做了一個處理,把滾動條跳動到最頂端
 // 解決 canvas 截圖頂部可能留空白的問題
 window.pageYoffset = 0;
 document.documentElement.scrollTop = 0;
 document.body.scrollTop = 0;
 canvasEle.scrollLeft = 0;

完整功能代碼

其中使用的svg2Canvas, exportPdfOnePage這兩個函數,可以在上文中找到代碼,此處不重複粘貼。

    try {
      // 增加頁面渲染所用的時間
      setTimeout(async () => {
        message.info('正在導出,請稍後。')
        const timestamp = (new Date()).getTime();
        const canvasEle: HTMLElement = reportRef.current
        if (!canvasEle) return

        // 解決 canvas 截圖頂部可能留空白的問題
        window.pageYoffset = 0;
        document.documentElement.scrollTop = 0;
        document.body.scrollTop = 0;
        canvasEle.scrollLeft = 0;
        const cloneEle = canvasEle.cloneNode(true)
        cloneEle.style.position = 'absolute'
        cloneEle.style.top = '2000px'
        cloneEle.style['max-width'] = '1600px'

        try {
          const fileName = `導出pdf_${timestamp}`
          document.body.appendChild(cloneEle);

          const tempEle = svg2Canvas(
            cloneEle,
            (node) => {
              const circleTrailColor = '#f5f5f5'
              const circleTrailPath = '#20486c'
              if (node.className.baseVal === 'ant-progress-circle') {
                // eslint-disable-next-line no-param-reassign
                node.querySelector('.ant-progress-circle-trail').style.stroke = circleTrailColor
                // eslint-disable-next-line no-param-reassign
                node.querySelector('.ant-progress-circle-path').style.stroke = circleTrailPath
              } else {
                const use = node.querySelector('use')
                if (use) {
                  node.style.fill = circleTrailPath
                  node.setAttribute('viewBox', '0 0 1024 1024')
                  const href = use.getAttribute('xlink:href')
                  const id = href.replace('#', '')
                  const svgSymbolEle = document.getElementById(id)
                  node.removeChild(use)
                  const path = svgSymbolEle.querySelectorAll('path')
                  path.forEach(pathNode => {
                    const clonePathNode = pathNode.cloneNode(true)
                    node.appendChild(clonePathNode)
                  })
                }
              }
            })
            
          // 設置放大倍數,處理畫布導出圖片模糊的問題
          const scale = 2.5
          const contentWidth = parseInt(tempEle.scrollWidth)
          const contentHeight = parseInt(tempEle.scrollHeight)
          const canvas: HTMLCanvasElement = await html2canvas(tempEle, {
            dpi: window.devicePixelRatio * scale,
            scale, // 放大倍數
            width: contentWidth,
            heigth: contentHeight,
            useCORS: true // 【重要】開啓跨域配置
          })

          document.body.removeChild(cloneEle);
          const pageData = canvas.toDataURL('image/jpeg')
          // 導出pdf文件
          exportPdfOnePage({
            pageData,
            contentWidth,
            contentHeight,
            fileName,
            scale
          })
        } catch (err) {
          document.body.removeChild(cloneEle);
          console.log(err)
        }
        setExportLoading(false)
      }, 500)
    } catch (err) {
      console.log(err)
      setExportLoading(false)
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章