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

news2024/12/25 12:17:50

背景

在日常前端开发中,经常会有把页面的 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.coloradmin.cn/o/2172755.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【项目经验分享】深度学习自然语言处理技术毕业设计项目案例定制

以下毕业设计是与深度学习自然语言处理&#xff08;NLP&#xff09;相关的毕业设计项目案例&#xff0c;涵盖文本分类、生成式模型、语义理解、机器翻译、对话系统、情感分析等多个领域&#xff1a; 实现案例截图&#xff1a; 基于深度学习的文本分类系统基于BERT的情感分析系…

RabbitMQ 界面管理说明

1.RabbitMQ界面访问端口和后端代码连接端口不一样 界面端口是15672 http://localhost:15672/ 后端端口是 5672 默认账户密码登录 guest 2.总览图 3.RabbitMq数据存储位置 4.队列 4.客户端消费者连接状态 5.队列运行状态 6.整体运行状态

【SpringCloud】环境和工程搭建

环境和工程搭建 1. 案例介绍1.1 需求1.2 服务拆分服务拆分原则服务拆分⽰例 1. 案例介绍 1.1 需求 实现⼀个电商平台(不真实实现, 仅为演⽰) ⼀个电商平台包含的内容⾮常多, 以京东为例, 仅从⾸⻚上就可以看到巨多的功能 我们该如何实现呢? 如果把这些功能全部写在⼀个服务…

基于大数据技术的足球数据分析与可视化系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码 精品专栏&#xff1a;Java精选实战项目…

java计算机毕设课设—博网即时通讯软件(附源码、文章、相关截图、部署视频)

这是什么系统&#xff1f; 资源获取方式在最下方 java计算机毕设课设—博网即时通讯软件(附源码、文章、相关截图、部署视频) 博网即时通讯软件是一款功能丰富的实时通讯平台&#xff0c;旨在提升用户的交流效率与体验。在服务器端&#xff0c;该软件支持运行监控功能&#…

Java中的Junit、类加载时机与机制、反射、注解及枚举

目录 Java中的Junit、类加载时机与机制、反射、注解及枚举 Junit Junit介绍与使用 Junit注意事项 Junit其他注解 类加载时机与机制 类加载时机 类加载器介绍 获取类加载器对象 双亲委派机制和缓存机制 反射 获取类对象 获取类对象的构造方法 使用反射获取的构造方法创建对象 获…

无环SLAM系统集成后端回环检测模块(loop):SC-A-LOAM以及FAST_LIO_SLAM

最近在研究SLAM目标检测相关知识&#xff0c;看到一篇论文&#xff0c;集成了SC-A-LOAM作为后端回环检测模块&#xff0c;在学习了论文相关内容后决定看一下代码知识&#xff0c;随后将其移植&#xff0c;学习过程中发现我找的论文已经集成了回环检测模块&#xff0c;但是我的另…

Postgresql源码(136)syscache/relcache 缓存及失效机制

相关 《Postgresql源码&#xff08;45&#xff09;SysCache内存结构与搜索流程分析》 0 总结速查 syscache&#xff1a;缓存系统表的行。通用数据结构&#xff0c;可以缓存一切数据&#xff08;hash dlist&#xff09;。可以分别缓存单行和多行查询。 syscache使用CatCache数…

Hadoop框架及应用场景说明

Hadoop是一个开源的分布式系统基础架构。由多个组件组成&#xff0c;组件之间协同工作&#xff0c;进行大规模数据集的存储和处理。 本文将探讨Hadoop的架构以及应用场景。 一Hadoop框架 Hadoop的核心组件包含&#xff1a; 1. Hadoop分布式文件系统&#xff08;HDFS&#xff…

windows10使用bat脚本安装前后端环境之msyql5.7安装配置并重置用户密码

首先需要搞清楚msyql在本地是怎么安装配置、然后在根据如下步骤编写bat脚本&#xff1a; 思路 1.下载mysql5.7 zip格式安装包 2.新增data文件夹与my.ini配置文件 3.初始化数据库 4.安装mysql windows服务 5.启动并修改root密码&#xff08;新增用户初始化授予权限&#xff09…

浅拷贝深拷贝

&#x1f4cb;目录 &#x1f4da;引入&#x1f4da;浅拷贝&#x1f4d6;定义&#x1f4d6;实现方式&#x1f4d6;特点 &#x1f4da;深拷贝&#x1f4d6; 定义&#x1f4d6;实现方式&#x1f4d6;特点 &#x1f4da;拓展&#x1f4d6;Object类✈️toString()方法✈️equals()方…

预防工作场所的违规政策

违规政策是指未经管理层制定或批准的工作场所政策。 它们也可能直接违反公司政策。如果管理不善&#xff0c;这些政策可能会对您的业务产生负面影响。 最常见的流氓政策来源是 试图绕过现有政策框架的员工&#xff0c;或 经理们未经高层领导批准&#xff0c;擅自制定自己的…

《凡人歌》中的IT职业启示录

《凡人歌》是由中央电视台、正午阳光、爱奇艺出品&#xff0c;简川訸执导&#xff0c;纪静蓉编剧&#xff0c;侯鸿亮任制片&#xff0c;殷桃、王骁领衔主演&#xff0c;章若楠、秦俊杰、张哲华、陈昊宇主演的都市话题剧 &#xff0c;改编自纪静蓉的小说《我不是废柴》。该剧于2…

基础漏洞——SSTI(服务器模板注入)

一.SSTI&#xff08;服务器模板注入&#xff09;的出现,框架漏洞 首先可以通过SSTI(Server-Side Template Injection)从名字可以看出即是服务器端模板注入。有些框架一般都采用MVC的模式。 用户的输入先进入Controller控制器&#xff0c;然后根据请求类型和请求的指令发送给对…

解决 Java 中由于 parallelStream 导致的死锁

并发性是软件开发的福音&#xff0c;也是祸根。通过并行处理提高性能的承诺与错综复杂的挑战相伴而生&#xff0c;例如臭名昭著的死锁。死锁是多线程编程世界中的隐患&#xff0c;它甚至可以使最强大的应用程序陷入瘫痪。它描述了两个或多个线程永远被阻塞&#xff0c;相互等待…

FOC矢量控制

目录 前言一、FOC简介1.1 FOC是什么1.2 FOC框图介绍 二、FOC坐标变换2.1 电流采集2.2 Clarke变换2.3 Park变换 三、闭环控制3.1 电流环控制3.3 速度环控制3.4 位置环控制 四、SVPWM原理4.1 空间矢量合成4.2 SVPWM法则4.3 MOS开关方式4.4 矢量作用时间 前言 本文主要介绍无刷直流…

未来医疗:从医技数字化2.0到全局变革

斯蒂芬•申弗&#xff08;Stephen C. Schimpff&#xff09;的《医疗大趋势&#xff1a;明日医学》被认为是全球第一本系统介绍未来医疗的权威著作。在书中&#xff0c;作者认为基因组学、手术技术革新、干细胞、数字医疗等关键技术将驱动医疗变革的发生&#xff0c;全面提升人类…

OpenAI o1-preview:详细分析

OpenAI 终于打破沉默&#xff0c;发布了万众期待的 “o1-preview”。其中有很多内容值得解读。 作为一家以 LLM 为生的人工智能初创公司&#xff0c;我们想知道这个新模型的性能如何&#xff0c;以及它能如何帮助我们改进产品。 因此&#xff0c;我花了一整天的时间来实验这个…

(JAVA)队列 和 符号表 两种数据结构的实现

1. 队列 1.1 队列的概述 队列是一种基于先进先出&#xff08;FIFO&#xff09;的数据结构&#xff0c;是一种只能在一端进行插入&#xff0c;在另一端进行删除操作的特殊线性表。 它按照先进先出的原则存储数据&#xff0c;先进入的数据&#xff0c;在读取时先被读出来 1.2 …

蓝桥杯【物联网】零基础到国奖之路:十二. TIM

蓝桥杯【物联网】零基础到国奖之路:十二. TIM 第一节 理论知识第二节 cubemx配置 第一节 理论知识 STM32L071xx器件包括4个通用定时器、1个低功耗定时器&#xff08;LPTIM&#xff09;、2个基本定时器、2个看门狗定时器和SysTick定时器。 通用定时器&#xff08;TIM2、TIM3、…