关于html2pdf.js的使用记录

1.背景

某天产品同学和我说要搞一个页面的pdf导出

我知道前端有成熟的库可以把页面导出成pdf的,但是我却和产品说不如想把这东西抛给后端同学,当然html的模板由前端提供,然后后端帮忙查询业务数据,再往前端提供的模板把对应的变量替换成字段的值,再导出成pdf应该是可以的,而且导出pdf的压力不由浏览器来承担肯定效果会更好

结果后端同学说这个东西前端弄就可以了吧,而且他不会弄

看来这个坑还是得前端的弟弟来填

2.需求

用户往表单,表格填写数据,点击预览按钮会出现弹窗,弹窗的内容就是整个pdf页面的内容,点解弹窗内的下载按钮即可把当前弹窗的内容下载成pdf

2.1 思路

我上社区查询了一下,目前前端使用的pdf导出主要有几个主流的库 (没提及到的主要是我没安装实际使用过不代表不是主流哈)

1.html2pdf.js(这个库是html2canvas与jsPDF两个库的结合体,它集成了两个库的全部api)

2.pdfmake (这个库是用类似canvas的语法来编写页面的pdf,学习成本很高,而且里面的中文字体需要自己特定把字体下载到本地或者项目里面,当加一种语言就要自己手动加一种,维护起来成本很高)

所以这里就暂时使用html2pdf.js这个库来完成这个需求

3.问题

3.1 在生成pdf的过程中,浏览器处于假死

因为页面元素实在太多了,我看了一下源码这个库需要我传一个元素id作为生成pdf的内容,而这个id是从我整个页面元素里面遍历递归取的,当我页面存在很多元素嵌套的情况下(尤其微前端),他要生成pdf的时间就越长,然后浏览器在操作dom的时候是占主线程的,所以浏览器在这段期间会假死

解决方法:

使用html2canvas的api(ignoreElements)可以忽略元素的方法,这样就可以让我们提前去识别一些不用遍历的元素,直接让这个库去遍历和我们传id最快捷的遍历路径,极大缩短浏览器假死时间

3.2 pdf的内容含有图片,导出pdf后图片会消失

解决方法:

使用html2canvas的api(useCORS)设置为true即可

3.3 产品说如果我现在不需要直接导出pdf的文件,我想要把pdf导出成文件流,然后再把整个文件流上传到oss(云盘)里呢

当我以为直接使用html2pdf的output,默认让pdf输出为pdf对象,然后再把pdf对象转成blob流再转成file流,再调用上传oss的接口即可实现,然而当我上传成功后,从云盘下载下来的这份pdf文件,完全是空白,我在想中间肯定有转换没对,肯定是在pdf对象转blob流这里,缺少charCode转换,然后我试了一下遍历pdf对象里面的内容全部转成 charCodeAt,再上传就对了

正确的流程:

output转成pdf对象 -> new Unit8Array -> 遍历整个pdf对象转换每一个字符为Unicode编码 -> 把转换之后的Unicode编码串转成blob流 -> 把blob流转成file流 -> 调用接口上传到oss

4.代码

直接上代码

4.1 编写 main.vue

因为项目有其他触发器需要使用整个导出按钮的逻辑,所以封装成一个hooks,这样可以在不同业务情况下进行复用

vue 复制代码
<script setup>
import { usePdf } from './hooks';

const props = defineProps({
  /**
   * 元素id
   */
  elementId: {
    type: String,
    default: null,
  },
  /**
   * 文件名称
   */
  filename: {
    type: String,
    default: 'download',
  },
  /**
   * 页面断点
   */
  pagebreak: {
    type: Object,
    default: () => {
      return {
        avoid: ['.avoid-break'],
      };
    },
  },
  /**
   * HTML 类型
   */
  htmlType: {
    type: String,
    default: 'button',
    validator(value) {
      return ['text', 'button'].includes(value);
    },
  },
  /**
   * md-button/md-link 类型
   */
  type: {
    type: String,
    default: '',
  },
});

const { loading, download } = usePdf({
  filename: props.filename,
  pagebreak: props.pagebreak,
});

/**
 * 点击下载按钮时触发
 */
function click() {
  const pdfEl = document.querySelector(`#${props.elementId}`);

  download(pdfEl);
}
</script>

<template>
  <button
    :type="type"
    :loading="loading"
    @click="click"
  >
    {{ loading ? '生成中' : '下载' }}
  </button>
</template>

4.2 编写 hooks.js

包含下载/获取文件流功能

js 复制代码
import { ref, nextTick } from 'vue';
import html2pdf from 'html2pdf.js';

/**
 * 生成pdf钩子
 * @param {object} root0 - 对象
 * @param  {string} root0.filename -文件名
 * @param {object} root0.pagebreak - 分页
 * @returns {object} - loading, download
 */
export function usePdf({ filename, pagebreak }) {
  const loading = ref(false);

  const options = {
    margin: [10, 10, 10, 10],
    filename,
    pagebreak,
    html2canvas: {
      scale: 2,
      useCORS: true,
      ignoreElements: (e) => {
        if (
          e.querySelector('.html2pdf__overlay') ||
          e.closest('.html2pdf__overlay') ||
          e === document.head ||
          e === document.body ||
          e.tagName === 'STYLE' ||
          e.tagName === 'LINK'
        ) {
          return false;
        }

        return true;
      },
    },
  };

  /**
   * 下载pdf
   * @param {object} pdfEl -dom元素
   */
  function download(pdfEl) {
    nextTick(() => {
      loading.value = true;
      html2pdf()
        .from(pdfEl)
        .set(options)
        .save()
        .then(() => {
          console.log('PDF生成完成');
          loading.value = false;

          return true;
        })
        .catch((error) => {
          console.error('PDF生成失败:', error);
          loading.value = false;

          return false;
        });
    });
  }

  /**
   * 生成文件流
   * @param {object} pdfEl -dom元素
   * @returns {object} - 文件流
   */
  async function getFile(pdfEl) {
    try {
      const res = await html2pdf().from(pdfEl).set(options).output();

      const pdfArray = new Uint8Array(res.length);

      for (let i = 0; i < res.length; i += 1) {
        pdfArray[`${i}`] = res.charCodeAt(i);
      }

      const blobType = { type: 'application/pdf;charset=utf-8' };

      const blob = new Blob([pdfArray], blobType);

      const file = new File([blob], `${filename}.pdf`, blobType);

      return file;
    } catch (e) {
      return e;
    }
  }

  return { loading, download, getFile };
}

4.3 编写index.js组件入口

把hooks导出出去方便任意触发器调用

vue 复制代码
import PdfDownload from './main.vue';
import { usePdf } from './hooks';

export { PdfDownload, usePdf };

4.4 使用组件

js 复制代码
<script setup>
import { PdfDownload, usePdf } from '@/components';

const filename = '测试pdf导出';

const pagebreak = {
  avoid: ['.avoid-break'],
};

const { getFile } = usePdf({ filename, pagebreak });

/**
 * 点击获取文件流按钮时触发
 */
async function onClick() {
  const pdfEl = document.querySelector('#pdfEl');

  const res = await getFile(pdfEl);

  console.log(res);
}
</script>

<template>
  <PdfDownload
    element-id="pdfEl"
    :filename="filename"
    :pagebreak="pagebreak"
  />
  <button @click="onClick">
    {{ '获取文件流' }}
  </button>
  <div id="pdfEl">
    {{ '我是pdf的内容' }}
  </div>
</template>

4.5 整体文件编排

5.结语

如果需求是生成一份50页以上并且size很大的pdf恐怕会放大这个假死的时间,恐怕要去文档里仔细看jspdf的api,除了分片段生成还需考虑如何重新整合这些分段的pdf文件流再进行下载,后续有时间我再研究下这个问题。

相关推荐
kite01212 小时前
浏览器工作原理06 [#]渲染流程(下):HTML、CSS和JavaScript是如何变成页面的
javascript·css·html
крон2 小时前
【Auto.js例程】华为备忘录导出到其他手机
开发语言·javascript·智能手机
coding随想4 小时前
JavaScript ES6 解构:优雅提取数据的艺术
前端·javascript·es6
年老体衰按不动键盘4 小时前
快速部署和启动Vue3项目
java·javascript·vue
小小小小宇4 小时前
一个小小的柯里化函数
前端
灵感__idea5 小时前
JavaScript高级程序设计(第5版):无处不在的集合
前端·javascript·程序员
小小小小宇5 小时前
前端双Token机制无感刷新
前端
小小小小宇5 小时前
重提React闭包陷阱
前端
小小小小宇5 小时前
前端XSS和CSRF以及CSP
前端
UFIT5 小时前
NoSQL之redis哨兵
java·前端·算法