添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

将html网页下载成pdf, 使用jspdf插件, 但是jspdf插件的中文介绍文档太少了, 记录踩坑过程: 首先我的实现方式是用htm2canvas将网页转换为canvas, 然后再将canvas转换为img, 然后再用jspdf的addImage将图片截取成pdf, 但是这样有一个问题, 就是可能会截取到任何地方, 比如一行字的中间, 或者一个表格行的中间, 这样就造成了问题, 一开始我是打算将页面按着a4纸的宽高来设计的, 就是计算好a4纸上面可以放下多少内容, 防止截断一些不可分割的元素, 但是很快发现, 因为我的页面上数据是服务端返回的, 根本没办法定死, 所以只能找别的办法, 后来找到jspdf的api, html(老版本是addHtml), 但发现下载下来的pdf是乱码, 查文档才发现jspdf虽然挺强大的, 但是对中文不友好, 如果想解决乱码问题, 就要自己导入字体文件, 但是网上的文章说字体文件非常大, 可能会造成内存溢出的问题, 而且设置字体文件还挺麻烦的, 只能另寻他法, 最后发现了一个大神的封装代码, 原理是遍历dom节点, 去控制在哪里分割, 下面把封装的js文件贴出来, 给大家参考

import jsPDF from 'jspdf';
import html2canvas from 'html2canvas';
import { Loading } from 'element-ui';
const A4_WIDTH = 592.28;
const A4_HEIGHT = 841.89;
// 将元素转化为canvas元素
// 通过 放大 提高清晰度
// width为内容宽度
async function toCanvas(element, width) {
  // canvas元素
  const canvas = await html2canvas(element, {
   // allowTaint: true, // 允许渲染跨域图片
    scale: window.devicePixelRatio * 2,  // 增加清晰度
    useCORS: true,// 允许跨域
    onrendered: function (canvas) {
      document.body.appendChild(canvas);
  // 获取canavs转化后的宽度
  const canvasWidth = canvas.width;
  // 获取canvas转化后的高度
  const canvasHeight = canvas.height;
  // 高度转化为PDF的高度
  const height = (width / canvasWidth) * canvasHeight;
  // 转化成图片Data
  const canvasData = canvas.toDataURL('image/jpeg', 1.0);
  //console.log(canvasData)
  return { width, height, data: canvasData };
 * 生成pdf(A4多页pdf截断问题, 包括页眉、页脚 和 上下左右留空的护理)
 * @param {Object} param
 * @param {HTMLElement} param.element - 需要转换的dom根节点
 * @param {number} [param.contentWidth=592.28] - 一页pdf的内容宽度,0-592.28
 * @param {string} [param.filename='document.pdf'] - pdf文件名
 * @param {HTMLElement} param.header - 页眉dom元素
 * @param {HTMLElement} param.footer - 页脚dom元素
 * @param {number} param.baseY - 距离PDF 页眉和页脚的间距, 留白留空
export async function outputPDF({ element, contentWidth = 592.28,
  footer, header, filename = "内容监测报告.pdf", baseY = 0 }) {
  if (!(element instanceof HTMLElement)) {
    return;
  const loading = Loading.service({
    lock: true,
    text: '正在下载pdf, 请稍后',
    spinner: 'el-icon-loadingIcon-small'
  // jsPDFs实例
  const pdf = new jsPDF({
    unit: 'pt',
    format: 'a4',
    orientation: 'p',
  // 一页的高度, 转换宽度为一页元素的宽度
  const { width, height, data } = await toCanvas(element, contentWidth);
  // 添加页脚
  async function addHeader(header, pdf, contentWidth) {
    const { height: headerHeight, data: headerData, width: hWidth } = await toCanvas(header, contentWidth);
    pdf.addImage(headerData, 'JPEG', 0, 0, contentWidth, headerHeight);
  // 添加页眉
  async function addFooter(pageNum, now, footer, pdf, contentWidth) {
    const newFooter = footer.cloneNode(true);
    newFooter.querySelector('.pdf-footer-page').innerText = now;
    newFooter.querySelector('.pdf-footer-page-count').innerText = pageNum;
    document.documentElement.append(newFooter);
    const { height: footerHeight, data: footerData, width: fWidth } = await toCanvas(newFooter, contentWidth);
    pdf.addImage(footerData, 'JPEG', 0, A4_HEIGHT - footerHeight, contentWidth, footerHeight)
  // 添加
  function addImage(_x, _y, pdf, data, width, height) {
    pdf.addImage(data, 'JPEG', _x, _y, width, height);
  // 增加空白遮挡
  function addBlank(x, y, width, height, pdf) {
    pdf.setFillColor(255, 255, 255);
    pdf.rect(x, y, Math.ceil(width), Math.ceil(height), 'F');
  // 页脚元素 经过转换后在PDF页面的高度
  let tfooterHeight = 0
  if (footer) {
    const { height } = await toCanvas(footer, contentWidth)
    tfooterHeight = height
  // 页眉元素 经过转换后在PDF的高度
  let theaderHeight = 0
  if (header) {
    const { height } = await toCanvas(header, contentWidth)
    theaderHeight = height
  // 距离PDF左边的距离,/ 2 表示居中 
  const baseX = (A4_WIDTH - contentWidth) / 2;        // 预留空间给左边
  // 出去页头、页眉、还有内容与两者之间的间距后 每页内容的实际高度
  const originalPageHeight = (A4_HEIGHT - tfooterHeight - theaderHeight - 2 * baseY);
  // 元素在网页页面的宽度
  const elementWidth = element.offsetWidth;
  // PDF内容宽度 和 在HTML中宽度 的比, 用于将 元素在网页的高度 转化为 PDF内容内的高度, 将 元素距离网页顶部的高度  转化为 距离Canvas顶部的高度
  const rate = contentWidth / elementWidth
  // 每一页的分页坐标, PDF高度, 初始值为根元素距离顶部的距离
  const pages = [rate * getElementTop(element)];
  // 获取元素距离网页顶部的距离
  // 通过遍历offsetParant获取距离顶端元素的高度值
  function getElementTop(element) {
    let actualTop = element.offsetTop;
    let current = element.offsetParent;
    while (current && current !== null) {
      actualTop += current.offsetTop;
      current = current.offsetParent;
    return actualTop;
  // 遍历正常的元素节点
  function traversingNodes(nodes) {
    for (let i = 0; i < nodes.length; ++i) {
      const one = nodes[i];
      // 需要判断跨页且内部存在跨页的元素
      const isDivideInside = one.classList && one.classList.contains('divide-inside');
      // 图片元素不需要继续深入,作为深度终点
      const isIMG = one.tagName === 'IMG' || (one.classList && one.classList.contains('as-img'));
      // table的每一行元素也是深度终点
      const isTableCol = one.classList && ((one.classList.contains('table-row')));
      // 特殊的富文本元素
      const isEditor = one.classList && (one.classList.contains('editor'));
      // 对需要处理分页的元素,计算是否跨界,若跨界,则直接将顶部位置作为分页位置,进行分页,且子元素不需要再进行判断
      let { offsetHeight } = one;
      // 计算出最终高度
      let offsetTop = getElementTop(one);
      // dom转换后距离顶部的高度
      // 转换成canvas高度
      const top = rate * (offsetTop)
      // 对于需要进行分页且内部存在需要分页(即不属于深度终点)的元素进行处理
      if (isDivideInside) {
        // 执行位置更新操作
        updatePos(rate * offsetHeight, top, one);
        // 执行深度遍历操作
        traversingNodes(one.childNodes);
      // 对于深度终点元素进行处理
      else if (isTableCol || isIMG) {
        // dom高度转换成生成pdf的实际高度
        // 代码不考虑dom定位、边距、边框等因素,需在dom里自行考虑,如将box-sizing设置为border-box
        updatePos(rate * offsetHeight, top, one);
      else if (isEditor) {
        // 执行位置更新操作
        updatePos(rate * offsetHeight, top, one);
        // 遍历富文本节点
        traversingEditor(one.childNodes)
      // 对于普通元素,则判断是否高度超过分页值,并且深入
      else {
        // 执行位置更新操作
        updateNomalElPos(top)
        // 遍历子节点
        traversingNodes(one.childNodes);
    return;
  // 对于富文本元素,观察所得段落之间都是以<p> / <img> 元素相隔,因此不需要进行深度遍历 (仅针对个人遇到的情况)
  function traversingEditor(nodes) {
    // 遍历子节点
    for (let i = 0; i < nodes.length; ++i) {
      const one = nodes[i];
      let { offsetHeight } = one;
      let offsetTop = getElementTop(one);
      const top = contentWidth / elementWidth * (offsetTop)
      updatePos(contentWidth / elementWidth * offsetHeight, top, one);
  // 普通元素更新位置的方法
  // 普通元素只需要考虑到是否到达了分页点,即当前距离顶部高度 - 上一个分页点的高度 大于 正常一页的高度,则需要载入分页点 
  function updateNomalElPos(top) {
    if (top - (pages.length > 0 ? pages[pages.length - 1] : 0) > originalPageHeight) {
      pages.push((pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight);
  // 可能跨页元素位置更新的方法
  // 需要考虑分页元素,则需要考虑两种情况
  // 1. 普通达顶情况,如上
  // 2. 当前距离顶部高度加上元素自身高度 大于 整页高度,则需要载入一个分页点
  function updatePos(eheight, top) {
    // 如果高度已经超过当前页,则证明可以分页了
    if (top - (pages.length > 0 ? pages[pages.length - 1] : 0) >= originalPageHeight) {
      pages.push((pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight);
    // 若 距离当前页顶部的高度 加上元素自身的高度 大于 一页内容的高度, 则证明元素跨页,将当前高度作为分页位置
    else if ((top + eheight - (pages.length > 0 ? pages[pages.length - 1] : 0) > originalPageHeight) && (top != (pages.length > 0 ? pages[pages.length - 1] : 0))) {
      pages.push(top);
  // 深度遍历节点的方法
  traversingNodes(element.childNodes);
  // 可能会存在遍历到底部元素为深度节点,可能存在最后一页位置未截取到的情况
  if (pages[pages.length - 1] + originalPageHeight < height) {
    pages.push(pages[pages.length - 1] + originalPageHeight);
  //console.log({ pages, contentWidth, width,height })
  // 根据分页位置 开始分页
  for (let i = 0; i < pages.length; ++i) {
    loading.text = `pdf共${pages.length}页,正在生成第${i + 1}页,请稍后`
    console.log(`pdf共${pages.length}页, 生成第${i + 1}页`)
    // message({message: `共${pages.length}页, 生成第${i + 1}页`, type: 'success'})
    // 根据分页位置新增图片
    addImage(baseX, baseY + theaderHeight - pages[i], pdf, data, width, height);
    // 将 内容 与 页眉之间留空留白的部分进行遮白处理
    addBlank(0, theaderHeight, A4_WIDTH, baseY, pdf);
    // 将 内容 与 页脚之间留空留白的部分进行遮白处理
    addBlank(0, A4_HEIGHT - baseY - tfooterHeight, A4_WIDTH, baseY, pdf);
    // 对于除最后一页外,对 内容 的多余部分进行遮白处理
    if (i < pages.length - 1) {
      // 获取当前页面需要的内容部分高度
      const imageHeight = pages[i + 1] - pages[i];
      // 对多余的内容部分进行遮白
      addBlank(0, baseY + imageHeight + theaderHeight, A4_WIDTH, A4_HEIGHT - (imageHeight), pdf);
    // 添加页眉
    if (header) {
      await addHeader(header, pdf, A4_WIDTH)
    // 添加页脚
    if (footer) {
      await addFooter(pages.length, i + 1, footer, pdf, A4_WIDTH);
    // 若不是最后一页,则分页
    if (i !== pages.length - 1) {
      // 增加分页
      pdf.addPage();
  loading.close()
  return pdf.save(filename)

其中有设置如果元素有class=table-row代表这个元素是表格行, 不能分割, 还有如果是img标签或者是class=as-img,表示这个元素是一个图片或者像图片一样的元素, 不能分割

非常棒, 感谢原作者的分享, 在这里贴上原作者的demo: gitee.com/liuloser/ou…

还有作者的掘金文章地址: juejin.cn/post/713837…

私信