DOM元素导出图片与PDF:多种方案对比与实现

news/2024/9/28 9:16:31 标签: pdf, 前端, javascript

背景

在日常前端开发中,经常会有把页面的 DOM 元素作为 PNG 或者 PDF 下载到本地的需求。例如海报功能,简历导出功能等等。在我们自家的产品「代码小抄」中,就使用了 html2canvas 来实现代码片段导出为图片:

是不是还行,大家如果想要分享代码片段,可以试试,非常好用。那有哪些方法可以实现下载 DOM 元素到本地呢?目前收集到的有:

  • 通过 html2canvas 、dom-to-image 等第三方库实现

  • 通过 Puppeteer 启动一个 node 服务实现

  • canvas 原生绘制

这些方式是真实项目会使用的方式,针对不同场景可以使用不同的方法,下面看一下每种方法如何实现、使用场景和优缺点,

方案 1 - html2canvas

html2canvas 专门用于解析 DOM 结构及其关联的 CSS 样式,进而将网页内容渲染为 Canvas 元素的 JavaScript 库,下面是下载元素为 PNG 的示例代码:

/**
 * 下载 dom 元素为图片
 * @param elementId DOM 元素id
 * @param fileName 下载图片的文件名
 * @returns
 */
export const downloadDOMElementAsImage = async (elementId: string, fileName: string) => {
  const element = document.getElementById(elementId) as HTMLElement;
  if (!element) return message.warn('无法找到 DOM 元素');
  try {
    // 将 DOM 元素转换为 canvas
    const canvas = await html2canvas(element, {
      useCORS: true,
      allowTaint: true,
      // 提高清晰度
      scale: 2,
      backgroundColor: 'transparent',
    });
    // 将 canvas 转换为数据 URL
    const dataUrl = canvas.toDataURL('image/png');
    // 创建一个临时的 <a> 元素,设置其 href 为数据 URL 并设置 download 属性
    const link = document.createElement('a');
    link.style.visibility = 'hidden';
    link.href = dataUrl;
    link.download = fileName;

    // 将 <a> 元素添加到 DOM,触发点击事件,然后从 DOM 中移除
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
  } catch (error: any) {
    message.error('无法将 DOM 元素转换为图片并下载', error);
  }
  element.style.transform = 'scale(1)';
};

通过 html2canvas ,我们封装了一个下载页面 DOM 为图片的方法,然后就可以很方便的调用方法进行页面元素的下载

使用场景

适用于需要将复杂的 DOM 结构(包括样式、背景图像、字体等)渲染为图片的场景。它可以捕获大部分 CSS 样式和 HTML 内容

优缺点

优点:

  • 使用非常简单,支持大多数 css

  • 内置跨域解决方案

  • 可以通过 ignoreElements 过滤指定 DOM,这在处理复杂 DOM 结构的时候非常有用

缺点:

  • 下载的图可能不清晰

  • 库比较大

  • 计算耗时,性能不好

  • 部分特殊的样式可能不支持,存在兼容性问题

方案 2 - dom-to-image

dom-to-image 是一个用 JavaScript 编写的库,可以将任意 DOM 节点转换为矢量(SVG)或光栅(PNG 或 JPEG)图像。它和 html2canvas 一样也是基于 canvas 封装的库。看一下生成 PNG 的示例代码:

/**
 * 下载 DOM 元素为高质量图片
 * @param elementId DOM 元素id
 * @param fileName 下载图片的文件名
 * @param sc 缩放比
 * @returns
 */
export const downloadDOMElementAsImage = async (elementId: string, fileName: string, sc = 3) => {
  const element = document.getElementById(elementId) as HTMLElement;
  if (!element || !window || !document) return message.warning("无法找到 DOM 元素");
  const messageKey = "loading";
  message.loading({
    content: "正在下载...",
    duration: 0,
    key: messageKey,
  });
  try {
    const clone = element.cloneNode(true) as HTMLElement;
    document.body.appendChild(clone);
    // 临时增加元素尺寸以提高分辨率
    const originalWidth = element.offsetWidth;
    const originalHeight = element.offsetHeight;
    const scale = sc; // 增加缩放因子以提高分辨率

    // 设置相对定位,zIndex 为 -1
    clone.style.position = "relative";
    // clone.style.zIndex = "-1";
    clone.style.transform = `scale(${scale})`;
    clone.style.transformOrigin = "top left";

    const dataUrl = await domtoimage.toPng(clone, {
      width: originalWidth * scale,
      height: originalHeight * scale,
      style: {
        transform: `scale(${scale})`,
        transformOrigin: "top left",
        width: `${originalWidth}px`,
        height: `${originalHeight}px`,
      },
      cacheBust: true,
      quality: 1,
      bgcolor: "transparent",
    });

    // 创建下载链接
    const link = document.createElement("a");
    link.href = dataUrl;
    link.download = fileName;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    message.destroy(messageKey);
    setTimeout(() => {
      document.body.removeChild(clone);
    }, 500);
  } catch (e: any) {
    message.destroy(messageKey);
    console.error("下载失败", e.message);
    message.error("下载失败: " + e.message);
  }
};

可以看到使用也非常简单,我们可以通过 sc 参数来控制下载图片的清晰度和大小。

使用场景

如果对项目大小有要求,希望文本排版支持度高,需要稳定的文字、图片渲染能力或者处理结构化数据的能力,可以使用 dom-to-image

优缺点

优点:

  • 库比较轻量

  • 适用于需要多格式导出的场景

缺点:

  • 需要手动处理跨域

方案 3.1 - Puppeteer

上面两种方法虽然可以在 web 端生成图片,但是如果需要:

  • 兼容多端,

  • 同时支持生成 PNG 和 PDF,并且要求非常清晰,

  • 兼容图片跨域

  • 兼容所有 css

  • 对项目体积有要求

那我们就可以使用 Puppeteer 来实现,它可以解决上面所有的问题。Puppeteer 是一个强大的 Node.js 库,用于控制 Chrome 或 Chromium 浏览器来帮我们生成想要的 PNG 或者 PDF,下面我们就使用 express + Puppeteer 来快速实现一个图片下载服务:

// node 服务 app.js 示例代码
import cors from "cors";
import express from "express";
import puppeteer from "puppeteer";

const app = express();
app.use(cors());

app.use(express.json());

app.get("/", (req, res) => {
  res.send("面试刷题,我只用面试鸭~");
});

app.post("/download", async (req, res) => {
  const { url, quality, format, filename, domId, type } = req.body;

  if (!url || !filename || !domId || !type) {
    return res.status(400).send("Missing required parameters");
  }

  try {
    // 启动浏览器
    const browser = await puppeteer.launch();
    // 新建一个页面
    const page = await browser.newPage();
    // 设置默认一分钟超时
    await page.setDefaultNavigationTimeout(60000);
    // 打开页面
    await page.goto(url, { waitUntil: "networkidle0" });

    if (type === "png") {
      // 等待元素加载
      await page.waitForSelector(`#${domId}`);

      // 等待元素加载
      await page.waitForSelector(`#${domId}`);

      // 截取指定元素的截图
      const element = await page.$(`#${domId}`);
      console.log(element, "element");

      const imageBuffer = await element.screenshot({
        type: "jpeg",
        quality: parseInt(quality), // 仅适用于 jpeg
        // omitBackground: true,
      });
      await browser.close();
      res.contentType("image/jpeg");
      res.attachment(filename + ".jpeg");
      // 返回二进制数据给前端
      res.send(Buffer.from(imageBuffer, "binary"));
    } else if (type === "pdf") {
      const pdf = await page.pdf({
        format: format || "A4",
        printBackground: true,
        pageRanges: "1-" + (req.body.pages || "1"),
      });
      res.contentType("application/pdf");
      res.attachment("resume.pdf");
      // 返回二进制数据给前端
      res.send(Buffer.from(pdf));
    } else {
      res.status(400).send("Invalid type");
    }

    await browser.close();
  } catch (error) {
    console.error(error);
    res.status(500).send("Internal Server Error");
  }
});

const PORT = 3001;
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

通过 Puppeteer 我们可以很方便的进行屏幕截取,因为它就是通过游览器内核来实现,所以它能完全还原展示效果

使用场景

如果需要下载图片和 PDF,对图片清晰度还有要求,页面元素还比较复杂,生成图片和 PDF 需要多端支持等等,可以使用 Puppeteer 来实现

优缺点

优点:

  • 高度还原视图:Puppeteer 使用的是无头 Chrome 浏览器,所以它生成的 PDF 和截图与用户在浏览器中看到的内容几乎完全一致

  • 丰富的 API: Puppeteer 提供了超多 API,基本可以解决所有遇到的问题,相关文档地址:puppeteer/docs/api.md at v1.5.0 · puppeteer/puppeteer · GitHub,非常多的 API

  • 支持最新的 css:由于 Puppeteer 使用的是 Chrome 浏览器,它支持所有现代 Web 特性,因此它在处理复杂网页时非常有优势

  • 跨域资源支持: Puppeteer 通常以无头模式运行,这种模式下浏览器跨域访问的限制会放宽

缺点:

  • 需要部署服务:Puppeteer 需要在服务器端运行,需要一个后端环境来支持它。

  • 资源消耗大: 由于 Puppeteer 启动的是一个完整的 Chrome 浏览器实例,因此它的资源消耗相对较大,可能会影响服务器的性能

  • 额外的学习成本:如果团队都对 Puppeteer 不了解,可能需要额外的学习和维护成本

但是如果使用 Puppeteer 去生产环境使用,可能还会有同时处理大量请求导致服务资源被消耗光,甚至导致下载服务奔溃的情况,这时候我们就可以使用 puppeteer-cluster 来实现请求队列, 使用队列系统来管理请求,确保同时只处理一定数量的请求,其他请求则排队等待

方案 3.2 - puppeteer-cluster

代码示例:

// 源码:https://github.com/chaseFunny/pdf-png-downloader
import cors from "cors";
import express from "express";
import { Cluster } from "puppeteer-cluster";

const app = express();
app.use(cors());
app.use(express.json());

let cluster;

// 初始化 cluster
async function initCluster() {
  cluster = await Cluster.launch({
    concurrency: Cluster.CONCURRENCY_CONTEXT,
    maxConcurrency: 2, // 最大并发数,可以根据服务器资源调整
    puppeteerOptions: {
      headless: true,
      args: ["--no-sandbox", "--disable-setuid-sandbox"],
    },
  });

  // 定义任务处理函数
  await cluster.task(async ({ page, data: { url, domId, type, quality, format, pages } }) => {
    await page.goto(url, { waitUntil: "networkidle0" });

    if (type === "png") {
      await page.waitForSelector(`#${domId}`);
      const element = await page.$(`#${domId}`);
      return await element.screenshot({
        type: "jpeg",
        quality: parseInt(quality),
      });
    } else if (type === "pdf") {
      return await page.pdf({
        format: format || "A4",
        printBackground: true,
        pageRanges: "1-" + (pages || "1"),
      });
    }
  });

  console.log("Cluster initialized");
}

initCluster();

app.get("/", (req, res) => {
  res.send("面试刷题,我只用面试鸭~");
});

app.post("/download", async (req, res) => {
  const { url, quality, format, filename, domId, type, pages } = req.body;

  if (!url || !filename || !domId || !type) {
    return res.status(400).send("Missing required parameters");
  }

  try {
    const result = await cluster.execute({ url, domId, type, quality, format, pages });

    if (type === "png") {
      res.contentType("image/png");
      res.attachment(filename + ".png");
      res.send(Buffer.from(result));
    } else if (type === "pdf") {
      res.contentType("application/pdf");
      res.attachment(filename + ".pdf");
      res.send(Buffer.from(result));
    } else {
      res.status(400).send("Invalid type");
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Internal Server Error");
  }
});

const PORT = 3001;
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

// 优雅关闭
process.on("SIGINT", async () => {
  console.log("Closing cluster...");
  await cluster.close();
  process.exit(0);
});

我们使用 puppeteer-cluster 创建了一个浏览器实例池,有如下优点:

  • 可以更有效地处理并发请求,它会自动把接受的请求加入队列,保证所有请求都会进行处理。

  • 将 PDF 和 PNG 生成的逻辑移到了 cluster.task 中,这样可以重用浏览器实例,提高效率

  • 设置了最大并发数(maxConcurrency),可以根据服务器资源进行调整,避免资源耗尽

注意:在生产环境,我们可能需要在 puppeteerOptions 的 executablePath 设置具体的 chrome 路径,保证服务能找到 chorme

方案 4 - canvas 原生绘制

参考代码:

//获取canvas元素
var canvas = document. getElementById( 'poster')
var ctx = canvas getContext ('2d')
// 设置canvas宽高
canvas.width = 600
canvas.height = 800
// 绘制背景
ctx.fillStyle = '#ff6600'
ctx. fillRect(0, 0, canvas.width, canvas.height)
// 绘制文字
ctx. font = 'bold 48px Arial'
ctx. fillStyle = '#ffffff•
ctx.textAlign = 'center'
ctx. fillText ('*HEd', canvas.width / 2, 120)
ctx-font = '24px Arial'
ctx.fillText('这里是副标题
canvas. width / 2, 180)
// 绘制图片
var img = new Image()
img. onload = function ()
{
ctx. drawImage(img, 100, 250, 400, 400)
}
img.src ='图片地址'

canvas 虽然高性能,但是工作量大,一般生产环境不会使用

总结

在实际开发中,面对不同场景我们会使用不同的方案,那我们公司的线上项目为例:在我们的「面试鸭」和「编程导航」的生成海报功能都是使用了 html2canvas 来生成海报,因为它要比 Puppeteer 快,能够让用户更快拿到海报图,在「老鱼简历」中,我们使用 Puppeteer 来导出简历,这样导出的简历和看到的更加一致,并且清晰度更加高。

上面的代码都在仓库:GitHub - chaseFunny/pdf-png-downloader,还提供了简单的页面方便大家体验调试


http://www.niftyadmin.cn/n/5680799.html

相关文章

vue仿chatGpt的AI聊天功能--大模型通义千问(阿里云)

vue仿chatGpt的AI聊天功能–大模型通义千问&#xff08;阿里云&#xff09; 通义千问是由阿里云自主研发的大语言模型&#xff0c;用于理解和分析用户输入的自然语言。 1. 创建API-KEY并配置环境变量 打开通义千问网站进行登录&#xff0c;登陆之后创建api-key&#xff0c;右…

C++-list使用学习

###list&#xff08;链表&#xff09;是C里面的一种容器&#xff0c;底层是双向的&#xff1b; 这就决定了它的迭代器使用的场景和能够使用的算法&#xff1b;双向&#xff08;例如list&#xff09;不能像随机&#xff08;例如vector&#xff09;那样用迭代器任意加上几去使用&…

Harbor使用

文章目录 1、上传镜像1.1、在Harbor上创建一个项目1.2、docker添加安全访问权限1.3、推送docker镜像到该项目中1.3.1、登录到Harbor1.3.2、给镜像重新打一个标签1.3.3、推送镜像到Harbor中 2、拉取镜像2.1、先删掉原来的镜像2.2、执行拉取命令 1、上传镜像 需求&#xff1a;将…

[论文精读]Polarized Graph Neural Networks

论文网址&#xff1a;Polarized Graph Neural Networks | Proceedings of the ACM Web Conference 2022 英文是纯手打的&#xff01;论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误&#xff0c;若有发现欢迎评论指正&#xff01;文章偏向于…

java日志门面之JCL和SLF4J

文章目录 前言一、JCL1、JCL简介2、快速入门3、 JCL原理 二、SLF4J1、SLF4J简介2、快速入门2.1、输出动态信息2.2、异常信息的处理 3、绑定日志的实现3.1、slf4j实现slf4j-simple和logback3.2、slf4j绑定适配器实现log4j3.2、Slf4j注解 4、桥接旧的日志框架4.1、log4j日志重构为…

InternVL 微调实践

任务 follow 教学文档和视频使用QLoRA进行微调模型&#xff0c;复现微调效果&#xff0c;并能成功讲出梗图. 复现过程 参考教程部署&#xff1a;https://github.com/InternLM/Tutorial/blob/camp3/docs/L2/InternVL/joke_readme.md 训练 合并权重&&模型转换 pyth…

【微服务即时通讯系统】——etcd一致性键值存储系统、etcd的介绍、etcd的安装、etcd使用和功能测试

文章目录 etcd1. etcd的介绍1.1 etcd的概念 2. etcd的安装2.1 安装etcd2.2 安装etcd客户端C/C开发库 3. etcd使用3.1 etcd接口介绍 4. etcd使用测试4.1 原生接口使用测试4.2 封装etcd使用测试 etcd 1. etcd的介绍 1.1 etcd的概念 Etcd 是一个基于GO实现的 分布式、高可用、一致…

逻辑回归(下): Sigmoid 函数的发展历史

背景 闲来无事翻了一下之前买的一个机器学习课程及之前记录的网络笔记&#xff0c;发现遇到公式都是截图&#xff0c;甚至是在纸上用笔推导的。重新整理一遍之前逻辑回归函数的学习笔记&#xff0c;主要是为了玩一下 LaTex 语法&#xff0c;写公式挺有意思的。 整理之前三篇笔…