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

背景

在日常前端开发中,经常会有把页面的 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 元素转换为 canvasconst canvas = await html2canvas(element, {useCORS: true,allowTaint: true,// 提高清晰度scale: 2,backgroundColor: 'transparent',});// 将 canvas 转换为数据 URLconst 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 为 -1clone.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.xdnf.cn/news/1548840.html

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

相关文章

【STM32】SPI回顾

一、定义 SPI是Motorola首先提出的全双工四线同步串行外围接口&#xff0c;采用主从模式&#xff08;Master-Slave&#xff09;架构。 二、单机与多机通信 4线SPI器件有四个信号&#xff1a;时钟(SPI CLK, SCLK)、主机输出从机输入(MOSI)、主机输入从机输出(MISO)、片选(CS/N…

简单理解C++在C的基础上的改变

1.C语言的一些不足 我们首先看下面用C语言实现栈 #include<stdio.h> #include<assert.h> #include<stdlib.h> typedef int StackDateType; typedef struct Stack {StackDateType* _ps;size_t _size;size_t _capacity; }Stack; void StackInit(Stack* ps) {…

Qt_网络编程

目录 1、Qt的UDP Socket 1.1 用Udp实现服务器 1.2 用Udp实现客户端 2、Qt的TCP Socket 2.1 用Tcp实现服务器 2.2 用Tcp实现客户端 3、Qt的HTTP 3.1使用Qt的HTTP 结语 前言&#xff1a; 网络协议是每个平台都必须遵守的&#xff0c;只是不同的平台所提供的网络API不…

工业缺陷检测——Windows 10本地部署AnomalyGPT工业缺陷检测大模型

0. 引言 在缺陷检测中&#xff0c;由于真实世界样本中的缺陷数据极为稀少&#xff0c;有时在几千甚至几万个样品中才会出现一个缺陷数据。因此&#xff0c;以往的模型只需在正常样本上进行训练&#xff0c;学习正常样品的数据分布。在测试时&#xff0c;需要手动指定阈值来区分…

实现语音合成的三种方法:HTML5 Web Speech 、speak-tts、百度语音合成

1. 使用HTML5 Web Speech API 1.1 使用方法 window.speechSynthesis 是HTML5 Web Speech API的一部分&#xff0c;是浏览器原生提供的文本转语音功能。它允许开发者在网页上通过JavaScript调用&#xff0c;将文本转换为语音进行播放。 https://developer.mozilla.org/zh-CN/d…

渗透测试--文件上传常用绕过方式

文件上传常用绕过方式 1.前端代码&#xff0c;限制只允许上传图片。修改png为php即可绕过前端校验。 2.后端校验Content-Type 校验文件格式 前端修改&#xff0c;抓取上传数据包&#xff0c;并且修改 Content-Type 3.服务端检测&#xff08;目录路径检测&#xff09; 对目…

LMDeploy 量化部署实践

任务 使用结合W4A16量化与kv cache量化的internlm2_5-1_8b-chat模型封装本地API并与大模型进行一次对话 复现过程 按照教材安装环境。https://github.com/InternLM/Tutorial/blob/camp3/docs/L2/LMDeploy/readme.md 使用LMDeploy部署原版的1.8b大模型&#xff0c;占用显存2…

Centos怎么执行脚本

方法一&#xff1a;切换到shell脚本所在的目录&#xff08;此时&#xff0c;称为工作目录&#xff09;执行shell脚本 cd /data/shell ./hello.sh 方法二&#xff1a;以绝对路径的方式去执行bash shell脚本 /data/shell/hello.sh 方法三&#xff1a;直接使用bash 或sh 来执行…

Kubernetes深入详解(一)

目录 第一部分 K8s概念和架构 1、k8s概述和特性 2、K8s架构组件 3、k8s核心概念 第二部分 从零搭建k8s集群 1、搭建k8s环境平台规划 2、服务器硬件配置要求 3、搭建k8s集群部署方式 (1) 基于客户端工具kubeadm 1、安装Docker 2、添加阿里云YUM软件源 3、安 装kubea…

代码随想录Day 58|拓扑排序、dijkstra算法精讲,题目:软件构建、参加科学大会

提示&#xff1a;DDU&#xff0c;供自己复习使用。欢迎大家前来讨论~ 文章目录 图论part08**拓扑排序精讲**题目&#xff1a;117. 软件构建拓扑排序的背景解题思路&#xff1a;模拟过程 **dijkstra&#xff08;朴素版&#xff09;精讲**题目&#xff1a;47. 参加科学大会解题思…

腾讯特效 SDK

腾讯云视立方腾讯特效 SDK&#xff08;Tencent Effect&#xff09;是音视频终端 SDK &#xff08;腾讯云视立方&#xff09;的子产品 SDK 之一&#xff0c;提供美颜特效功能。基于优图精准的 AI 能力和天天 P 图丰富的实时特效处理&#xff0c;为各类视频处理场景提供丰富的产品…

SpringCloud-Netflix第一代微服务快速入门

1.springCloud常用组件 Netflix Eureka 当我们的微服务过多的时候&#xff0c;管理服务的通信地址是一个非常麻烦的事情&#xff0c;Eureka就是用来管理微服务的通信地址清单的&#xff0c;有了Eureka之后我们通过服务的名字就能实现服务的调用。 Netflix Ribbon\Feign : 客…

卫星导航定位原理学习(三)

GNSS信号体制及其性能分析 GNSS信号体制直接影响卫星导航系统的性能&#xff0c;是卫星导航系统设计的重要内容。卫星导航信号体制主要包括信号频率、信号结构、导航电文3部分。其中信号结构又包括调制波形、频率带宽、扩频码码长、码速率、码结构、信号功率等内容。导航电文设…

8086介绍

内部结构 执行部件EU&#xff08;Execution Unit&#xff09; 包含运算器、通用寄存器组、EU控制单元。 只负责控制&#xff0c;不和外部总线打交道 总线接口部件BIU&#xff08;Bus Interface Unit&#xff09; 包含指令队列缓冲器、16位指令指针寄存器IP、16位段寄存器&am…

【L波段差分干涉SAR卫星(陆地探测一号01组)】

L波段差分干涉SAR卫星&#xff08;陆地探测一号01组&#xff09; L波段差分干涉SAR卫星&#xff08;陆地探测一号01组&#xff09;是我国自主研发的重要卫星系统&#xff0c;以下是对该卫星的详细介绍&#xff1a; 一、基本信息 卫星组成&#xff1a;陆地探测一号01组由A星…

全网最适合入门的面向对象编程教程:53 Python字符串与序列化-字符串与字符编码

全网最适合入门的面向对象编程教程&#xff1a;53 Python 字符串与序列化-字符串与字符编码 摘要&#xff1a; 在 Python 中&#xff0c;字符串是文本的表示&#xff0c;默认使用 Unicode 编码&#xff0c;这允许你处理各种字符集&#xff0c;字符编码是将字符转换为字节的规则…

一文上手SpringSecurity【三】

一、认证流程分析 上篇文章当中,我们一步一步查阅源码方式对认证流程有了一些认证,本章节梳理一下整个流程,最后形成一张图,以更直观的方式来理解认证的整个流程. 1.1 认证当中步及的接口和类 1.1.1 【抽象类】AbstractAuthenticationProcessingFilter 实现了GenericFilter…

OFDM通信系统发射端需要做ifftshift的原因分析

对频率为15Hz的正弦波信号进行FFT分析&#xff0c;并且直接画图&#xff0c;matlab代码如下&#xff1a; fs 100; % sampling frequency t 0:(1/fs):(10-1/fs); % time vector S cos(2*pi*15*t); n length(S); X fft(S); f (0:n-1)*(fs/n); %frequenc…

使用canvas截取web camera指定区域,并生成图片

目标&#xff0c;截取红色色块背后的视频区域。 代码结构如下&#xff1a; <div id"p1"><video id"v1" autoplay playsinline></video><div id"mrz"></div><canvas id"captureCanvas"></can…

优化|深入解读DeepOPF:一种用于安全约束直流最优潮流问题的深度神经网络方法

原文信息&#xff08;包括题目、发表期刊、原文链接等&#xff09;&#xff1a; DeepOPF: A Deep Neural Network Approach for Security-Constrained DC Optimal Power Flow https://ieeexplore.ieee.org/document/9205647 原文作者&#xff1a;Xiang Pan; Tianyu Zhao; Ming…