html2pdf-chrome:现阶段效果最佳的一个 HTML 转 PDF 的服务,使用无任何语言限制,再开发需使用go语言
html2pdf-chrome:一个 HTML 转 PDF 的 Go 库 / 服务
Go + Chrome Headless + CDP,支持连接池复用、可组合等待策略、Docker 部署。
约 3400 行 Go,无 Node 依赖。
仓库反正是开源的,写这个贴子主要分享一下技术方案和踩过的坑,给有类似需求的同学一个参考。
现阶段效果最佳的,直接想使用的同学请看章节不用 Go 也能用,仅下载html2pdf-server,在服务器安装chrome即可。
仓库: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.navigate 和 Page.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(获取实例):
- 从 idle 队列 pop → 做健康检查(HTTP 探
/json/version) - 空闲 + 未达上限 → 新建实例
- 已达上限 →
cond.Wait()阻塞,等别人归还
Release(归还实例):
taskCount >= MaxTasksPerInstance(默认 100) → 销毁重建(防 Chrome 内存泄漏)- 不健康 → 销毁重建
- 正常 → 放回 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"
不用 Go 也能用
虽然库本身是 Go 写的,但实际使用者完全不需要懂 Go。
启动一个 HTTP 服务(一行命令),然后任何语言都能用:
# 启动服务
html2pdf-server -addr :8080 -max-instances 4 -min-instances 2 -no-sandbox
# 任何语言调用(curl / Python requests / Java HttpClient / Node fetch / ...)
curl -X POST localhost:8080/convert \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com","paper":"a4"}' \
-o output.pdf
没有语言限制,没有 SDK 依赖。Docker 镜像拉下来就能跑,K8s 上就是一个 Deployment + Service。
CLI / HTTP / Go 库三种模式
三种模式复用同一条渲染链路:
CLI(本地 / 脚本):
html2pdf-chrome -url https://example.com -paper a4 -landscape -out output.pdf
HTTP 服务(微服务):
curl -X POST localhost:8080/convert \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com","paper":"a4","waitNetworkIdle":true}' \
-o output.pdf
服务内部用连接池,/health 端点返回 {"idle":2,"active":1,"total":3}。
Go 库(嵌入式):
converter, _ := html2pdf.NewConverter(html2pdf.ConverterConfig{
MaxInstances: 4, MinInstances: 2,
})
defer converter.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 + 阿里云镜像),给国内服务器用
一些踩坑记录
- Chrome headless 模式:用
--headless=new而不是旧版--headless,新版的渲染行为和 GUI 模式一致,Canvas/SVG 不会出现偏移。 - WebSocket 断开重连:chromedp 的
RemoteAllocator不负责断线重连,每次任务结束cancel()关闭 Tab,下次任务重新Connect建新 Tab,避免重连问题。 - PDF stream 模式:默认 base64 返回对大 PDF(50MB+)可能 OOM,stream 模式通过 IO 流分块读取,适合大文件场景。
- 进程退出信号:Chrome 有时不会正常退出,
Close()直接用Kill而不是Signal(os.Interrupt)确保清理。不优雅但可靠。 - 共享内存:Docker 里 Chrome 需要
/dev/shm,否则页面加载会 hang。K8s 里需要设shm-size或挂载emptyDir。
当前状态和规划
当前约 3400 行 Go,34 个文件。已经能跑生产,但还有几个明显短板:
- 错误处理是扁平 wrap,没分类。计划加
ErrorCode枚举,让调用方能做类型判断(启动失败 vs 渲染超时) - 缺可观测性:没 metrics,没结构化日志。需要加 Prometheus 指标和 slog
- 平台验证:路径查找逻辑写了 macOS/Linux/Windows 三套,但只在 macOS 测过
- 没有发布工程:缺交叉编译脚本、版本注入、自动发版
欢迎 issue 和 PR。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)