当前位置: 首页 > news >正文

html2pdf-chrome:一个 HTML 转 PDF 的 Go 库 / 服务,依旧是现阶段效果最佳的

html2pdf-chrome:一个 HTML 转 PDF 的 Go 库 / 服务

Go + Chrome Headless + CDP,支持连接池复用、可组合等待策略、Docker 部署。
约 3400 行 Go,无 Node 依赖。

仓库反正是开源的,写这个贴子主要分享一下技术方案和踩过的坑,给有类似需求的同学一个参考。
现阶段效果最佳的。
仓库:github.com/PiZhai/html2pdf-chrome

解决了什么问题

服务端 HTML 转 PDF 的几种常见方案:

方案优势短板
Puppeteer/Playwright渲染保真,生态成熟需要 Node 运行时,启动开销大
wkhtmltopdf资源开销低不支持现代 CSS(Grid/Flexbox)、Canvas
纯 Go 库 (unidoc 等)纯 Go,静态二进制不是真实浏览器,复杂页面易翻车
Selenium WebDriver真实浏览器重,速度慢

我们的需求场景:日均数万次转换,Go 技术栈,需要在 Kubernetes 上运行,页面含 Canvas 图表和 Web Font。

结论:必须用真实浏览器引擎,但不想引入 Node 运行时。方案是直接走 CDP 协议驱动 Chrome Headless。

实现原理

一句话:Go 程序通过 WebSocket 连接 Chrome 的调试端口,用 CDP 协议下发Page.navigatePage.printToPDF命令,Chrome 负责渲染,Go 程序只做编排。

Request → Go → WebSocket → Chrome Headless → Page.printToPDF → PDF 文件

CDP 通信层用了 chromedp,Go 生态里最成熟的选择。

连接池设计(重点)

v0.1 版每次调用启动一个 Chrome,用完就关。适合 CLI,不适合服务端——Chrome 冷启动 1-3 秒,内存开销几百 MB。

v0.2 核心改动:Chrome 实例池

Pool { idle: []*Instance // 空闲队列 activeCount int // 使用中 totalCount int // 存活总数 mu: sync.Mutex cond: *sync.Cond // 阻塞等待 }

Acquire(获取实例):

  1. 从 idle 队列 pop → 做健康检查(HTTP 探/json/version
  2. 空闲 + 未达上限 → 新建实例
  3. 已达上限 →cond.Wait()阻塞,等别人归还

Release(归还实例):

  1. taskCount >= MaxTasksPerInstance(默认 100) → 销毁重建(防 Chrome 内存泄漏)
  2. 不健康 → 销毁重建
  3. 正常 → 放回 idle 队列,cond.Broadcast()唤醒等待者

后台 reaper:每 30 秒扫一次,空闲超过IdleTimeout(默认 5 分钟)的被回收,但至少保留MinInstances个热备。

关键细节:

  • 新建实例前先totalCount++预留槽位,防止并发冲破上限
  • 每个请求用 chromedp 创建独立 Tab(NewContext),Tab 之间不共享 cookie/storage
  • Tab 关闭后 Chrome 自己回收内存,不会累积

等待策略

静态页面简单——等readyState === "complete"。但实际页面经常需要等更多东西:

Navigate → WaitReady("body") → WaitDocumentReady(15s) // readyState === "complete" → WaitFontsReady(10s) // document.fonts.status === "loaded" → [可选] WaitNetworkIdle(idle, timeout) // 网络空闲 + 静默期 → [可选] WaitVisible(selector) // CSS 选择器可见 → [可选] WaitForExpression(expr, timeout) // 自定义 JS 条件 → printToPDF

网络空闲检测的原理:Enable Network domain → 监听requestWillBeSent/loadingFinished/loadingFailed→ 维护 inflight counter → 归零后静默 500ms → 判定空闲。

忽略了 WebSocket、data:blob:请求。如果页面有 SSE 长轮询,用-wait-expression替代:

-wait-expression"document.querySelector('#chart') !== null"

CLI / HTTP / Go 库三种模式

三种模式复用同一条渲染链路:

CLI(本地 / 脚本):

html2pdf-chrome-urlhttps://example.com-papera4-landscape-outoutput.pdf

HTTP 服务(微服务):

curl-XPOST localhost:8080/convert\-H"Content-Type: application/json"\-d'{"url":"https://example.com","paper":"a4","waitNetworkIdle":true}'\-ooutput.pdf

服务内部用连接池,/health 端点返回{"idle":2,"active":1,"total":3}

Go 库(嵌入式):

converter,_:=html2pdf.NewConverter(html2pdf.ConverterConfig{MaxInstances:4,MinInstances:2,})deferconverter.Close()converter.Convert(html2pdf.Request{URL:"https://example.com",OutputPath:"./out.pdf",})

Docker 部署

docker build -t html2pdf-chrome . docker run --rm html2pdf-chrome -url https://example.com -out /app/output/out.pdf

镜像特点:

  • Noto CJK + STIX 数学 + DejaVu + Emoji → 中英数全覆盖
  • --no-sandbox默认开启(容器本身是隔离层,不需要 Chrome 再开沙箱)
  • 另有Dockerfile.cn(Chromium + 阿里云镜像),给国内服务器用

一些踩坑记录

  1. Chrome headless 模式:用--headless=new而不是旧版--headless,新版的渲染行为和 GUI 模式一致,Canvas/SVG 不会出现偏移。
  2. WebSocket 断开重连:chromedp 的RemoteAllocator不负责断线重连,每次任务结束cancel()关闭 Tab,下次任务重新Connect建新 Tab,避免重连问题。
  3. PDF stream 模式:默认 base64 返回对大 PDF(50MB+)可能 OOM,stream 模式通过 IO 流分块读取,适合大文件场景。
  4. 进程退出信号:Chrome 有时不会正常退出,Close()直接用Kill而不是Signal(os.Interrupt)确保清理。不优雅但可靠。
  5. 共享内存:Docker 里 Chrome 需要/dev/shm,否则页面加载会 hang。K8s 里需要设shm-size或挂载emptyDir

当前状态和规划

当前约 3400 行 Go,34 个文件。已经能跑生产,但还有几个明显短板:

  • 错误处理是扁平 wrap,没分类。计划加ErrorCode枚举,让调用方能做类型判断(启动失败 vs 渲染超时)
  • 缺可观测性:没 metrics,没结构化日志。需要加 Prometheus 指标和 slog
  • 平台验证:路径查找逻辑写了 macOS/Linux/Windows 三套,但只在 macOS 测过
  • 没有发布工程:缺交叉编译脚本、版本注入、自动发版

欢迎 issue 和 PR。


仓库:github.com/PiZhai/html2pdf-chrome

http://www.cnnetsun.cn/news/2566423.html

相关文章:

  • Unity JSON解析救星:Newtonsoft.Json-for-Unity实战指南
  • C++基础 类和对象(三)
  • 别再折腾驱动了!用DKMS一劳永逸管理你的Linux网卡(以RTL8822CE/Ubuntu 18.04为例)
  • 别再死记硬背了!用Wirtinger导数轻松搞定复数求导(附Python代码验证)
  • 别再傻等自动下载了!手把手教你从国内镜像站搞定Wine 5.0的mono和gecko插件
  • LOOKAHEAD REASONING:大型推理模型的并行加速技术
  • RK3588 Debian 系统安装与WiFi/SSH配置笔记
  • FPG财盛国际:从风险提示看平台责任意识
  • Linux系统启动慢?从UEFI的DXE阶段入手,优化驱动加载让你的开机快人一步
  • 【复现】中国上市公司全要素生产率测算与分析(论文+数据)
  • 从Sora 2原始张量到可交付MP4:端到端Pipeline中被92%开发者忽略的色彩空间转换断点(BT.2020→BT.709→sRGB三级校准手册)
  • 【Claude AI深度SWOT解码】:20年AI架构师亲授,4大维度拆解其商用致命短板与突围路径
  • 你的副业计划又黄了。不是意志力的锅
  • 基于ESP32打造智能网络收音机:硬件选型、软件实现与音质优化全攻略
  • ESP32多任务水位监测:从Arduino到ESP-IDF的FreeRTOS实战
  • 高频率登录尝试 ip封禁已经实现
  • 给服务器添加最外层风控系统
  • 基于ESP8266与WS2812B的智能氛围灯DIY:从硬件连接到Web控制
  • 基于STM32WB与BLE-MIDI的体感节奏控制器:BeatShaker设计与实现
  • AMD锐龙SDT调试工具终极指南:5个进阶技巧解锁处理器深度调优
  • Linux——进程和线程
  • Linux服务器被挖矿木马劫持的五步应急处置指南
  • 基于放射性衰变的真随机数生成器:从量子物理到嵌入式实现
  • ‌2026智慧校园规划必读:如何在预算吃紧下选到高性价比方案‌
  • 抖音批量下载神器:douyin-downloader 免费工具全攻略
  • Lovable电商网站搭建陷阱大全(2024最新版):Nuxt 3 SSR失效、Stripe Webhook丢包、SEO结构坍塌三大隐形杀手曝光
  • 惠普战99新机踩坑记:Win11家庭版下VMware装Ubuntu,键盘延迟1秒怎么破?
  • AI写的论文双率如何压到20%以下?这几款工具实测有效
  • 基于TTP223的离线电容触摸开关设计:厨房灯控DIY方案
  • 转行网络安全运维:从0到1的可落地指南