前端导出PDF

118 阅读2分钟

背景

现在后台的导出主要分为两种情况:

  1. 纯服务端导出,这种适用于大量数据的excel导出
  2. 纯前端导出:导出格式:png、pdf、excel

为了统一前端的导出能力,统一维护。需要沉淀一套通用的前端导出方案。

为什么选择前端导出

我们最终选择了前端导出方案,主要基于以下考虑:

  • 所见即所得:用户导出的内容与当前页面展示高度一致
  • 开发部署更灵活:前端直接调用导出逻辑,无需服务端改造
  • 适用于轻量场景:当前导出页面结构相对简单,无需分页、大图或复杂模板
  • 避免服务端压力:全部在客户端完成,不增加后端压力或资源消耗

调研

路径:(前端dom + 获取服务端数据) => canvas => 转换(png、pdf、excel) => 导出

方案

代码

import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
import { useState } from 'react';

/**
 * 导出PDF
 * @param 导出后的文件名
 * @param 要导出的dom节点
 */

interface pdfParams {
  title?: string;
  eles: any;
  size?: Array<number>;
  orientation?: 'p' | 'l';
}

const useExportPDF = () => {
  const [schedule, setSchedule] = useState(0);
  const [isInprogress, setIsInprogress] = useState(false);

  const createPDF = async (params: pdfParams) => {
    const { title = '未命名', eles = null, size = [595.28, 841.89], orientation = 'p' } = params;

    if (!eles) return;
    setIsInprogress(true);

    const [a4Width, a4Height] = size;

    // 根据dpi放大,防止图片模糊
    const scale = window.devicePixelRatio > 1 ? window.devicePixelRatio : 2;

    // 设置为横向的a4
    const pdf = new jsPDF(orientation, 'pt', size, true);

    // 兼容多组dom
    for (let i = 0; i < eles.length; i++) {
      const currentEle = eles[i];

      // dom 宽高
      const { offsetWidth, offsetHeight } = currentEle;

      // 生成canvas
      const canvas = document.createElement('canvas');
      canvas.width = offsetWidth * scale;
      canvas.height = offsetHeight * scale;

      // 生成图片
      const pdfCanvas = await html2canvas(currentEle, {
        useCORS: true,
        canvas,
        scale,
        width: offsetWidth,
        height: offsetHeight,
        x: 0,
        y: 0,
      });

      const imgDataUrl = pdfCanvas.toDataURL(); // base64

      let position = 0;

      // 内容宽高
      let { width: contentWidth, height: contentHeight } = canvas;

      // 一页高
      var pageHeight = (contentWidth / a4Width) * a4Height;

      // 总高度
      let leftHeight = contentHeight;

      // 添加到pdf的高度
      let imgHeight = (a4Width / contentWidth) * contentHeight;

      if (pageHeight < a4Height) {
        pdf.addImage(imgDataUrl, 'png', 0, 0, a4Width, imgHeight);
      } else {
        while (leftHeight > 0) {
          pdf.addImage(imgDataUrl, 'png', 0, position, a4Width, imgHeight);
          leftHeight -= pageHeight;
          position -= a4Height;

          // 避免添加空白页
          if (leftHeight > 0) {
            pdf.addPage();
          }
        }
      }

      if (i < eles.length - 1) {
        pdf.addPage();
      }
      setSchedule(i + 1);
    }

    pdf.save(`${title}.pdf`);
    setIsInprogress(false);
  };

  return [createPDF, schedule, isInprogress];
};

export default useExportPDF;