html 转 pdf 技术方案调研

近期项目需要实现将 html 页面转换成 pdf 报告的需求,经过一番调研以及结合过往经验,发现了有如下三种技术方案,本篇文章主要内容是基于这三种不同方案的 demo 实现及对比。

方案选型

  • html2canvas + jspdf
  • wkhtmltopdf
  • node + puppeteer

准备工作

利用 chatgpt 生成一个报告的 html 模板,每页包含了一个表格和一个由 echarts 生成的柱状图。实际项目中基本也是这样的内容,文字 + charts,所以用这个来测试具有一定的参考意义,生成的 html 模板如下:

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Report Page</title>
    <!-- ECharts库 -->
    <script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
    <style>
      body {
        font-family: Arial, sans-serif;
        margin: 20px;
      }
      .page {
        margin-bottom: 40px;
      }
      .chart {
        width: 600px;
        height: 400px;
        margin-top: 20px;
      }
      table {
        width: 100%;
        border-collapse: collapse;
      }
      th,
      td {
        text-align: left;
        padding: 8px;
        border-bottom: 1px solid #ddd;
      }
    </style>
  </head>
  <body>
    <div class="page" id="page1">
      <h2>Page 1 - Data and Chart</h2>
      <table>
        <tr>
          <th>Item</th>
          <th>Value</th>
        </tr>
        <tr>
          <td>Item 1</td>
          <td>100</td>
        </tr>
        <tr>
          <td>Item 2</td>
          <td>200</td>
        </tr>
        <!-- 更多列表数据 -->
      </table>
      <div id="chart1" class="chart"></div>
    </div>    

    <div class="page" id="page2">
      <table>
        <tr>
          <th>Item</th>
          <th>Value</th>
        </tr>
        <tr>
          <td>Item 1</td>
          <td>100</td>
        </tr>
        <tr>
          <td>Item 2</td>
          <td>200</td>
        </tr>
        <!-- 更多列表数据 -->
      </table>
      <h2>Page 2 - Data and Chart</h2>
      <!-- 类似的列表和图表占位符 -->
      <div id="chart2" class="chart"></div>
    </div>    

    <div class="page" id="page3">
      <table>
        <tr>
          <th>Item</th>
          <th>Value</th>
        </tr>
        <tr>
          <td>Item 1</td>
          <td>100</td>
        </tr>
        <tr>
          <td>Item 2</td>
          <td>200</td>
        </tr>
        <!-- 更多列表数据 -->
      </table>
      <h2>Page 3 - Data and Chart</h2>
      <!-- 类似的列表和图表占位符 -->
      <div id="chart3" class="chart"></div>
    </div>

    <script>
      // ECharts 图表示例
      var chart1 = echarts.init(document.getElementById('chart1'));
      var option1 = {
        title: {text: 'ECharts 示例 1'},
        tooltip: {},
        xAxis: {data: ['类别1', '类别2', '类别3', '类别4']},
        yAxis: {},
        series: [{type: 'bar', data: [5, 20, 36, 10]}]
      };
      chart1.setOption(option1);

      // 为page2和page3重复上面的过程,设置不同的option配置
      var chart2 = echarts.init(document.getElementById('chart2'));
      chart2.setOption(option1);
      var chart3 = echarts.init(document.getElementById('chart3'));
      chart3.setOption(option1);
    </script>
  </body>
</html>

使用 http-server 在 html 页面目录下启动一个 web 服务器(没有 http-server, 参考这里安装 www.npmjs.com/package/htt...)

bash 复制代码
http-server . -p 9090

访问 http://127.0.0.1:9090/report.html,即可看到对应的报告模板页面,如下:

wkhtmltopdf

到这里 wkhtmltopdf.org/downloads.h... 下载系统对应版本的安装包,然后安装好

执行如下命令,即可生成 pdf

bash 复制代码
wkhtmltopdf http://127.0.0.1:9090/report.html report.pdf

查看生成的 pdf 内容,能正常显示,看起来也没啥问题

实际报告肯定是有多页的,接下来我们尝试一下看看如何分页。在 page1 和 page2 元素后分别加入一个
元素,对应的 class 如下:

css 复制代码
.page-break-after {
	page-break-after: always;
}

然后再次执行转换命令,得到的 pdf 如下:

问题

  1. 偶尔会卡在某个进度不动,不知道为啥
  1. 执行时间不稳定,时快时慢

putteteer

安装就不多说明了,自行 google。安装好后,同样让 chatgpt 生成一段测试的代码,如下:

javascript 复制代码
const puppeteer = require('puppeteer');

async function savePageAsPDF(url, outputPath) {
  // 启动浏览器
  const browser = await puppeteer.launch();
  // 打开新页面
  const page = await browser.newPage();
  // 导航到指定URL
  await page.goto(url, { waitUntil: 'networkidle2' });
  // 保存页面为PDF
  await page.pdf({ path: outputPath, format: 'A4' });
  // 关闭浏览器
  await browser.close();
  console.log(`PDF已保存到:${outputPath}`);
}

// 调用函数保存网页为PDF
savePageAsPDF('http://127.0.0.1:9090/report.html', 'report1.pdf').catch(console.error);

执行转换脚本

bash 复制代码
node index.js

得到的 pdf 内容如下,看起来也没啥问题

html2canvas + jspdf

引入 html2canvas 和 jspdf 相关脚本

html 复制代码
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.3.2/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.3.1/jspdf.umd.min.js"></script>

添加下载按钮及转换函数

html 复制代码
<button onclick="convertToPDF()">Convert to PDF</button>

<script>
		async function addPage(id, pdf) {
			const element = document.getElementById(id);

			// 使用 html2canvas 渲染指定元素
			const canvas = await html2canvas(element, {scale: 2});
			const imgData = canvas.toDataURL('image/png');

			// 使用 jsPDF 创建PDF文档
			const imgProps = pdf.getImageProperties(imgData);
			const pdfWidth = pdf.internal.pageSize.getWidth();
			// const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width;
			const pdfHeight = (imgProps.height / imgProps.width) * pdfWidth;
			// const pdfHeight = pdf.internal.pageSize.getHeight();
			console.log(imgProps.width, pdfWidth, pdfHeight, pdf.internal.pageSize.getHeight());
			pdf.addImage(imgData, 'JPEG', 0, 0, pdfWidth, pdfHeight, 'NONE', 'FAST');
		}

		async function convertToPDF() {
			const pdf = new window.jspdf.jsPDF();

			await addPage('page1', pdf);
			pdf.addPage();
			await addPage('page2', pdf);
			pdf.addPage();
			await addPage('page3', pdf);

			pdf.save('converted.pdf');
		}
</script>

访问页面,点击按钮即可下载 pdf 了,内容如下:

方案对比

  • wkhtmltopdf,依赖后端,配置比较灵活,但是速度好像不稳定
  • pupeteer,依赖后端,生成的 pdf 比较准确,质量也还可以
  • html2canvas + jspdf,纯前端实现,依赖少

综上,本着简单至上的原则,项目暂时敲定使用 html2canvas + jspdf 的技术方案,后续在项目应用中的踩坑记录会在后续再更新,敬请期待。

参考

相关推荐
MarkHD2 小时前
javascript 常见设计模式
开发语言·javascript·设计模式
baozongwi2 小时前
ctfshow web入门 nodejs web334--web337
web安全·node.js
托尼沙滩裤3 小时前
【js面试题】js的数据结构
前端·javascript·数据结构
朝阳393 小时前
vue3【实战】来回拖拽放置图片
javascript·vue.js
不如喫茶去3 小时前
VUE自定义新增、复制、删除dom元素
前端·javascript·vue.js
阿垚啊4 小时前
vue事件参数
前端·javascript·vue.js
加仑小铁4 小时前
【区分vue2和vue3下的element UI Dialog 对话框组件,分别详细介绍属性,事件,方法如何使用,并举例】
javascript·vue.js·ui
Simaoya5 小时前
vue判断元素滚动到底部后加载更多
前端·javascript·vue.js
头顶一只喵喵5 小时前
Vue基础知识:Vue3.3出现的defineOptions,如何使用,解决了什么问题?
前端·javascript·vue.js·vue3
掘金安东尼5 小时前
上周前端发生哪些新鲜事儿?#370
前端·javascript·面试