用 Rust 与 Typst 重建 PDF 生成链路:从秒级渲染到毫秒级文档基础设施

2026-06-29 41 预计阅读时间: 1 分钟
来源: infoq.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

在银行、制造业这类强监管行业里,PDF 不是“导出按钮”的附属品,而是合同、账单、审计记录和生产文档的最终形态。Erik Steiger 在分享中谈到的痛点很典型:传统 PDF 生成链路依赖 Puppeteer、LaTeX 这类重量级引擎,机器吃得多、冷启动慢、排障困难;一旦模板版本和业务数据对不上,合规团队很难回答“当时到底生成了什么”。

他的方向是把文档生成当成现代基础设施来做:Rust 负责服务端执行与隔离,Typst 负责高性能排版,模板注册表借鉴 Git 和 Docker 的版本化思路。目标不是把 PDF 做得更花哨,而是让它更快、更可追溯、更容易在事故发生时复现。

为什么 Puppeteer 和 LaTeX 会拖慢文档系统

很多团队最初选择 Puppeteer,是因为 HTML/CSS 人才多,预览也方便。但当 PDF 生成成为高频后台任务时,问题会逐渐暴露:

  • 浏览器进程重,容器镜像大,冷启动成本高。
  • 字体、分页、打印 CSS 在不同环境里容易漂移。
  • 并发扩容时,CPU 和内存压力非常明显。
  • 生成失败时,定位问题常常要同时看 HTML、CSS、浏览器日志和业务数据。

LaTeX 在学术和复杂排版上很强,但在企业后台里也有类似负担:依赖链长、编译模型偏重、错误信息对业务工程师不友好。对于银行账单、保单、发票、制造工单这类结构化文档,很多场景并不需要一个完整浏览器或传统 TeX 发行版。

Typst 的吸引力正在这里:它把排版能力和现代工程体验结合起来,语法更轻,编译路径更适合嵌入服务。配合 Rust,文档渲染可以从“启动一个庞大外部进程”变成“调用一个可控的库或轻量服务”。分享中提到,迁移到基于 Rust 与 Typst 的 serverless 架构后,渲染延迟可以降到 2ms 以下;这个数字背后的关键不是单点优化,而是整体架构减重。

把模板当成镜像,而不是散落的文件

受监管行业最怕的不是模板改错,而是改错以后说不清楚。一个现代文档平台至少要回答四个问题:

  • 这份 PDF 使用了哪个模板版本?
  • 模板依赖了哪些字体、组件和局部片段?
  • 生成时输入数据是什么 schema?
  • 如果客户或审计要求复现,能否得到同一份输出?

Erik 提到的模板注册表思路,很像 Git 与 Docker 的结合:模板不只是一个文件,而是一个带版本、摘要、依赖和发布状态的 artifact。业务系统不应该引用“最新版合同模板”,而应该引用一个不可变的版本,例如 contract@sha256:...invoice:v2024-10-01

可以这样设计模板元数据:

name: monthly-statement
version: 2024.10.3
engine: typst
entrypoint: main.typ
schema: schema.json
fonts:
  - Inter
  - Noto Sans CJK SC
labels:
  domain: banking
  owner: document-platform
  compliance_review: approved

这类元数据的价值在事故排查时会放大:如果某批客户账单出现分页异常,平台可以快速定位对应模板版本、输入 schema、字体依赖和发布时间,而不是在共享目录里翻找“final_v7_fixed_new.typ”。

一个可改造的 Typst 模板与渲染命令

下面是一个最小 Typst 模板示例,适合用来理解“结构化数据 + 模板版本 + 可重复渲染”的工作流。运行前需要安装 Typst CLI,并把示例保存为 main.typ

#set page(margin: 18mm)
#set text(font: "Noto Sans CJK SC", size: 10pt)

#let statement(customer, period, items) = [
  = 月度账单

  客户:#customer  \
  账期:#period

  #table(
    columns: (1fr, 80pt),
    inset: 8pt,
    [项目], [金额],
    ..items.map(item => (
      [#item.name], [#item.amount]
    )).flatten()
  )
]

#statement(
  customer: "上海某制造有限公司",
  period: "2024-10",
  items: (
    (name: "设备租赁", amount: "¥12,000"),
    (name: "维护服务", amount: "¥3,500"),
    (name: "税费", amount: "¥930"),
  ),
)

本地渲染:

typst compile main.typ statement.pdf

在真实系统里,业务数据通常不会直接写进模板文件,而是由服务端注入。可以先用一个简单目录结构模拟模板注册表:

mkdir -p registry/monthly-statement/2024.10.3
cp main.typ registry/monthly-statement/2024.10.3/main.typ
sha256sum registry/monthly-statement/2024.10.3/main.typ > registry/monthly-statement/2024.10.3/SHA256SUMS
cat registry/monthly-statement/2024.10.3/SHA256SUMS

这并不是完整平台,但它体现了核心习惯:模板发布后生成摘要,业务侧记录版本和摘要,出问题时用同一份 artifact 复现。

Rust 服务层应该负责什么

Rust 不只是为了“快”。在文档平台里,它更适合承担边界清晰、资源敏感、需要稳定并发的部分:

  • 接收渲染请求并校验输入 schema。
  • 根据模板名和版本从注册表拉取 artifact。
  • 管理字体、缓存和沙箱执行环境。
  • 调用 Typst 渲染并返回 PDF 字节流。
  • 写入审计日志,包括模板摘要、输入摘要、耗时和调用方。

可以把服务接口设计得非常朴素。例如一个后端渲染 API 可以长这样:

curl -X POST http://localhost:8080/render \
  -H 'content-type: application/json' \
  -d '{
    "template": "monthly-statement",
    "version": "2024.10.3",
    "data": {
      "customer": "上海某制造有限公司",
      "period": "2024-10",
      "items": [
        {"name": "设备租赁", "amount": "¥12,000"},
        {"name": "维护服务", "amount": "¥3,500"}
      ]
    }
  }' \
  --output statement.pdf

即使底层实现未来从 CLI 调用改成嵌入式库,API 也不必变化。对上游业务系统来说,重要的是模板版本、输入数据和输出 PDF 的契约稳定。

采用前的检查清单

这类迁移最容易失败在“只替换渲染引擎”。如果仍然没有版本化、没有审计日志、没有可复现构建,那么从 Puppeteer 换到 Typst 只能解决一部分性能问题。

落地时可以按这份清单推进:

  • 先选高频、结构稳定、排版复杂度中等的 PDF 作为试点。
  • 为模板建立不可变版本,不允许生产系统引用浮动的 latest
  • 把字体纳入 artifact 或基础镜像,避免环境漂移。
  • 为每次渲染记录模板摘要、输入摘要、耗时和调用方。
  • 保留旧链路一段时间,用双跑结果比较分页、金额和关键字段。
  • 明确 Typst 的边界:极端复杂的浏览器特性、动态前端组件和特殊交互式 PDF 可能仍需其他方案。

Rust 与 Typst 的组合真正改变的不是“PDF 怎么画”,而是“文档系统怎么运营”。当模板像镜像一样发布,渲染像函数一样轻量,审计像日志一样自然,PDF 生成就从脆弱的后台脚本变成了可治理的基础设施。


相关推荐