项目背景
笔者所在团队开发了一个智能报告系统,其提供客户服务概况、智能问答(借助ChatGPT)等能力。其中存在导出报告的场景,需要重新实现。通过前期调研,最终确定了以无头浏览器模拟访问报告页面,并调用打印能力,将HTML输出为PDF。
技术方案
HTML To PDF,社区还是有很多成熟方案的。
纯客户端方案
Google or 百度一下html2pdf,有大把方案。但是这些纯客户端方案存在一个致命不足:无法打印iframe里的内容。尤其当iframe里的页面是跨域页面时,更是无能为力。
无头浏览器方案
作为纯客户端方案的替换,使用无头浏览器在服务层(node、php、python、go)模拟页面访问并打印页面,可以有效解决iframe空白问题。
这时候会有同学有疑问,用无头浏览器打印页面也会有iframe空白问题。如何解决呢?
如果有iframe,那就需要先将HTML转换为image,然后把image转换为PDF。(image怎么来呢?截图大法了解一下,不要用render方式进行转换,只要涉及render的,都无法处理iframe问题,采用物理截图才是正解)。
方案实现
作为一名刚入门的前端工程师,自然而然就会想到用nodejs+puppeteer实现无头浏览器服务。考虑到后续的其他页面控制场景(比如页面性能检测、报告质检、报告内容提前爬取等),决定搭一个express服务提供http API。
编码实现
首先,需要明确能力边界,期望的导出PDF文档应该具备以下特征:
- 基本还原页面样式
- 能够完整导出页面
- 体积足够小,提高用户下载速度
对于第一点,最终的实现其实是有取舍的。tcbi服务报告支持自适应终端,也就是说可以实现同一份报告在PC端和移动端都能正常展示,并且保证样式正常。
考虑到PDF文档页面一般为A4,更加匹配PC端的样式,因此跟产品同学讨论后决定所有报告都有PC端样式导出,而不用去做移动端的样式兼容(其实在开发阶段做了兼容,结果就是导出的PDF页面两边留白,反而不美观)。
关于第三点,因为采用的是puppeteer原生的Page.pdf API,其体积还是很小的,包含大量echarts图表的5页报告只有不到700kb。
所以,主要的技术难点在于如何完整地导出页面。
前置准备
考虑到页面的一次性(访问一次就会close),不需要进行优化,而express服务中的Browser实例完全可以多次复用,以减少开启、关闭Browser的开销。决定采用单例模式提供一个Browser工具函数,用来创建唯一的browser实例,并且基于该函数再提供一个创建Page实例的方法&#